Public release from ruodoo-project: 19.0 - 2026-05-10 21:19:01 UTC
This commit is contained in:
103
account_move_templates_invoice/README.rst
Normal file
103
account_move_templates_invoice/README.rst
Normal file
@ -0,0 +1,103 @@
|
||||
====================================================
|
||||
Шаблоны типовых операций - Интеграция с инвойсами
|
||||
====================================================
|
||||
|
||||
.. |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:
|
||||
|
||||
Возможности
|
||||
===========
|
||||
|
||||
* Автоматическое применение шаблонов при проведении инвойсов
|
||||
* Замена стандартного распределения по счетам на распределение из шаблона
|
||||
* Поддержка инвойсов клиентов и счетов поставщиков
|
||||
* Настройка через параметры системы
|
||||
* Сохранение строк инвойса с обновлением счетов в проводках
|
||||
* Автоматическое создание строк оплаты с датами погашения
|
||||
|
||||
Настройка
|
||||
=========
|
||||
|
||||
Для настройки модуля необходимо:
|
||||
|
||||
#. Перейти в *Бухгалтерия > Настройка > Параметры*
|
||||
#. В разделе "Шаблоны типовых операций" включить опцию "Использовать шаблоны для инвойсов"
|
||||
#. Сохранить настройки
|
||||
|
||||
Настройка шаблонов:
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
При создании шаблонов для инвойсов настройте типы строк:
|
||||
|
||||
* **Продуктовая строка**: Заменяет счёт в существующих продуктовых строках (счета доходов/расходов)
|
||||
* **Строка оплаты**: Создаёт строки дебиторки/кредиторки с датами погашения
|
||||
|
||||
Использование
|
||||
=============
|
||||
|
||||
После настройки модуль работает автоматически:
|
||||
|
||||
#. Создайте инвойс клиента или счёт поставщика
|
||||
#. Добавьте строки инвойса как обычно
|
||||
#. Выберите шаблон типовой операции в поле "Шаблон типовой операции"
|
||||
#. Нажмите "Провести"
|
||||
|
||||
Модуль выполнит следующие действия:
|
||||
|
||||
* Удалит стандартные строки условий оплаты
|
||||
* Обновит счета в продуктовых строках согласно шаблону (line_type='product')
|
||||
* Создаст новые строки оплаты согласно шаблону (line_type='payment')
|
||||
* Установит корректные даты погашения для счетов дебиторки/кредиторки
|
||||
* Проведёт инвойс с распределением по счетам из шаблона
|
||||
|
||||
Известные проблемы / Планы развития
|
||||
====================================
|
||||
|
||||
* В настоящее время поддерживаются только типы документов out_invoice и in_invoice
|
||||
* Шаблон должен быть выбран до проведения документа
|
||||
* В планах: Автоматический выбор шаблона на основе свойств инвойса
|
||||
|
||||
Отслеживание ошибок
|
||||
===================
|
||||
|
||||
Ошибки отслеживаются на `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_invoice/__init__.py
Normal file
1
account_move_templates_invoice/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
42
account_move_templates_invoice/__manifest__.py
Normal file
42
account_move_templates_invoice/__manifest__.py
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
'name': 'Accounting Journal Templates - Invoice',
|
||||
'version': '19.0.1.0.0',
|
||||
'summary': 'Интеграция шаблонов типовых операций с Invoice и Vendor Bill',
|
||||
'description': """
|
||||
Шаблоны типовых операций - Интеграция с инвойсами
|
||||
==================================================
|
||||
|
||||
Автоматическое применение шаблонов типовых операций к инвойсам клиентов и счетам
|
||||
поставщиков при проведении, заменяя стандартное распределение по счетам на
|
||||
распределение из шаблона.
|
||||
|
||||
Возможности:
|
||||
------------
|
||||
* Автоматическое применение шаблонов при проведении инвойсов
|
||||
* Замена счетов в продуктовых строках на счета из шаблона
|
||||
* Создание строк условий оплаты с корректными датами погашения
|
||||
* Поддержка инвойсов клиентов и счетов поставщиков
|
||||
* Настройка через параметры системы
|
||||
* Сохранение итоговых сумм инвойса
|
||||
|
||||
Технические детали:
|
||||
-------------------
|
||||
* Расширяет модель account.move
|
||||
* Переопределяет метод action_post()
|
||||
* Обновляет существующие продуктовые строки вместо создания дубликатов
|
||||
* Создаёт строки payment_term для счетов дебиторки/кредиторки
|
||||
* Поддерживает баланс и валидацию инвойса
|
||||
""",
|
||||
'author': 'MK.Lab, RuOdoo',
|
||||
'website': 'https://ruodoo.ru',
|
||||
'category': 'Accounting/Accounting',
|
||||
'depends': ['account', 'account_move_templates'],
|
||||
'data': [
|
||||
'views/account_move_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
'maintainers': ['mklab', 'ruodoo'],
|
||||
}
|
||||
3
account_move_templates_invoice/models/__init__.py
Normal file
3
account_move_templates_invoice/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from . import account_move
|
||||
from . import account_move_line
|
||||
from . import res_config_settings
|
||||
151
account_move_templates_invoice/models/account_move.py
Normal file
151
account_move_templates_invoice/models/account_move.py
Normal file
@ -0,0 +1,151 @@
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = ['account.move', 'account.move.template.mixin']
|
||||
|
||||
move_ids = fields.Many2many(
|
||||
comodel_name='account.move',
|
||||
relation='account_move_template_invoice_move_rel',
|
||||
column1='document_id',
|
||||
column2='move_id',
|
||||
string='Проводки по шаблону',
|
||||
)
|
||||
|
||||
journal_template_id = fields.Many2one(
|
||||
comodel_name='account.move.template',
|
||||
string='Шаблон типовой операции',
|
||||
default=lambda self: self._default_journal_template(),
|
||||
)
|
||||
|
||||
def _default_journal_template(self):
|
||||
use = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'account_move_templates_invoice.use_journal_templates_for_invoices'
|
||||
)
|
||||
if use:
|
||||
return self.env['account.move.template'].search([], limit=1, order='id asc')
|
||||
return False
|
||||
|
||||
def get_move_line_amounts(self):
|
||||
"""Return invoice total as the base amount for the wizard."""
|
||||
self.ensure_one()
|
||||
return [{'name': self.name or '/', 'amount': self.amount_total}]
|
||||
|
||||
def _replace_lines_from_template(self, move):
|
||||
"""
|
||||
Replace ALL lines (product + payment_term) with template lines
|
||||
plus one explicit payment_term line with date_maturity.
|
||||
|
||||
We pass 'balance' explicitly in vals so _get_protected_vals protects
|
||||
it from being overwritten by _sync_invoice.
|
||||
"""
|
||||
_logger.info("TEMPLATE DEBUG: _replace_lines_from_template START for move %s", move.id)
|
||||
template = move.journal_template_id
|
||||
_logger.info("TEMPLATE DEBUG: template = %s", template)
|
||||
base_amount = move.amount_untaxed or move.amount_total
|
||||
_logger.info("TEMPLATE DEBUG: base_amount = %s", base_amount)
|
||||
|
||||
# Determine receivable/payable account
|
||||
partner = move.commercial_partner_id.with_company(move.company_id)
|
||||
if move.is_sale_document(include_receipts=True):
|
||||
term_account = partner.property_account_receivable_id
|
||||
else:
|
||||
term_account = partner.property_account_payable_id
|
||||
_logger.info("TEMPLATE DEBUG: term_account = %s", term_account)
|
||||
|
||||
# due date: use invoice_date_due, invoice_date, or today
|
||||
due_date = (
|
||||
move.invoice_date_due
|
||||
or move.invoice_date
|
||||
or fields.Date.context_today(move)
|
||||
)
|
||||
_logger.info("TEMPLATE DEBUG: due_date = %s", due_date)
|
||||
|
||||
# Remove only payment_term lines, keep product lines from invoice_line_ids
|
||||
_logger.info("TEMPLATE DEBUG: Removing existing payment_term lines")
|
||||
lines_to_remove = move.line_ids.filtered(lambda l: l.display_type == 'payment_term')
|
||||
_logger.info("TEMPLATE DEBUG: Lines to remove: %s", [(l.id, l.account_id.code, l.display_type) for l in lines_to_remove])
|
||||
lines_to_remove.with_context(
|
||||
dynamic_unlink=True,
|
||||
check_move_validity=False,
|
||||
skip_invoice_sync=True,
|
||||
).unlink()
|
||||
_logger.info("TEMPLATE DEBUG: payment_term lines removed")
|
||||
|
||||
AML = self.env['account.move.line']
|
||||
ctx = dict(check_move_validity=False, skip_invoice_sync=True)
|
||||
|
||||
# 1. Process template lines based on line_type
|
||||
_logger.info("TEMPLATE DEBUG: Processing template lines")
|
||||
|
||||
# Get existing product lines from invoice_line_ids
|
||||
product_lines = move.line_ids.filtered(lambda l: l.display_type == 'product')
|
||||
_logger.info("TEMPLATE DEBUG: Found %d product lines", len(product_lines))
|
||||
|
||||
# Separate template lines by type
|
||||
template_product_lines = template.line_ids.filtered(lambda l: l.line_type == 'product')
|
||||
template_payment_lines = template.line_ids.filtered(lambda l: l.line_type == 'payment')
|
||||
|
||||
_logger.info("TEMPLATE DEBUG: Template has %d product lines and %d payment lines",
|
||||
len(template_product_lines), len(template_payment_lines))
|
||||
|
||||
# Update existing product lines with accounts from template (line_type='product')
|
||||
for idx, product_line in enumerate(product_lines):
|
||||
if idx < len(template_product_lines):
|
||||
tpl_line = template_product_lines[idx]
|
||||
_logger.info("TEMPLATE DEBUG: Updating product line %d: old account=%s, new account=%s",
|
||||
product_line.id, product_line.account_id.code, tpl_line.account_id.code)
|
||||
product_line.with_context(**ctx).write({'account_id': tpl_line.account_id.id})
|
||||
|
||||
# Create payment lines from template (line_type='payment')
|
||||
_logger.info("TEMPLATE DEBUG: Creating payment lines from template")
|
||||
for tpl_line in template_payment_lines:
|
||||
line_amount = base_amount * tpl_line.percent / 100.0
|
||||
balance = line_amount if tpl_line.move_type == 'debit' else -line_amount
|
||||
_logger.info("TEMPLATE DEBUG: Creating payment line: account=%s, balance=%s",
|
||||
tpl_line.account_id.id, balance)
|
||||
|
||||
vals = {
|
||||
'move_id': move.id,
|
||||
'account_id': tpl_line.account_id.id,
|
||||
'name': template.name,
|
||||
'balance': balance,
|
||||
'amount_currency': balance,
|
||||
'date_maturity': due_date,
|
||||
'display_type': 'payment_term',
|
||||
}
|
||||
|
||||
AML.with_context(**ctx).create(vals)
|
||||
_logger.info("TEMPLATE DEBUG: Template lines processed")
|
||||
|
||||
_logger.info("TEMPLATE DEBUG: _replace_lines_from_template END")
|
||||
|
||||
def action_post(self):
|
||||
_logger.info("TEMPLATE DEBUG: action_post called for moves: %s", self.ids)
|
||||
use_templates = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'account_move_templates_invoice.use_journal_templates_for_invoices'
|
||||
)
|
||||
_logger.info("TEMPLATE DEBUG: use_templates = %s", use_templates)
|
||||
|
||||
if use_templates:
|
||||
for move in self.filtered(
|
||||
lambda m: m.move_type in ('out_invoice', 'in_invoice')
|
||||
):
|
||||
_logger.info("TEMPLATE DEBUG: Processing move %s, template: %s", move.id, move.journal_template_id)
|
||||
if not move.journal_template_id:
|
||||
raise UserError(
|
||||
'Выберите шаблон типовой операции перед проведением.'
|
||||
)
|
||||
try:
|
||||
_logger.info("TEMPLATE DEBUG: Calling _replace_lines_from_template")
|
||||
self._replace_lines_from_template(move)
|
||||
_logger.info("TEMPLATE DEBUG: _replace_lines_from_template completed")
|
||||
except Exception as e:
|
||||
_logger.error("TEMPLATE DEBUG: Error in _replace_lines_from_template: %s", e, exc_info=True)
|
||||
raise
|
||||
|
||||
return super().action_post()
|
||||
@ -0,0 +1,7 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMoveLine(models.Model):
|
||||
_inherit = 'account.move.line'
|
||||
|
||||
# No overrides needed - we handle everything in action_post()
|
||||
10
account_move_templates_invoice/models/res_config_settings.py
Normal file
10
account_move_templates_invoice/models/res_config_settings.py
Normal file
@ -0,0 +1,10 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
use_journal_templates_for_invoices = fields.Boolean(
|
||||
string='Использовать шаблоны типовых операций для счетов',
|
||||
config_parameter='account_move_templates_invoice.use_journal_templates_for_invoices',
|
||||
)
|
||||
31
account_move_templates_invoice/views/account_move_views.xml
Normal file
31
account_move_templates_invoice/views/account_move_views.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_account_move_template_invoice_form" model="ir.ui.view">
|
||||
<field name="name">account.move.template.invoice.form</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Smart button: visible when move_count > 0 -->
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button type="object"
|
||||
name="action_view_moves"
|
||||
class="oe_stat_button"
|
||||
icon="fa-book"
|
||||
invisible="move_count == 0">
|
||||
<field name="move_count" widget="statinfo" string="Проводки"/>
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
<!-- Template selector after invoice_date -->
|
||||
<xpath expr="//field[@name='invoice_date']" position="after">
|
||||
<field name="journal_template_id"
|
||||
string="Шаблон типовой операции"
|
||||
options="{'no_create': True}"/>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="res_config_settings_view_form_template_invoice" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.template.invoice</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//app[@name='account']" position="inside">
|
||||
<block title="Шаблоны типовых операций" name="journal_templates_setting_container">
|
||||
<setting help="Использовать шаблоны типовых операций вместо стандартных проводок для счетов">
|
||||
<field name="use_journal_templates_for_invoices"/>
|
||||
<label for="use_journal_templates_for_invoices"/>
|
||||
</setting>
|
||||
</block>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user