Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC
This commit is contained in:
235
rsbu_fns_reporting/models/rsbu_ndfl_report.py
Normal file
235
rsbu_fns_reporting/models/rsbu_ndfl_report.py
Normal file
@@ -0,0 +1,235 @@
|
||||
# -*- 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')
|
||||
Reference in New Issue
Block a user