Files
public/rsbu_journal/models/rsbu_move.py

149 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_is_zero
class RsbuMove(models.Model):
"""Хозяйственная операция РСБУ (аналог account.move)."""
_name = 'rsbu.move'
_description = 'РСБУ — Хозяйственная операция'
_order = 'date desc, name desc, id desc'
_check_company_auto = True
_inherit = ['mail.thread']
name = fields.Char(
string='Номер', default='/', copy=False, readonly=True, index=True)
date = fields.Date(
string='Дата операции', required=True, index=True,
default=fields.Date.context_today, tracking=True)
company_id = fields.Many2one(
'res.company', string='Компания', required=True, index=True,
default=lambda self: self.env.company)
currency_id = fields.Many2one(
'res.currency', string='Валюта',
compute='_compute_currency_id', store=True)
state = fields.Selection(
selection=[('draft', 'Черновик'), ('posted', 'Проведён')],
string='Состояние', default='draft', required=True,
copy=False, tracking=True, index=True)
line_ids = fields.One2many(
'rsbu.move.line', 'move_id', string='Строки проводки',
copy=True)
ref = fields.Char(string='Ссылка / описание')
note = fields.Text(string='Примечание')
# Ссылка на документ-источник (используется Этапом 3).
source_model = fields.Char(string='Модель источника', copy=False)
source_res_id = fields.Integer(string='ID источника', copy=False)
# Контрольные обороты по проводке (для проверки баланса, не «сумма
# операции»): при наличии внутренних проводок (напр. НДС на 90.03)
# оборот по дебету не равен сумме документа, поэтому «сумму операции»
# намеренно не храним и не показываем.
total_debit = fields.Monetary(
string='Оборот Дт', compute='_compute_totals', store=True,
currency_field='currency_id')
total_credit = fields.Monetary(
string='Оборот Кт', compute='_compute_totals', store=True,
currency_field='currency_id')
line_count = fields.Integer(
string='Кол-во строк', compute='_compute_line_count')
# ------------------------------------------------------------------
# Вычисляемые поля
# ------------------------------------------------------------------
@api.depends('company_id')
def _compute_currency_id(self):
for move in self:
move.currency_id = move.company_id.currency_id
@api.depends('line_ids.debit', 'line_ids.credit')
def _compute_totals(self):
for move in self:
move.total_debit = sum(move.line_ids.mapped('debit'))
move.total_credit = sum(move.line_ids.mapped('credit'))
@api.depends('line_ids')
def _compute_line_count(self):
for move in self:
move.line_count = len(move.line_ids)
# ------------------------------------------------------------------
# Проверки при проведении
# ------------------------------------------------------------------
def _check_has_lines(self):
for move in self:
if not move.line_ids:
raise UserError(_(
'Нельзя провести операцию «%s» без строк.')
% move.display_name)
def _check_balanced(self):
for move in self:
rounding = move.currency_id.rounding or 0.01
total_debit = sum(move.line_ids.mapped('debit'))
total_credit = sum(move.line_ids.mapped('credit'))
if not float_is_zero(total_debit - total_credit,
precision_rounding=rounding):
raise UserError(_(
'Операция «%(name)s» не сбалансирована: '
'дебет %(debit)s ≠ кредит %(credit)s.',
name=move.display_name,
debit=total_debit, credit=total_credit))
def _check_required_subconto(self):
for move in self:
for line in move.line_ids:
required = line.account_id.get_required_subconto()
missing = [dim for dim in required
if not line._has_subconto(dim)]
if missing:
labels = {'partner': 'Контрагент', 'product': 'Номенклатура',
'user': 'Пользователь', 'employee': 'Сотрудник'}
names = ', '.join(labels.get(m, m) for m in missing)
raise UserError(_(
'Строка со счётом «%(acc)s» требует обязательные '
'субконто: %(subs)s.',
acc=line.account_id.display_name, subs=names))
# ------------------------------------------------------------------
# Жизненный цикл
# ------------------------------------------------------------------
def action_post(self):
for move in self:
if move.state == 'posted':
continue
move._check_has_lines()
move._check_balanced()
move._check_required_subconto()
if move.name == '/' or not move.name:
move.name = self.env['ir.sequence'].with_company(
move.company_id).next_by_code('rsbu.move') or '/'
move.state = 'posted'
return True
def action_draft(self):
for move in self:
move.state = 'draft'
return True
def write(self, vals):
# Ограничение редактирования ключевых полей у проведённой операции.
protected = {'line_ids', 'date', 'company_id'}
if any(f in vals for f in protected):
for move in self:
if move.state == 'posted':
raise UserError(_(
'Операция «%s» проведена. Верните её в черновик '
'перед изменением строк или даты.')
% move.display_name)
return super().write(vals)
@api.ondelete(at_uninstall=False)
def _unlink_only_draft(self):
for move in self:
if move.state == 'posted':
raise UserError(_(
'Нельзя удалить проведённую операцию «%s». '
'Сначала верните её в черновик.') % move.display_name)