Public release from ruodoo-project: 19.0 - 2026-05-31 21:19:12 UTC
This commit is contained in:
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'}
|
||||
Reference in New Issue
Block a user