Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
4
rsbu_mapping/models/__init__.py
Normal file
4
rsbu_mapping/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import rsbu_operation_template
|
||||
from . import rsbu_operation_matrix
|
||||
from . import account_move
|
||||
from . import account_payment
|
||||
68
rsbu_mapping/models/account_move.py
Normal file
68
rsbu_mapping/models/account_move.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from odoo import api, fields, models
|
||||
from ..resolvers.registry import get_resolver
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
rsbu_template_id = fields.Many2one(
|
||||
'rsbu.operation.template', string='Шаблон РСБУ',
|
||||
compute='_compute_rsbu_template_id', store=True, readonly=False,
|
||||
copy=False)
|
||||
|
||||
@api.depends('move_type', 'state', 'line_ids')
|
||||
def _compute_rsbu_template_id(self):
|
||||
for move in self:
|
||||
if move.rsbu_template_id:
|
||||
# ручное переопределение сохраняем
|
||||
continue
|
||||
move.rsbu_template_id = move._rsbu_detect_template()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Классификация
|
||||
# ------------------------------------------------------------------
|
||||
def _rsbu_doc_kind(self):
|
||||
self.ensure_one()
|
||||
return self.move_type
|
||||
|
||||
def _rsbu_has_advance(self):
|
||||
self.ensure_one()
|
||||
return False
|
||||
|
||||
def _rsbu_partner_kind(self):
|
||||
self.ensure_one()
|
||||
if self.move_type in ('out_invoice', 'out_refund'):
|
||||
return 'customer'
|
||||
if self.move_type in ('in_invoice', 'in_refund'):
|
||||
return 'supplier'
|
||||
return 'any'
|
||||
|
||||
def _rsbu_detect_template(self):
|
||||
self.ensure_one()
|
||||
# sudo: детект — внутренняя механика; пользователь, проводящий
|
||||
# документ, может не иметь прав на РСБУ-модели.
|
||||
Matrix = self.env['rsbu.operation.matrix'].sudo()
|
||||
rules = Matrix.search([
|
||||
('company_id', 'in', (False, self.company_id.id)),
|
||||
], order='priority, id')
|
||||
for rule in rules:
|
||||
if rule._match(self):
|
||||
return rule.template_id
|
||||
return self.env['rsbu.operation.template'].sudo()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Реализация точек миксина (Этап 3)
|
||||
# ------------------------------------------------------------------
|
||||
def _rsbu_is_applicable(self):
|
||||
self.ensure_one()
|
||||
return bool(self.rsbu_template_id or self._rsbu_detect_template())
|
||||
|
||||
def _rsbu_build_lines(self):
|
||||
self.ensure_one()
|
||||
template = (self.rsbu_template_id or self._rsbu_detect_template()).sudo()
|
||||
if not template:
|
||||
return []
|
||||
fn = get_resolver(template.resolver_key)
|
||||
if not fn:
|
||||
return []
|
||||
return fn(self, template)
|
||||
74
rsbu_mapping/models/account_payment.py
Normal file
74
rsbu_mapping/models/account_payment.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from odoo import api, fields, models
|
||||
from ..resolvers.registry import get_resolver
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = 'account.payment'
|
||||
|
||||
rsbu_vat_amount = fields.Monetary(
|
||||
string='В том числе НДС', currency_field='currency_id',
|
||||
help='Сумма НДС в составе платежа (для авансового НДС). '
|
||||
'Заполняется вручную или при загрузке банковской выписки.')
|
||||
rsbu_template_id = fields.Many2one(
|
||||
'rsbu.operation.template', string='Шаблон РСБУ',
|
||||
compute='_compute_rsbu_template_id', store=True, readonly=False,
|
||||
copy=False)
|
||||
|
||||
@api.depends('payment_type', 'state')
|
||||
def _compute_rsbu_template_id(self):
|
||||
for payment in self:
|
||||
if payment.rsbu_template_id:
|
||||
continue
|
||||
payment.rsbu_template_id = payment._rsbu_detect_template()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Классификация
|
||||
# ------------------------------------------------------------------
|
||||
def _rsbu_doc_kind(self):
|
||||
self.ensure_one()
|
||||
return 'payment_inbound' if self.payment_type == 'inbound' \
|
||||
else 'payment_outbound'
|
||||
|
||||
def _rsbu_has_advance(self):
|
||||
self.ensure_one()
|
||||
# Признак используется матрицей лишь при has_advance != 'any'.
|
||||
# Для клиентских платежей тип (аванс/постоплата/переплата) определяет
|
||||
# резолвер customer_payment по сальдо дебиторки на дату платежа, а
|
||||
# матрица ведёт на один шаблон (has_advance='any'), поэтому этот
|
||||
# признак на выбор шаблона клиентского платежа не влияет.
|
||||
return self.payment_type == 'inbound' \
|
||||
and self.partner_type == 'customer'
|
||||
|
||||
def _rsbu_partner_kind(self):
|
||||
self.ensure_one()
|
||||
return self.partner_type or 'any'
|
||||
|
||||
def _rsbu_detect_template(self):
|
||||
self.ensure_one()
|
||||
# sudo: детект — внутренняя механика; пользователь, проводящий
|
||||
# документ, может не иметь прав на РСБУ-модели.
|
||||
Matrix = self.env['rsbu.operation.matrix'].sudo()
|
||||
rules = Matrix.search([
|
||||
('company_id', 'in', (False, self.company_id.id)),
|
||||
], order='priority, id')
|
||||
for rule in rules:
|
||||
if rule._match(self):
|
||||
return rule.template_id
|
||||
return self.env['rsbu.operation.template'].sudo()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Реализация точек миксина (Этап 3)
|
||||
# ------------------------------------------------------------------
|
||||
def _rsbu_is_applicable(self):
|
||||
self.ensure_one()
|
||||
return bool(self.rsbu_template_id or self._rsbu_detect_template())
|
||||
|
||||
def _rsbu_build_lines(self):
|
||||
self.ensure_one()
|
||||
template = (self.rsbu_template_id or self._rsbu_detect_template()).sudo()
|
||||
if not template:
|
||||
return []
|
||||
fn = get_resolver(template.resolver_key)
|
||||
if not fn:
|
||||
return []
|
||||
return fn(self, template)
|
||||
58
rsbu_mapping/models/rsbu_operation_matrix.py
Normal file
58
rsbu_mapping/models/rsbu_operation_matrix.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class RsbuOperationMatrix(models.Model):
|
||||
"""Матрица связности: подбор шаблона по признакам документа."""
|
||||
_name = 'rsbu.operation.matrix'
|
||||
_description = 'РСБУ — Матрица связности операций'
|
||||
_order = 'priority, id'
|
||||
_check_company_auto = True
|
||||
|
||||
name = fields.Char(string='Наименование правила', required=True)
|
||||
doc_kind = fields.Selection(
|
||||
selection=[
|
||||
# account.move
|
||||
('out_invoice', 'Счёт покупателю (реализация)'),
|
||||
('out_refund', 'Возврат покупателю (кредит-нота)'),
|
||||
('in_invoice', 'Счёт поставщика'),
|
||||
('in_refund', 'Возврат поставщику'),
|
||||
# account.payment
|
||||
('payment_inbound', 'Входящий платёж'),
|
||||
('payment_outbound', 'Исходящий платёж'),
|
||||
],
|
||||
string='Тип документа', required=True)
|
||||
has_advance = fields.Selection(
|
||||
selection=[('any', 'Не важно'), ('yes', 'Есть аванс'),
|
||||
('no', 'Без аванса')],
|
||||
string='Признак аванса', default='any', required=True)
|
||||
partner_kind = fields.Selection(
|
||||
selection=[('any', 'Не важно'), ('customer', 'Клиент'),
|
||||
('supplier', 'Поставщик')],
|
||||
string='Тип контрагента', default='any', required=True,
|
||||
help='Для платежей: различает клиента и поставщика при '
|
||||
'одинаковом направлении платежа.')
|
||||
template_id = fields.Many2one(
|
||||
'rsbu.operation.template', string='Шаблон РСБУ', required=True,
|
||||
check_company=True)
|
||||
priority = fields.Integer(string='Приоритет', default=10)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Компания',
|
||||
default=lambda self: self.env.company)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
def _match(self, doc):
|
||||
"""Подходит ли правило документу."""
|
||||
self.ensure_one()
|
||||
doc_kind = doc._rsbu_doc_kind()
|
||||
if self.doc_kind != doc_kind:
|
||||
return False
|
||||
if self.has_advance != 'any':
|
||||
has_adv = doc._rsbu_has_advance()
|
||||
if self.has_advance == 'yes' and not has_adv:
|
||||
return False
|
||||
if self.has_advance == 'no' and has_adv:
|
||||
return False
|
||||
if self.partner_kind != 'any':
|
||||
if self.partner_kind != doc._rsbu_partner_kind():
|
||||
return False
|
||||
return True
|
||||
82
rsbu_mapping/models/rsbu_operation_template.py
Normal file
82
rsbu_mapping/models/rsbu_operation_template.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from odoo import fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from ..resolvers.registry import resolver_selection
|
||||
|
||||
# Роли счетов, используемые резолверами.
|
||||
ROLE_SELECTION = [
|
||||
('bank', 'Денежный счёт (51/50)'),
|
||||
('advance_received', 'Авансы полученные (62.АВ)'),
|
||||
('receivable', 'Расчёты с покупателями (62.01)'),
|
||||
('revenue', 'Выручка (90.01)'),
|
||||
('revenue_vat', 'НДС в составе продаж (90.03)'),
|
||||
('vat_output', 'НДС начисленный (68.НДС)'),
|
||||
('vat_advance', 'НДС с авансов (76.АВ)'),
|
||||
('payable', 'Расчёты с поставщиками (60.01)'),
|
||||
('advance_paid', 'Авансы выданные (60.02)'),
|
||||
('expense', 'Затраты / расходы (44/26/20)'),
|
||||
('vat_input', 'Входящий НДС (19)'),
|
||||
]
|
||||
|
||||
|
||||
class RsbuOperationTemplate(models.Model):
|
||||
"""Шаблон типовой операции РСБУ (декларативная часть маппинга)."""
|
||||
_name = 'rsbu.operation.template'
|
||||
_description = 'РСБУ — Шаблон типовой операции'
|
||||
_check_company_auto = True
|
||||
|
||||
name = fields.Char(string='Наименование', required=True)
|
||||
code = fields.Char(string='Код', required=True)
|
||||
resolver_key = fields.Selection(
|
||||
selection=lambda self: resolver_selection(),
|
||||
string='Резолвер', required=True,
|
||||
help='Python-резолвер (в коде модуля), формирующий проводки.')
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Компания',
|
||||
default=lambda self: self.env.company)
|
||||
role_ids = fields.One2many(
|
||||
'rsbu.operation.template.role', 'template_id', string='Роли счетов')
|
||||
active = fields.Boolean(default=True)
|
||||
note = fields.Text(string='Описание логики')
|
||||
|
||||
_code_uniq = models.Constraint(
|
||||
'unique(code, company_id)',
|
||||
'Код шаблона должен быть уникальным в пределах компании.')
|
||||
|
||||
def _account(self, role):
|
||||
"""Счёт РСБУ по роли шаблона (обязательная роль)."""
|
||||
self.ensure_one()
|
||||
line = self.role_ids.filtered(lambda r: r.role == role)[:1]
|
||||
if not line:
|
||||
raise UserError(_(
|
||||
'В шаблоне «%(tmpl)s» не задана роль счёта «%(role)s».',
|
||||
tmpl=self.display_name, role=role))
|
||||
return line.account_id
|
||||
|
||||
def _account_or_none(self, role):
|
||||
"""Счёт РСБУ по роли шаблона или None, если роль не задана."""
|
||||
self.ensure_one()
|
||||
line = self.role_ids.filtered(lambda r: r.role == role)[:1]
|
||||
return line.account_id if line else None
|
||||
|
||||
|
||||
class RsbuOperationTemplateRole(models.Model):
|
||||
"""Роль счёта в шаблоне операции."""
|
||||
_name = 'rsbu.operation.template.role'
|
||||
_description = 'РСБУ — Роль счёта в шаблоне'
|
||||
_check_company_auto = True
|
||||
|
||||
template_id = fields.Many2one(
|
||||
'rsbu.operation.template', string='Шаблон', required=True,
|
||||
ondelete='cascade')
|
||||
role = fields.Selection(
|
||||
selection=ROLE_SELECTION, string='Роль', required=True)
|
||||
account_id = fields.Many2one(
|
||||
'rsbu.account', string='Счёт РСБУ', required=True, check_company=True)
|
||||
company_id = fields.Many2one(
|
||||
related='template_id.company_id', store=True)
|
||||
subconto_source = fields.Selection(
|
||||
selection=[
|
||||
('partner_from_doc', 'Контрагент из документа'),
|
||||
('none', 'Без субконто по умолчанию'),
|
||||
],
|
||||
string='Источник субконто', default='partner_from_doc')
|
||||
Reference in New Issue
Block a user