Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

View 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')