157 lines
7.8 KiB
Python
157 lines
7.8 KiB
Python
from odoo import api, fields, models
|
||
|
||
# Код счёта начисленного НДС по умолчанию (кодировка 94н). Фактический счёт
|
||
# определяется по роли `vat_output` шаблонов маппинга (может быть 68.02 в
|
||
# кодировке 1С), а эта константа — фолбэк, если роль нигде не задана.
|
||
VAT_OUTPUT_CODE = '68.НДС'
|
||
# Код счёта единого налогового счёта (ЕНС). Перенос сальдо НДС на ЕНС
|
||
# (Дт 68.02 − Кт 68.90) — регламентная операция уплаты налога, а не
|
||
# начисление/вычет НДС, поэтому такие проводки в книги НДС не попадают.
|
||
ENS_ACCOUNT_CODE = '68.90'
|
||
|
||
|
||
class RsbuVatBookLine(models.Model):
|
||
"""Запись книги продаж / покупок (собирается из проводок НДС журнала)."""
|
||
_name = 'rsbu.vat.book.line'
|
||
_description = 'РСБУ — Запись книги НДС'
|
||
_order = 'date, id'
|
||
_check_company_auto = True
|
||
|
||
book = fields.Selection(
|
||
selection=[('sales', 'Книга продаж'),
|
||
('purchases', 'Книга покупок')],
|
||
string='Книга', required=True, index=True)
|
||
date = fields.Date(string='Дата', required=True, index=True)
|
||
company_id = fields.Many2one(
|
||
'res.company', string='Компания', required=True, index=True,
|
||
default=lambda self: self.env.company)
|
||
move_id = fields.Many2one('rsbu.move', string='Операция',
|
||
ondelete='cascade')
|
||
move_line_id = fields.Many2one('rsbu.move.line', string='Строка журнала',
|
||
ondelete='cascade')
|
||
invoice_id = fields.Many2one('rsbu.invoice', string='Счёт-фактура')
|
||
partner_id = fields.Many2one('res.partner', string='Контрагент')
|
||
base_amount = fields.Monetary(string='База', currency_field='currency_id')
|
||
vat_amount = fields.Monetary(string='Сумма НДС',
|
||
currency_field='currency_id')
|
||
currency_id = fields.Many2one('res.currency', string='Валюта',
|
||
compute='_compute_currency_id', store=True)
|
||
period_key = fields.Char(string='Квартал', compute='_compute_period_key',
|
||
store=True, index=True)
|
||
|
||
@api.depends('company_id')
|
||
def _compute_currency_id(self):
|
||
for line in self:
|
||
line.currency_id = line.company_id.currency_id
|
||
|
||
@api.depends('date')
|
||
def _compute_period_key(self):
|
||
for line in self:
|
||
if line.date:
|
||
quarter = (line.date.month - 1) // 3 + 1
|
||
line.period_key = '%s-Q%s' % (line.date.year, quarter)
|
||
else:
|
||
line.period_key = False
|
||
|
||
@api.model
|
||
def _vat_output_accounts(self, company):
|
||
"""Счета начисленного НДС для компании (по роли vat_output).
|
||
|
||
Определяем счёт(а) НДС по роли `vat_output` в шаблонах маппинга —
|
||
это работает независимо от кодировки (68.НДС в академическом плане
|
||
либо 68.02 в кодировке 1С). Фолбэк — счёт с кодом VAT_OUTPUT_CODE.
|
||
"""
|
||
Role = self.env['rsbu.operation.template.role'].sudo()
|
||
roles = Role.search([
|
||
('role', '=', 'vat_output'),
|
||
('company_id', 'in', (False, company.id)),
|
||
])
|
||
accounts = roles.mapped('account_id')
|
||
if not accounts:
|
||
accounts = self.env['rsbu.account'].sudo().search([
|
||
('code', '=', VAT_OUTPUT_CODE),
|
||
('company_id', '=', company.id)], limit=1)
|
||
return accounts
|
||
|
||
@api.model
|
||
def _ens_accounts(self, company):
|
||
"""Счёт(а) единого налогового счёта (ЕНС) для компании (код 68.90)."""
|
||
return self.env['rsbu.account'].sudo().search([
|
||
('code', '=', ENS_ACCOUNT_CODE),
|
||
('company_id', '=', company.id)])
|
||
|
||
@api.model
|
||
def _is_ens_transfer(self, move, vat_accounts, ens_accounts):
|
||
"""Проводка — перенос НДС на ЕНС (не операция начисления/вычета).
|
||
|
||
Признак: в проводке одновременно есть строка счёта НДС (vat_output)
|
||
и строка счёта ЕНС (68.90). Такой перенос сальдо расчётов по НДС на
|
||
единый налоговый счёт не является ни начислением (книга продаж), ни
|
||
вычетом (книга покупок) — в книги НДС не попадает.
|
||
"""
|
||
if not ens_accounts:
|
||
return False
|
||
accounts = move.line_ids.mapped('account_id')
|
||
return bool(accounts & vat_accounts) and bool(accounts & ens_accounts)
|
||
|
||
@api.model
|
||
def _is_vat_account(self, account, vat_accounts=None):
|
||
if vat_accounts is not None:
|
||
return account in vat_accounts
|
||
# Обратная совместимость (одиночный вызов): сверка по коду.
|
||
return account.code == VAT_OUTPUT_CODE
|
||
|
||
@api.model
|
||
def recompute_period(self, date_from, date_to, company):
|
||
"""Пересобрать книги продаж/покупок за период из проводок журнала.
|
||
|
||
Книга продаж — начисление НДС (Кт счёта vat_output);
|
||
книга покупок — вычет НДС (Дт счёта vat_output).
|
||
Счёт НДС определяется по роли (68.НДС или 68.02). Идемпотентно.
|
||
|
||
TODO(perf): при больших объёмах вынести в cron. Сейчас не делаем.
|
||
"""
|
||
self.search([
|
||
('company_id', '=', company.id),
|
||
('date', '>=', date_from),
|
||
('date', '<=', date_to),
|
||
]).unlink()
|
||
|
||
vat_accounts = self._vat_output_accounts(company)
|
||
ens_accounts = self._ens_accounts(company)
|
||
lines = self.env['rsbu.move.line'].search([
|
||
('company_id', '=', company.id),
|
||
('state', '=', 'posted'),
|
||
('date', '>=', date_from),
|
||
('date', '<=', date_to),
|
||
])
|
||
vals_list = []
|
||
for line in lines:
|
||
if not self._is_vat_account(line.account_id, vat_accounts):
|
||
continue
|
||
# Перенос сальдо НДС на ЕНС (Дт 68.02 − Кт 68.90) — регламентная
|
||
# уплата налога, не начисление/вычет НДС. В книги не включаем.
|
||
if self._is_ens_transfer(line.move_id, vat_accounts, ens_accounts):
|
||
continue
|
||
if line.credit: # начисление -> книга продаж
|
||
vals_list.append(self._line_vals(line, 'sales',
|
||
line.credit))
|
||
elif line.debit: # вычет -> книга покупок
|
||
vals_list.append(self._line_vals(line, 'purchases',
|
||
line.debit))
|
||
if vals_list:
|
||
return self.create(vals_list)
|
||
return self.browse()
|
||
|
||
@api.model
|
||
def _line_vals(self, line, book, vat_amount):
|
||
return {
|
||
'book': book,
|
||
'date': line.date,
|
||
'company_id': line.company_id.id,
|
||
'move_id': line.move_id.id,
|
||
'move_line_id': line.id,
|
||
'partner_id': line.partner_id.id or False,
|
||
'vat_amount': vat_amount,
|
||
}
|