131 lines
6.4 KiB
Python
131 lines
6.4 KiB
Python
from odoo import api, fields, models, _
|
||
from odoo.exceptions import UserError
|
||
|
||
|
||
class RsbuPostingMixin(models.AbstractModel):
|
||
"""Абстрактный миксин связи документа-источника с журналом РСБУ.
|
||
|
||
Инкапсулирует механику: идемпотентную генерацию РСБУ-операции,
|
||
зеркалирование отмены, транзакционную блокировку и навигацию.
|
||
Расчёт строк Дт/Кт делегируется методу _rsbu_build_lines (Этап 4);
|
||
на Этапе 3 это заглушка, возвращающая пустой список.
|
||
"""
|
||
_name = 'rsbu.posting.mixin'
|
||
_description = 'РСБУ — Миксин проведения'
|
||
|
||
rsbu_move_id = fields.Many2one(
|
||
'rsbu.move', string='РСБУ-проводка', copy=False, readonly=True)
|
||
rsbu_move_count = fields.Integer(
|
||
string='РСБУ-проводки', compute='_compute_rsbu_move_count')
|
||
|
||
@api.depends('rsbu_move_id')
|
||
def _compute_rsbu_move_count(self):
|
||
for rec in self:
|
||
rec.rsbu_move_count = 1 if rec.rsbu_move_id else 0
|
||
|
||
# ------------------------------------------------------------------
|
||
# Точки расширения (реализуются Этапом 4)
|
||
# ------------------------------------------------------------------
|
||
def _rsbu_is_applicable(self):
|
||
"""Входит ли документ в РСБУ-периметр (нужна ли РСБУ-проводка).
|
||
|
||
На Этапе 3 маппинга ещё нет, поэтому по умолчанию False —
|
||
штатное проведение документов Odoo не затрагивается. На Этапе 4
|
||
применимость определяется матрицей связности (наличием шаблона).
|
||
"""
|
||
self.ensure_one()
|
||
return False
|
||
|
||
def _rsbu_get_move_values(self):
|
||
"""Значения шапки rsbu.move для документа-источника."""
|
||
self.ensure_one()
|
||
return {
|
||
'date': self._rsbu_move_date(),
|
||
'company_id': self.company_id.id,
|
||
'ref': self.display_name,
|
||
'source_model': self._name,
|
||
'source_res_id': self.id,
|
||
}
|
||
|
||
def _rsbu_move_date(self):
|
||
"""Дата РСБУ-операции. По умолчанию — поле date документа/сегодня."""
|
||
self.ensure_one()
|
||
return getattr(self, 'date', False) or fields.Date.context_today(self)
|
||
|
||
def _rsbu_build_lines(self):
|
||
"""Список значений строк rsbu.move.line (Дт/Кт + субконто).
|
||
|
||
ЗАГЛУШКА Этапа 3: возвращает пустой список -> трактуется как
|
||
«нет маппинга». Реализуется маппингом на Этапе 4.
|
||
"""
|
||
self.ensure_one()
|
||
return []
|
||
|
||
# ------------------------------------------------------------------
|
||
# Механика
|
||
# ------------------------------------------------------------------
|
||
def _rsbu_generate(self):
|
||
"""Идемпотентно создать и провести РСБУ-операцию по документу.
|
||
|
||
Управляемый обход: при context ``rsbu_skip=True`` генерация
|
||
пропускается (по аналогии с with_user/with_context Odoo) —
|
||
для служебных/внешних вызовов, где РСБУ-слой не нужен.
|
||
"""
|
||
self.ensure_one()
|
||
if self.env.context.get('rsbu_skip'):
|
||
return
|
||
if not self._rsbu_is_applicable() or self.rsbu_move_id:
|
||
return
|
||
lines = self._rsbu_build_lines()
|
||
if not lines:
|
||
raise UserError(_(
|
||
'Не удалось сформировать РСБУ-проводку для «%s»: '
|
||
'не определён маппинг типовой операции.')
|
||
% self.display_name)
|
||
vals = self._rsbu_get_move_values()
|
||
# Служебный маркер резолвера: строки с origin_self=True ссылаются на
|
||
# саму создаваемую проводку как на документ-основание (аванс).
|
||
# Убираем маркер из vals строк (ORM его не знает) и запоминаем позиции.
|
||
origin_self_idx = []
|
||
clean_lines = []
|
||
for i, line in enumerate(lines):
|
||
line = dict(line)
|
||
if line.pop('origin_self', False):
|
||
origin_self_idx.append(i)
|
||
clean_lines.append(line)
|
||
vals['line_ids'] = [(0, 0, line) for line in clean_lines]
|
||
# sudo: РСБУ-проводка — внутренняя механика, создаётся при проведении
|
||
# документа независимо от прав пользователя на РСБУ-модели.
|
||
move = self.env['rsbu.move'].sudo().create(vals)
|
||
# Проставить origin_move_id = сама проводка для помеченных строк.
|
||
if origin_self_idx:
|
||
move_lines = move.line_ids
|
||
for i in origin_self_idx:
|
||
if i < len(move_lines):
|
||
move_lines[i].origin_move_id = move.id
|
||
move.action_post() # проверки баланса/субконто Этапа 2
|
||
self.sudo().rsbu_move_id = move.id
|
||
|
||
def _rsbu_remove(self):
|
||
"""Зеркалирование отмены: удалить связанную РСБУ-операцию."""
|
||
for rec in self:
|
||
move = rec.rsbu_move_id.sudo()
|
||
if move:
|
||
rec.sudo().rsbu_move_id = False
|
||
if move.state == 'posted':
|
||
move.action_draft()
|
||
move.unlink()
|
||
|
||
# ------------------------------------------------------------------
|
||
# Навигация
|
||
# ------------------------------------------------------------------
|
||
def action_view_rsbu_move(self):
|
||
self.ensure_one()
|
||
return {
|
||
'type': 'ir.actions.act_window',
|
||
'name': _('РСБУ-проводка'),
|
||
'res_model': 'rsbu.move',
|
||
'view_mode': 'form',
|
||
'res_id': self.rsbu_move_id.id,
|
||
}
|