236 lines
12 KiB
Python
236 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Расчёт сумм НДФЛ налогового агента (6-НДФЛ, КНД 1151100, формат 5.05).
|
||
|
||
Строится поквартально нарастающим итогом с начала года из помесячных
|
||
итогов НДФЛ (rsbu.ndfl.summary).
|
||
|
||
Разделы:
|
||
* Раздел 1 (ОбязНА) — удержанный НДФЛ к перечислению: сумма нарастающим
|
||
итогом + разбивка по шести срокам квартала (каждый месяц квартала —
|
||
два периода: дни 1–22 и 23–конец);
|
||
* Раздел 2 (РасчСумНал) — по каждой ставке: число физлиц, начисленный
|
||
доход, база, исчисленный и удержанный налог, с помесячной разбивкой
|
||
(3 месяца последнего квартала, логика как в РСВ);
|
||
* Приложение №1 (СправДох) — годовые справки о доходах; заложена
|
||
структура (строки rsbu.ndfl.report.cert), наполнение — для годового
|
||
расчёта.
|
||
"""
|
||
from odoo import api, fields, models
|
||
|
||
NDFL_PERIOD_CODE = {1: '21', 2: '31', 3: '33', 4: '34'}
|
||
|
||
|
||
class RsbuNdflReport(models.Model):
|
||
_name = 'rsbu.ndfl.report'
|
||
_description = 'РСБУ — Расчёт 6-НДФЛ'
|
||
_order = 'year desc, quarter desc, id desc'
|
||
_check_company_auto = True
|
||
|
||
name = fields.Char(string='Наименование', compute='_compute_name',
|
||
store=True)
|
||
company_id = fields.Many2one(
|
||
'res.company', string='Компания', required=True, index=True,
|
||
default=lambda self: self.env.company)
|
||
year = fields.Integer(string='Отчётный год', required=True,
|
||
default=lambda self: fields.Date.today().year)
|
||
quarter = fields.Selection(
|
||
selection=[('1', 'I квартал'), ('2', 'Полугодие'),
|
||
('3', '9 месяцев'), ('4', 'Год')],
|
||
string='Отчётный период', required=True, default='1')
|
||
currency_id = fields.Many2one(
|
||
'res.currency', related='company_id.currency_id')
|
||
state = fields.Selection(
|
||
selection=[('draft', 'Черновик'), ('computed', 'Рассчитан')],
|
||
default='draft', string='Состояние', copy=False)
|
||
oktmo = fields.Char(string='ОКТМО')
|
||
kbk = fields.Char(string='КБК')
|
||
|
||
# --- Раздел 1 (ОбязНА): удержанный НДФЛ по 6 срокам квартала ---
|
||
withheld_total = fields.Monetary(
|
||
string='Удержано (нараст. итогом)', currency_field='currency_id')
|
||
returned_total = fields.Monetary(
|
||
string='Возвращено (нараст. итогом)', currency_field='currency_id')
|
||
term1 = fields.Monetary(string='Срок 1', currency_field='currency_id')
|
||
term2 = fields.Monetary(string='Срок 2', currency_field='currency_id')
|
||
term3 = fields.Monetary(string='Срок 3', currency_field='currency_id')
|
||
term4 = fields.Monetary(string='Срок 4', currency_field='currency_id')
|
||
term5 = fields.Monetary(string='Срок 5', currency_field='currency_id')
|
||
term6 = fields.Monetary(string='Срок 6', currency_field='currency_id')
|
||
|
||
line_ids = fields.One2many(
|
||
'rsbu.ndfl.report.line', 'report_id',
|
||
string='Расчёт по ставкам (раздел 2)')
|
||
cert_ids = fields.One2many(
|
||
'rsbu.ndfl.report.cert', 'report_id',
|
||
string='Справки о доходах (приложение №1)')
|
||
|
||
@api.depends('year', 'quarter')
|
||
def _compute_name(self):
|
||
labels = dict(self._fields['quarter'].selection)
|
||
for rec in self:
|
||
rec.name = '6-НДФЛ за %s, %s' % (
|
||
labels.get(rec.quarter, ''), rec.year or '?')
|
||
|
||
def _period_bounds(self):
|
||
self.ensure_one()
|
||
import calendar
|
||
q = int(self.quarter)
|
||
last_month = q * 3
|
||
last_day = calendar.monthrange(self.year, last_month)[1]
|
||
return (fields.Date.to_date('%04d-01-01' % self.year),
|
||
fields.Date.to_date('%04d-%02d-%02d' % (
|
||
self.year, last_month, last_day)), q)
|
||
|
||
# ------------------------------------------------------------------
|
||
def action_compute(self):
|
||
"""Пересобрать итоги НДФЛ за год и рассчитать 6-НДФЛ."""
|
||
Summary = self.env['rsbu.ndfl.summary'].sudo()
|
||
for rec in self:
|
||
date_from, date_to, _q = rec._period_bounds()
|
||
Summary.recompute_period(date_from, date_to, rec.company_id)
|
||
return self.compute_from_summary()
|
||
|
||
def compute_from_summary(self):
|
||
"""Рассчитать показатели 6-НДФЛ из готовых помесячных итогов."""
|
||
Summary = self.env['rsbu.ndfl.summary'].sudo()
|
||
for rec in self:
|
||
company = rec.company_id
|
||
q = int(rec.quarter)
|
||
m1, m2, m3 = Summary._quarter_months(q)
|
||
ytd = Summary.ytd_records(company, rec.year, q)
|
||
# Последние 3 месяца квартала (для раздела 2 и сроков раздела 1).
|
||
last3 = ytd.filtered(lambda r: m1 <= r.month <= m3)
|
||
|
||
kbk = company.rsbu_kbk_ndfl or ''
|
||
rec.oktmo = rec.oktmo or company.rsbu_oktmo or ''
|
||
rec.kbk = kbk
|
||
|
||
# --- Раздел 1: удержано нарастающим + по 6 срокам квартала ---
|
||
# Срок считается по дате удержания (месяц удержания в квартале).
|
||
# Удержано нарастающим итогом с начала года — вся сумма удержаний.
|
||
rec.withheld_total = round(sum(ytd.mapped('withheld_tax')), 2)
|
||
rec.returned_total = 0.0
|
||
terms = {i: 0.0 for i in range(1, 7)}
|
||
for r in ytd:
|
||
# Срок относится к кварталу удержания; в раздел 1 отчётного
|
||
# периода попадают сроки последнего квартала (удержания в
|
||
# месяцах m1..m3). Определяем срок по месяцу удержания.
|
||
if r.withholding_month and m1 <= r.withholding_month <= m3:
|
||
term = r.deadline_term or 0
|
||
if 1 <= term <= 6:
|
||
terms[term] += r.withheld_tax
|
||
rec.term1, rec.term2, rec.term3 = (
|
||
round(terms[1], 2), round(terms[2], 2), round(terms[3], 2))
|
||
rec.term4, rec.term5, rec.term6 = (
|
||
round(terms[4], 2), round(terms[5], 2), round(terms[6], 2))
|
||
|
||
# --- Раздел 2: по ставкам, помесячно (3 месяца квартала) ---
|
||
rec.line_ids.unlink()
|
||
Line = self.env['rsbu.ndfl.report.line']
|
||
by_rate = {}
|
||
for r in ytd:
|
||
by_rate.setdefault(r.rate, []).append(r)
|
||
line_vals = []
|
||
for rate, recs in by_rate.items():
|
||
emp_ids = set(x.employee_id.id for x in recs)
|
||
def msum(month, field):
|
||
return round(sum(x[field] for x in recs
|
||
if x.month == month), 2)
|
||
|
||
def mwithheld_half(month, half):
|
||
# Удержано в месяце квартала по полупериоду (1-22/23-).
|
||
total = 0.0
|
||
for x in recs:
|
||
if x.withholding_month == month:
|
||
is_first = x.deadline_term and x.deadline_term % 2 == 1
|
||
if (half == 1 and is_first) or \
|
||
(half == 2 and not is_first):
|
||
total += x.withheld_tax
|
||
return round(total, 2)
|
||
|
||
line_vals.append({
|
||
'report_id': rec.id,
|
||
'rate': rate,
|
||
'kbk': kbk,
|
||
'persons_count': len(emp_ids),
|
||
'accrued': round(sum(x.accrued for x in recs), 2),
|
||
'deduction': round(sum(x.deduction for x in recs), 2),
|
||
'base': round(sum(x.base for x in recs), 2),
|
||
'computed_tax': round(sum(x.computed_tax
|
||
for x in recs), 2),
|
||
'withheld_tax': round(sum(x.withheld_tax
|
||
for x in recs), 2),
|
||
'wh_m1_h1': mwithheld_half(m1, 1),
|
||
'wh_m1_h2': mwithheld_half(m1, 2),
|
||
'wh_m2_h1': mwithheld_half(m2, 1),
|
||
'wh_m2_h2': mwithheld_half(m2, 2),
|
||
'wh_m3_h1': mwithheld_half(m3, 1),
|
||
'wh_m3_h2': mwithheld_half(m3, 2),
|
||
})
|
||
if line_vals:
|
||
Line.create(line_vals)
|
||
rec.state = 'computed'
|
||
return True
|
||
|
||
|
||
class RsbuNdflReportLine(models.Model):
|
||
_name = 'rsbu.ndfl.report.line'
|
||
_description = 'РСБУ — Строка расчёта 6-НДФЛ (раздел 2, по ставке)'
|
||
_order = 'report_id, rate'
|
||
|
||
report_id = fields.Many2one(
|
||
'rsbu.ndfl.report', string='Расчёт', required=True,
|
||
ondelete='cascade', index=True)
|
||
currency_id = fields.Many2one(
|
||
'res.currency', related='report_id.currency_id')
|
||
rate = fields.Integer(string='Ставка, %', required=True)
|
||
kbk = fields.Char(string='КБК')
|
||
persons_count = fields.Integer(string='Кол-во физлиц')
|
||
accrued = fields.Monetary(string='Начислено дохода',
|
||
currency_field='currency_id')
|
||
deduction = fields.Monetary(string='Вычеты',
|
||
currency_field='currency_id')
|
||
base = fields.Monetary(string='Налоговая база',
|
||
currency_field='currency_id')
|
||
computed_tax = fields.Monetary(string='Исчислено',
|
||
currency_field='currency_id')
|
||
withheld_tax = fields.Monetary(string='Удержано',
|
||
currency_field='currency_id')
|
||
# Помесячная разбивка удержаний по полупериодам (3 месяца квартала).
|
||
wh_m1_h1 = fields.Monetary(string='Уд. 1 мес. (1-22)',
|
||
currency_field='currency_id')
|
||
wh_m1_h2 = fields.Monetary(string='Уд. 1 мес. (23-)',
|
||
currency_field='currency_id')
|
||
wh_m2_h1 = fields.Monetary(string='Уд. 2 мес. (1-22)',
|
||
currency_field='currency_id')
|
||
wh_m2_h2 = fields.Monetary(string='Уд. 2 мес. (23-)',
|
||
currency_field='currency_id')
|
||
wh_m3_h1 = fields.Monetary(string='Уд. 3 мес. (1-22)',
|
||
currency_field='currency_id')
|
||
wh_m3_h2 = fields.Monetary(string='Уд. 3 мес. (23-)',
|
||
currency_field='currency_id')
|
||
|
||
|
||
class RsbuNdflReportCert(models.Model):
|
||
_name = 'rsbu.ndfl.report.cert'
|
||
_description = 'РСБУ — Справка о доходах 6-НДФЛ (приложение №1, годовая)'
|
||
_order = 'report_id, employee_id'
|
||
|
||
report_id = fields.Many2one(
|
||
'rsbu.ndfl.report', string='Расчёт', required=True,
|
||
ondelete='cascade', index=True)
|
||
currency_id = fields.Many2one(
|
||
'res.currency', related='report_id.currency_id')
|
||
employee_id = fields.Many2one('hr.employee', string='Сотрудник')
|
||
rate = fields.Integer(string='Ставка, %')
|
||
income_total = fields.Monetary(string='Сумма дохода',
|
||
currency_field='currency_id')
|
||
base = fields.Monetary(string='Налоговая база',
|
||
currency_field='currency_id')
|
||
computed_tax = fields.Monetary(string='Налог исчислен',
|
||
currency_field='currency_id')
|
||
withheld_tax = fields.Monetary(string='Налог удержан',
|
||
currency_field='currency_id')
|
||
not_withheld_tax = fields.Monetary(string='Налог не удержан',
|
||
currency_field='currency_id')
|