Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
645
rsbu_vat/models/rsbu_period_close.py
Normal file
645
rsbu_vat/models/rsbu_period_close.py
Normal file
@@ -0,0 +1,645 @@
|
||||
"""Регламентные операции закрытия периода (РСБУ, УСН+НДС).
|
||||
|
||||
Документ конца квартала, порождающий регламентные РСБУ-проводки, которых
|
||||
нет среди первичных документов:
|
||||
|
||||
* вычет авансового НДС по зачтённым облагаемым авансам
|
||||
(Дт vat_output(68.02) — Кт vat_advance(76.АВ)) — «формирование записей
|
||||
книги покупок»;
|
||||
* перенос налогов на единый налоговый счёт (ЕНС):
|
||||
Дт 68.02 — Кт 68.90 (НДС), Дт 68.12 — Кт 68.90 (УСН);
|
||||
* начисление налога УСН: Дт 99.01.1 — Кт 68.12.
|
||||
|
||||
Каждая операция создаёт отдельную rsbu.move (сводную регламентную) и
|
||||
хранит ссылку для отмены/переигрывания. Суммы берутся из сальдо/оборотов
|
||||
счётов РСБУ-журнала (get_balance / get_origin_balances), без внешних данных.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class RsbuPeriodClose(models.Model):
|
||||
_name = 'rsbu.period.close'
|
||||
_description = 'РСБУ — Закрытие периода (регламент)'
|
||||
_order = 'date_to desc, id desc'
|
||||
_check_company_auto = True
|
||||
|
||||
name = fields.Char(string='Наименование', compute='_compute_name',
|
||||
store=True)
|
||||
# Закрытие помесячное (как в 1С). Период задаётся месяцем; границы
|
||||
# date_from/date_to вычисляются как первый/последний день месяца.
|
||||
period_month = fields.Date(
|
||||
string='Месяц закрытия', required=True,
|
||||
default=lambda self: fields.Date.today().replace(day=1),
|
||||
help='Любая дата месяца; закрытие выполняется за календарный месяц.')
|
||||
date_from = fields.Date(string='Период с', compute='_compute_period',
|
||||
store=True)
|
||||
date_to = fields.Date(string='Период по', compute='_compute_period',
|
||||
store=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', string='Компания', required=True, index=True,
|
||||
default=lambda self: self.env.company)
|
||||
move_ids = fields.Many2many(
|
||||
'rsbu.move', string='Регламентные проводки', copy=False)
|
||||
move_count = fields.Integer(compute='_compute_move_count')
|
||||
state = fields.Selection(
|
||||
selection=[('draft', 'Черновик'), ('done', 'Проведён')],
|
||||
default='draft', string='Состояние', copy=False)
|
||||
|
||||
@api.constrains('period_month', 'company_id')
|
||||
def _check_unique_month(self):
|
||||
for rec in self:
|
||||
if not rec.period_month:
|
||||
continue
|
||||
dup = self.search([
|
||||
('id', '!=', rec.id),
|
||||
('company_id', '=', rec.company_id.id),
|
||||
('period_month', '>=', rec.date_from),
|
||||
('period_month', '<=', rec.date_to),
|
||||
], limit=1)
|
||||
if dup:
|
||||
raise ValidationError(_(
|
||||
'Закрытие за этот месяц уже существует: %s.') % dup.name)
|
||||
|
||||
_MONTH_NAMES = [
|
||||
'', 'январь', 'февраль', 'март', 'апрель', 'май', 'июнь',
|
||||
'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь']
|
||||
|
||||
@api.depends('period_month', 'company_id')
|
||||
def _compute_period(self):
|
||||
"""Границы календарного месяца по period_month."""
|
||||
import calendar
|
||||
for rec in self:
|
||||
if not rec.period_month:
|
||||
rec.date_from = rec.date_to = False
|
||||
continue
|
||||
first = rec.period_month.replace(day=1)
|
||||
last_day = calendar.monthrange(first.year, first.month)[1]
|
||||
rec.date_from = first
|
||||
rec.date_to = first.replace(day=last_day)
|
||||
|
||||
@api.depends('period_month')
|
||||
def _compute_name(self):
|
||||
for rec in self:
|
||||
if rec.period_month:
|
||||
rec.name = _('Закрытие: %(m)s %(y)s',
|
||||
m=self._MONTH_NAMES[rec.period_month.month],
|
||||
y=rec.period_month.year)
|
||||
else:
|
||||
rec.name = _('Закрытие периода')
|
||||
|
||||
def _is_quarter_end(self):
|
||||
"""Месяц — последний в квартале (март/июнь/сентябрь/декабрь)."""
|
||||
self.ensure_one()
|
||||
return bool(self.period_month) and self.period_month.month in (3, 6, 9, 12)
|
||||
|
||||
def _is_year_end(self):
|
||||
"""Месяц — декабрь (для реформации баланса)."""
|
||||
self.ensure_one()
|
||||
return bool(self.period_month) and self.period_month.month == 12
|
||||
|
||||
@api.depends('move_ids')
|
||||
def _compute_move_count(self):
|
||||
for rec in self:
|
||||
rec.move_count = len(rec.move_ids)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _acc(self, code):
|
||||
return self.env['rsbu.account'].sudo().search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('code', '=', code)], limit=1)
|
||||
|
||||
def _role_account(self, role):
|
||||
rec = self.env['rsbu.operation.template.role'].sudo().search([
|
||||
('role', '=', role),
|
||||
('company_id', 'in', (False, self.company_id.id)),
|
||||
], limit=1)
|
||||
return rec.account_id
|
||||
|
||||
def _new_move(self, ref, lines):
|
||||
"""Создать и провести регламентную rsbu.move; вернуть её."""
|
||||
self.ensure_one()
|
||||
move = self.env['rsbu.move'].sudo().create({
|
||||
'date': self.date_to,
|
||||
'company_id': self.company_id.id,
|
||||
'ref': ref,
|
||||
'line_ids': [(0, 0, ln) for ln in lines],
|
||||
})
|
||||
move.action_post()
|
||||
self.move_ids = [(4, move.id)]
|
||||
return move
|
||||
|
||||
def _has_move_by_ref(self, ref):
|
||||
"""Идемпотентность: уже есть регламентная проводка с таким ref."""
|
||||
self.ensure_one()
|
||||
return any(m.ref == ref for m in self.move_ids)
|
||||
|
||||
def _month_turnover(self, account):
|
||||
"""Обороты Дт/Кт счёта (без потомков) за месяц закрытия."""
|
||||
self.ensure_one()
|
||||
return account.get_turnover(
|
||||
date_from=self.date_from, date_to=self.date_to,
|
||||
company_id=self.company_id.id, include_children=False)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Регламент: закрытие затрат 26 → 90.08 (директ-костинг)
|
||||
# ------------------------------------------------------------------
|
||||
def action_close_expenses(self):
|
||||
"""Списать общехозяйственные расходы (26) на 90.08 (директ-костинг).
|
||||
|
||||
Дебетовый оборот 26 за месяц переносится проводкой
|
||||
Дт 90.08 — Кт 26; счёт 26 на конец месяца обнуляется.
|
||||
"""
|
||||
for rec in self:
|
||||
ref = _('Закрытие счёта 26 (директ-костинг)')
|
||||
if rec._has_move_by_ref(ref):
|
||||
continue
|
||||
acc26 = rec._acc('26')
|
||||
acc9008 = rec._acc('90.08.1') or rec._acc('90.08')
|
||||
if not acc26 or not acc9008:
|
||||
continue
|
||||
turnover = rec._month_turnover(acc26)
|
||||
amount = round(turnover['debit'] - turnover['credit'], 2)
|
||||
if amount <= 0:
|
||||
continue
|
||||
rec._new_move(ref, [
|
||||
{'account_id': acc9008.id, 'debit': amount,
|
||||
'name': _('Управленческие расходы')},
|
||||
{'account_id': acc26.id, 'credit': amount,
|
||||
'name': _('Списаны общехозяйственные расходы')},
|
||||
])
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Регламент: закрытие 90 → 99 и 91 → 99 через 90.09 / 91.09
|
||||
# ------------------------------------------------------------------
|
||||
def _close_result_group(self, group_code, result_sub_code, ref):
|
||||
"""Закрыть результатный счёт (90/91) на 99 через итоговый субсчёт.
|
||||
|
||||
1. Каждый субсчёт группы (кроме итогового *.09) закрывается за месяц
|
||||
встречной проводкой на итоговый субсчёт (90.09/91.09), обнуляя
|
||||
свой месячный оборот.
|
||||
2. Итоговый субсчёт переносится на 99.01.1: прибыль
|
||||
Дт *.09 — Кт 99.01.1, убыток — обратной проводкой.
|
||||
|
||||
Универсально для обоих планов (коды субсчетов берутся из дерева,
|
||||
а не хардкодятся: 1С 90.01.1 vs 94н 90.01).
|
||||
"""
|
||||
self.ensure_one()
|
||||
group = self._acc(group_code)
|
||||
result_sub = self._acc(result_sub_code)
|
||||
profit = self._acc('99.01.1') or self._acc('99.01') or self._acc('99')
|
||||
if not group or not result_sub or not profit:
|
||||
return
|
||||
if self._has_move_by_ref(ref):
|
||||
return
|
||||
# Листовые субсчета группы, кроме итогового (*.09) и самой группы.
|
||||
Account = self.env['rsbu.account'].sudo()
|
||||
subs = Account.search([
|
||||
('id', 'child_of', group.id),
|
||||
('id', '!=', group.id),
|
||||
('id', '!=', result_sub.id),
|
||||
('company_id', '=', self.company_id.id),
|
||||
])
|
||||
# Только листья (без промежуточных групп вроде 90.01 над 90.01.1).
|
||||
leaf_subs = subs.filtered(lambda a: not a.child_ids)
|
||||
lines = []
|
||||
net = 0.0 # + прибыль (Кт группы больше), − убыток
|
||||
for sub in leaf_subs:
|
||||
t = self._month_turnover(sub)
|
||||
debit = round(t['debit'], 2)
|
||||
credit = round(t['credit'], 2)
|
||||
# Закрываем субсчёт встречной суммой на итоговый субсчёт.
|
||||
diff = round(credit - debit, 2)
|
||||
if diff > 0:
|
||||
# У субсчёта кредитовый оборот (выручка) → Дт суб — Кт *.09.
|
||||
lines.append({'account_id': sub.id, 'debit': diff,
|
||||
'name': _('Закрытие субсчёта')})
|
||||
lines.append({'account_id': result_sub.id, 'credit': diff,
|
||||
'name': _('Финрезультат')})
|
||||
net += diff
|
||||
elif diff < 0:
|
||||
# Дебетовый оборот (расходы/НДС) → Дт *.09 — Кт суб.
|
||||
amt = -diff
|
||||
lines.append({'account_id': result_sub.id, 'debit': amt,
|
||||
'name': _('Финрезультат')})
|
||||
lines.append({'account_id': sub.id, 'credit': amt,
|
||||
'name': _('Закрытие субсчёта')})
|
||||
net -= amt
|
||||
# Перенос итогового субсчёта на 99.01.1.
|
||||
net = round(net, 2)
|
||||
if net > 0: # прибыль: Дт *.09 — Кт 99.01.1
|
||||
lines.append({'account_id': result_sub.id, 'debit': net,
|
||||
'name': _('Прибыль от продаж')})
|
||||
lines.append({'account_id': profit.id, 'credit': net,
|
||||
'name': _('Прибыль отнесена на 99')})
|
||||
elif net < 0: # убыток: Дт 99.01.1 — Кт *.09
|
||||
amt = -net
|
||||
lines.append({'account_id': profit.id, 'debit': amt,
|
||||
'name': _('Убыток отнесён на 99')})
|
||||
lines.append({'account_id': result_sub.id, 'credit': amt,
|
||||
'name': _('Убыток от продаж')})
|
||||
if lines:
|
||||
self._new_move(ref, lines)
|
||||
|
||||
def action_close_pl(self):
|
||||
"""Закрыть счета продаж (90) и прочих доходов/расходов (91) на 99."""
|
||||
for rec in self:
|
||||
rec._close_result_group('90', '90.09', _('Закрытие счёта 90'))
|
||||
rec._close_result_group('91', '91.09', _('Закрытие счёта 91'))
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Регламент: реформация баланса (только декабрь)
|
||||
# ------------------------------------------------------------------
|
||||
def action_reform_balance(self):
|
||||
"""Реформация баланса (закрытие года): сальдо 99 → 84.
|
||||
|
||||
Выполняется только для декабря. Субсчета 90/91 к этому моменту
|
||||
обнулены ежемесячным закрытием (Этап 4); переносим итоговое сальдо
|
||||
99 (накопленную прибыль/убыток года) на 84.
|
||||
"""
|
||||
for rec in self:
|
||||
if not rec._is_year_end():
|
||||
continue
|
||||
ref = _('Реформация баланса (99 → 84)')
|
||||
if rec._has_move_by_ref(ref):
|
||||
continue
|
||||
acc99 = rec._acc('99')
|
||||
acc84 = rec._acc('84')
|
||||
if not acc99 or not acc84:
|
||||
continue
|
||||
bal = acc99.get_balance(
|
||||
date_to=rec.date_to, company_id=rec.company_id.id,
|
||||
include_children=True)
|
||||
amount = round(bal['balance'], 2)
|
||||
if amount <= 0:
|
||||
continue
|
||||
profit = rec._acc('99.01.1') or rec._acc('99.01') or acc99
|
||||
# Прибыль закрывается Дт 90.09/91.09 — Кт 99.01.1, поэтому
|
||||
# прибыль года — КРЕДИТОВОЕ сальдо 99, убыток — ДЕБЕТОВОЕ.
|
||||
if bal['side'] == 'debit':
|
||||
# Дебетовое сальдо 99 → непокрытый убыток: Дт 84 — Кт 99.01.1.
|
||||
rec._new_move(ref, [
|
||||
{'account_id': acc84.id, 'debit': amount,
|
||||
'name': _('Непокрытый убыток года')},
|
||||
{'account_id': profit.id, 'credit': amount,
|
||||
'name': _('Списан убыток на 84')},
|
||||
])
|
||||
else:
|
||||
# Кредитовое сальдо 99 → прибыль: Дт 99.01.1 — Кт 84.
|
||||
rec._new_move(ref, [
|
||||
{'account_id': profit.id, 'debit': amount,
|
||||
'name': _('Списана прибыль на 84')},
|
||||
{'account_id': acc84.id, 'credit': amount,
|
||||
'name': _('Нераспределённая прибыль года')},
|
||||
])
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Регламент 1: вычет авансового НДС по зачтённым облагаемым авансам
|
||||
# ------------------------------------------------------------------
|
||||
def _vat_deduction_lines(self):
|
||||
"""Строки вычета Дт 68.02 — Кт 76.АВ по каждому авансу-основанию.
|
||||
|
||||
По каждому origin с начисленным авансовым НДС (Дт 76.АВ) вычитается
|
||||
доля, пропорциональная зачтённой ОБЛАГАЕМОЙ отгрузкой части аванса
|
||||
(Дт 62.02 по origin в проводке-реализации, содержащей НДС).
|
||||
"""
|
||||
self.ensure_one()
|
||||
Line = self.env['rsbu.move.line'].sudo()
|
||||
vat_advance = self._role_account('vat_advance') # 76.АВ
|
||||
vat_output = self._role_account('vat_output') # 68.02/68.НДС
|
||||
advance = self._role_account('advance_received') # 62.02
|
||||
if not (vat_advance and vat_output and advance):
|
||||
return []
|
||||
|
||||
dom = [('company_id', '=', self.company_id.id), ('state', '=', 'posted'),
|
||||
('date', '<=', self.date_to)]
|
||||
# НДС по origin (Дт 76.АВ).
|
||||
data = {}
|
||||
for ln in Line.search(dom + [('account_id', '=', vat_advance.id),
|
||||
('debit', '>', 0),
|
||||
('origin_move_id', '!=', False)]):
|
||||
d = data.setdefault(ln.origin_move_id.id, {
|
||||
'origin': ln.origin_move_id, 'vat': 0.0,
|
||||
'advance': 0.0, 'offset': 0.0, 'partner': ln.partner_id})
|
||||
d['vat'] += ln.debit
|
||||
# Сумма аванса и зачёт облагаемой отгрузкой (Дт/Кт 62.02 по origin).
|
||||
for ln in Line.search(dom + [('account_id', '=', advance.id),
|
||||
('origin_move_id', 'in',
|
||||
list(data.keys()))]):
|
||||
d = data.get(ln.origin_move_id.id)
|
||||
if not d:
|
||||
continue
|
||||
if ln.credit:
|
||||
d['advance'] += ln.credit
|
||||
if ln.debit and self._move_has_vat(ln.move_id, vat_output):
|
||||
d['offset'] += ln.debit
|
||||
|
||||
lines = []
|
||||
for d in data.values():
|
||||
if d['advance'] <= 0 or d['offset'] <= 0:
|
||||
continue
|
||||
share = min(1.0, d['offset'] / d['advance'])
|
||||
vat_deduct = round(d['vat'] * share, 2)
|
||||
if vat_deduct <= 0:
|
||||
continue
|
||||
lines.append({
|
||||
'account_id': vat_output.id, 'debit': vat_deduct,
|
||||
'name': _('Вычет авансового НДС')})
|
||||
lines.append({
|
||||
'account_id': vat_advance.id, 'credit': vat_deduct,
|
||||
'partner_id': d['partner'].id if d['partner'] else False,
|
||||
'origin_move_id': d['origin'].id,
|
||||
'name': _('Списан авансовый НДС')})
|
||||
return lines
|
||||
|
||||
def _move_has_vat(self, move, vat_output):
|
||||
"""Есть ли в проводке-реализации начисление НДС (Кт vat_output)."""
|
||||
return any(l.account_id == vat_output and l.credit > 0
|
||||
for l in move.line_ids)
|
||||
|
||||
def action_vat_deduction(self):
|
||||
"""Сформировать проводку вычета авансового НДС (книга покупок)."""
|
||||
for rec in self:
|
||||
lines = rec._vat_deduction_lines()
|
||||
if lines:
|
||||
rec._new_move(_('Формирование записей книги покупок'), lines)
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Регламент 2: перенос налогов на ЕНС и начисление УСН
|
||||
# ------------------------------------------------------------------
|
||||
def action_ens_transfer(self):
|
||||
"""Перенести НДС и УСН на ЕНС (68.90) по кредитовому сальдо счетов."""
|
||||
for rec in self:
|
||||
ens = rec._acc('68.90')
|
||||
vat = rec._role_account('vat_output') or rec._acc('68.02')
|
||||
usn = rec._acc('68.12')
|
||||
if not ens:
|
||||
# Счёт ЕНС есть в 1С-кодировке; в академическом плане 94н
|
||||
# его может не быть — перенос на ЕНС тогда пропускаем.
|
||||
continue
|
||||
lines = []
|
||||
for tax_acc in (vat, usn):
|
||||
if not tax_acc:
|
||||
continue
|
||||
bal = tax_acc.get_balance(
|
||||
date_to=rec.date_to, company_id=rec.company_id.id)
|
||||
# Кредитовое сальдо налога → переносим на ЕНС.
|
||||
amount = bal['balance'] if bal['side'] == 'credit' else 0.0
|
||||
amount = round(amount, 2)
|
||||
if amount <= 0:
|
||||
continue
|
||||
lines.append({
|
||||
'account_id': tax_acc.id, 'debit': amount,
|
||||
'name': _('Перенос на ЕНС')})
|
||||
lines.append({
|
||||
'account_id': ens.id, 'credit': amount,
|
||||
'name': _('Перенос на ЕНС: %s') % tax_acc.code})
|
||||
if lines:
|
||||
rec._new_move(_('Перенос налогов на ЕНС'), lines)
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Регламент 3: начисление налога УСН (Дт 99.01.1 — Кт 68.12)
|
||||
# ------------------------------------------------------------------
|
||||
usn_tax_amount = fields.Monetary(
|
||||
string='Налог УСН к начислению', currency_field='currency_id',
|
||||
help='Сумма налога УСН за период. Рассчитывается нарастающим итогом '
|
||||
'с начала года кнопкой «Рассчитать налог УСН» (база КУДиР × '
|
||||
'ставка − вычет взносов − авансы прошлых периодов) либо '
|
||||
'вводится вручную. Начисляется проводкой Дт 99.01.1 — Кт 68.12.')
|
||||
usn_tax_base = fields.Monetary(
|
||||
string='База УСН (нараст.)', currency_field='currency_id',
|
||||
help='Налоговая база УСН нарастающим итогом с начала года: доход '
|
||||
'раздела I КУДиР (НДС уже исключён).')
|
||||
usn_reduction = fields.Monetary(
|
||||
string='Вычет (взносы, пособия)', currency_field='currency_id',
|
||||
help='Сумма страховых взносов и пособий, уменьшающая налог УСН '
|
||||
'«Доходы» (в пределах лимита). Заполняется расчётом из раздела '
|
||||
'IV КУДиР; при необходимости корректируется вручную.')
|
||||
usn_advance_paid = fields.Monetary(
|
||||
string='Авансы УСН прошлых периодов', currency_field='currency_id',
|
||||
help='Ранее начисленный налог УСН с начала года (обороты Дт 99.01.1 '
|
||||
'до начала периода), вычитается из налога нарастающим итогом.')
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', related='company_id.currency_id')
|
||||
|
||||
def _year_start(self):
|
||||
"""Первое января года периода (для расчёта нарастающим итогом)."""
|
||||
self.ensure_one()
|
||||
return self.date_to.replace(month=1, day=1) if self.date_to else False
|
||||
|
||||
def _usn_income_base(self, date_from, date_to):
|
||||
"""Доход раздела I КУДиР за интервал (НДС уже исключён)."""
|
||||
self.ensure_one()
|
||||
Kudir = self.env['rsbu.kudir.line'].sudo()
|
||||
Kudir.recompute_period(date_from, date_to, self.company_id)
|
||||
lines = Kudir.search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('section', '=', 'section_1'),
|
||||
('date', '>=', date_from), ('date', '<=', date_to),
|
||||
])
|
||||
return round(sum(lines.mapped('income_amount'))
|
||||
- sum(lines.mapped('expense_amount')), 2)
|
||||
|
||||
def _usn_reduction_amount(self, date_from, date_to):
|
||||
"""Вычет по УСН «Доходы»: взносы/пособия (раздел IV КУДиР) за период."""
|
||||
self.ensure_one()
|
||||
Kudir = self.env['rsbu.kudir.line'].sudo()
|
||||
lines = Kudir.search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('section', '=', 'section_4'),
|
||||
('date', '>=', date_from), ('date', '<=', date_to),
|
||||
])
|
||||
return round(sum(lines.mapped('expense_amount')), 2)
|
||||
|
||||
def _usn_advance_paid_amount(self, year_start):
|
||||
"""Ранее начисленный УСН с начала года до начала периода (Дт 99.01.1)."""
|
||||
self.ensure_one()
|
||||
if not self.date_from:
|
||||
return 0.0
|
||||
profit = self._acc('99.01.1')
|
||||
if not profit:
|
||||
return 0.0
|
||||
Line = self.env['rsbu.move.line'].sudo()
|
||||
lines = Line.search([
|
||||
('account_id', '=', profit.id), ('debit', '>', 0),
|
||||
('company_id', '=', self.company_id.id),
|
||||
('date', '>=', year_start), ('date', '<', self.date_from),
|
||||
])
|
||||
return round(sum(lines.mapped('debit')), 2)
|
||||
|
||||
def compute_usn_tax(self):
|
||||
"""Рассчитать налог УСН нарастающим итогом с начала года.
|
||||
|
||||
Алгоритм (УСН «Доходы» + НДС 5%, п.1 ст.248 НК; методика 1С):
|
||||
1. База = доход раздела I КУДиР с начала года (НДС уже исключён);
|
||||
2. Исчислено = база × ставка (rsbu_usn_rate);
|
||||
3. Уменьшение на взносы/пособия (раздел IV КУДиР) — в пределах
|
||||
лимита rsbu_usn_reduction_limit % от исчисленного (50% для
|
||||
организаций/ИП с работниками, 100% для ИП без работников);
|
||||
4. Минус ранее начисленный УСН с начала года (авансы прошлых
|
||||
периодов).
|
||||
Результат — налог к начислению за текущий период (Дт 99.01.1 —
|
||||
Кт 68.12). Взнос/пособия и авансы показываются отдельными полями
|
||||
для прозрачности; их можно скорректировать вручную перед
|
||||
начислением.
|
||||
"""
|
||||
for rec in self:
|
||||
company = rec.company_id
|
||||
year_start = rec._year_start()
|
||||
if not year_start:
|
||||
continue
|
||||
base = rec._usn_income_base(year_start, rec.date_to)
|
||||
rate = (company.rsbu_usn_rate or 0.0) / 100.0
|
||||
computed = round(base * rate, 2)
|
||||
reduction = rec._usn_reduction_amount(year_start, rec.date_to)
|
||||
# Лимит уменьшения (для «Доходы»): не более N% исчисленного.
|
||||
limit_pct = (company.rsbu_usn_reduction_limit or 0.0) / 100.0
|
||||
if company.rsbu_usn_mode == 'usn_income' and limit_pct:
|
||||
reduction = min(reduction, round(computed * limit_pct, 2))
|
||||
advance_paid = rec._usn_advance_paid_amount(year_start)
|
||||
tax = round(computed - reduction - advance_paid, 2)
|
||||
rec.usn_tax_base = base
|
||||
rec.usn_reduction = reduction
|
||||
rec.usn_advance_paid = advance_paid
|
||||
rec.usn_tax_amount = max(tax, 0.0)
|
||||
return True
|
||||
|
||||
def action_accrue_usn(self):
|
||||
"""Начислить налог УСН: Дт 99.01.1 — Кт 68.12 на usn_tax_amount."""
|
||||
for rec in self:
|
||||
amount = round(rec.usn_tax_amount or 0.0, 2)
|
||||
if amount <= 0:
|
||||
continue
|
||||
if rec._has_move_by_ref(_('Начисление налога УСН')):
|
||||
continue
|
||||
profit = rec._acc('99.01.1') or rec._acc('99.01') or rec._acc('99')
|
||||
usn = rec._acc('68.12')
|
||||
if not (profit and usn):
|
||||
raise UserError(_('Не найдены счета 99.01.1 или 68.12.'))
|
||||
rec._new_move(_('Начисление налога УСН'), [
|
||||
{'account_id': profit.id, 'debit': amount,
|
||||
'name': _('Начислен налог УСН')},
|
||||
{'account_id': usn.id, 'credit': amount,
|
||||
'name': _('Налог УСН к уплате')},
|
||||
])
|
||||
return True
|
||||
|
||||
def action_close(self):
|
||||
"""Полное закрытие месяца в порядке 1С.
|
||||
|
||||
Цепочка (по методике 1С для УСН+НДС):
|
||||
1. Закрытие затрат 26 → 90.08 (директ-костинг);
|
||||
2. Налоги (только на конец квартала): расчёт и начисление УСН,
|
||||
вычет авансового НДС, перенос НДС/УСН на ЕНС;
|
||||
3. Финансовый результат: закрытие 90 → 99 и 91 → 99;
|
||||
4. Реформация баланса (только декабрь): сальдо 99 → 84.
|
||||
"""
|
||||
for rec in self:
|
||||
rec._check_can_close()
|
||||
# 1. Затраты.
|
||||
rec.action_close_expenses()
|
||||
# 2. Налоги — на конец квартала (март/июнь/сентябрь/декабрь).
|
||||
if rec._is_quarter_end():
|
||||
rec.compute_usn_tax()
|
||||
if rec.usn_tax_amount:
|
||||
rec.action_accrue_usn()
|
||||
rec.action_vat_deduction()
|
||||
rec.action_ens_transfer()
|
||||
# 3. Финрезультат.
|
||||
rec.action_close_pl()
|
||||
# 4. Реформация баланса (декабрь).
|
||||
if rec._is_year_end():
|
||||
rec.action_reform_balance()
|
||||
rec.state = 'done'
|
||||
return True
|
||||
|
||||
def _check_can_close(self):
|
||||
"""Проверки перед закрытием месяца (даты, завершённость, порядок).
|
||||
|
||||
1. Границы периода должны соответствовать ровно календарному месяцу
|
||||
period_month (защита от рассинхрона date_from/date_to).
|
||||
2. Нельзя закрывать месяц раньше его окончания (незавершённый или
|
||||
будущий месяц).
|
||||
3. Нельзя закрывать месяц раньше даты начала учёта РСБУ.
|
||||
4. Последовательность (как в 1С): предыдущий месяц должен быть
|
||||
закрыт — но только если он не раньше месяца начала учёта. Месяц
|
||||
начала учёта закрывается свободно (первый в цепочке).
|
||||
5. Повторно закрывать уже закрытый месяц нельзя.
|
||||
"""
|
||||
import calendar
|
||||
self.ensure_one()
|
||||
if self.state == 'done':
|
||||
raise UserError(_('Месяц уже закрыт. Сначала переоткройте.'))
|
||||
if not self.period_month:
|
||||
raise UserError(_('Не задан месяц закрытия.'))
|
||||
# 1. Согласованность границ с месяцем.
|
||||
first = self.period_month.replace(day=1)
|
||||
last_day = calendar.monthrange(first.year, first.month)[1]
|
||||
expected_to = first.replace(day=last_day)
|
||||
if self.date_from != first or self.date_to != expected_to:
|
||||
raise UserError(_(
|
||||
'Период закрытия должен точно соответствовать календарному '
|
||||
'месяцу %(m)s %(y)s (с %(f)s по %(t)s). Проверьте поле '
|
||||
'«Месяц закрытия».',
|
||||
m=self._MONTH_NAMES[first.month], y=first.year,
|
||||
f=first, t=expected_to))
|
||||
# 2. Месяц должен быть завершён.
|
||||
if expected_to >= fields.Date.context_today(self):
|
||||
raise UserError(_(
|
||||
'Нельзя закрывать незавершённый или будущий месяц: закрытие '
|
||||
'возможно только после окончания месяца (%s).') % expected_to)
|
||||
# 3. Не раньше месяца начала учёта.
|
||||
start = self.company_id.rsbu_accounting_start
|
||||
start_month = start.replace(day=1) if start else False
|
||||
if start_month and first < start_month:
|
||||
raise UserError(_(
|
||||
'Учёт РСБУ ведётся с %(s)s. Месяцы до месяца начала учёта '
|
||||
'закрывать не требуется.', s=start))
|
||||
# 4. Последовательность — только если предыдущий месяц не раньше
|
||||
# месяца начала учёта. Месяц начала учёта — первый в цепочке.
|
||||
if not (start_month and first <= start_month):
|
||||
prev_last = first - timedelta(days=1)
|
||||
prev_first = prev_last.replace(day=1)
|
||||
# Требуем закрытым предыдущий месяц, если он в периоде учёта
|
||||
# (>= месяца начала) и по нему есть проводки.
|
||||
in_accounting = not start_month or prev_first >= start_month
|
||||
Line = self.env['rsbu.move.line'].sudo()
|
||||
has_prev_postings = bool(Line.search_count([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('state', '=', 'posted'),
|
||||
('date', '>=', prev_first),
|
||||
('date', '<=', prev_last),
|
||||
]))
|
||||
if in_accounting and has_prev_postings:
|
||||
prev_closed = self.search_count([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('state', '=', 'done'),
|
||||
('date_from', '=', prev_first),
|
||||
('date_to', '=', prev_last),
|
||||
])
|
||||
if not prev_closed:
|
||||
raise UserError(_(
|
||||
'Сначала закройте предыдущий месяц (%(m)s %(y)s): '
|
||||
'закрытие выполняется последовательно.',
|
||||
m=self._MONTH_NAMES[prev_first.month],
|
||||
y=prev_first.year))
|
||||
return True
|
||||
|
||||
def action_reopen(self):
|
||||
"""Отменить регламентные проводки и вернуть в черновик."""
|
||||
for rec in self:
|
||||
moves = rec.move_ids.sudo()
|
||||
posted = moves.filtered(lambda m: m.state == 'posted')
|
||||
if posted:
|
||||
posted.action_draft()
|
||||
moves.unlink()
|
||||
rec.move_ids = [(5, 0, 0)]
|
||||
rec.state = 'draft'
|
||||
return True
|
||||
Reference in New Issue
Block a user