Files
public/rsbu_fns_reporting/models/rsbu_ndfl_report.py

236 lines
12 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.
# -*- 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')