# -*- 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')