# -*- coding: utf-8 -*- """Уведомление об исчисленных суммах налогов, авансов, сборов, взносов. Форма ФНС КНД 1110355, формат UT_UVISCHSUMNAL версии 5.03 (приказ ФНС от 16.01.2024 № ЕД-7-8/20@). Подаётся в составе механизма ЕНП в месяцы, когда налог/взносы исчислены, но срок декларации/расчёта ещё не наступил. Автозаполнение строк за отчётный месяц из данных учёта: * страховые взносы — помесячные итоги rsbu.rsv.summary (ОПС+ОМС+ВНиМ); * НДФЛ — удержанный налог из проведённых расчётных листков (строки NDFL); * аванс УСН — из закрытия периода (rsbu.period.close) за квартал. Каждая строка несёт КБК/ОКТМО/период/сумму. Один документ может содержать несколько строк по разным налогам (НДФЛ + взносы + аванс УСН). """ from odoo import api, fields, models # Код квартала (Период) по номеру квартала. ENP_PERIOD_CODE = {1: '21', 2: '31', 3: '33', 4: '34'} class RsbuEnpNotification(models.Model): _name = 'rsbu.enp.notification' _description = 'РСБУ — Уведомление об исчисленных суммах (ЕНП)' _order = 'year desc, month 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) month = fields.Integer( string='Отчётный месяц', required=True, default=lambda self: fields.Date.today().month, help='Календарный месяц, за который исчислены суммы. Определяет ' 'код периода (квартал) и номер месяца в квартале.') date_doc = fields.Date( string='Дата документа', default=fields.Date.context_today) state = fields.Selection( selection=[('draft', 'Черновик'), ('computed', 'Рассчитано')], default='draft', string='Состояние', copy=False) currency_id = fields.Many2one( 'res.currency', related='company_id.currency_id') line_ids = fields.One2many( 'rsbu.enp.notification.line', 'notification_id', string='Строки') @api.depends('year', 'month') def _compute_name(self): names = ['', 'январь', 'февраль', 'март', 'апрель', 'май', 'июнь', 'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь'] for rec in self: m = names[rec.month] if rec.month and 1 <= rec.month <= 12 else '?' rec.name = 'Уведомление ЕНП: %s %s' % (m, rec.year or '?') def _quarter(self): self.ensure_one() return (self.month - 1) // 3 + 1 if self.month else 1 def _month_in_quarter(self): """Порядковый номер месяца в квартале (01..03).""" self.ensure_one() return '%02d' % ((self.month - 1) % 3 + 1) def _month_bounds(self): import calendar self.ensure_one() last = calendar.monthrange(self.year, self.month)[1] return (fields.Date.to_date('%04d-%02d-01' % (self.year, self.month)), fields.Date.to_date('%04d-%02d-%02d' % ( self.year, self.month, last))) # ------------------------------------------------------------------ def _insurance_amount(self): """Начисленные страховые взносы (ОПС+ОМС+ВНиМ) за отчётный месяц.""" self.ensure_one() Summary = self.env['rsbu.rsv.summary'].sudo() recs = Summary.search([ ('company_id', '=', self.company_id.id), ('year', '=', self.year), ('month', '=', self.month), ]) return round(sum(recs.mapped('ops')) + sum(recs.mapped('oms')) + sum(recs.mapped('vnim')), 2) def _ndfl_amount(self): """Удержанный НДФЛ за отчётный месяц (строки NDFL в листках).""" self.ensure_one() date_from, date_to = self._month_bounds() slips = self.env['hr.payslip'].sudo().search([ ('company_id', '=', self.company_id.id), ('state', '=', 'done'), ('date_from', '>=', date_from), ('date_from', '<=', date_to), ]) total = 0.0 for slip in slips: total += abs(sum(slip.line_ids.filtered( lambda l: l.code == 'NDFL').mapped('total'))) return round(total, 2) def _usn_advance_amount(self): """Аванс УСН за квартал (если месяц — месяц уплаты аванса). Авансы УСН уплачиваются по итогам квартала в следующем месяце (апрель/июль/октябрь). Берём из закрытия периода последнего месяца соответствующего квартала. """ self.ensure_one() # Уведомление по УСН-авансу подаётся в апреле/июле/октябре за # предыдущий квартал (1/2/3). За 4 квартал — годовая декларация. usn_quarter = {4: 1, 7: 2, 10: 3}.get(self.month) if not usn_quarter: return 0.0 import calendar q_last_month = usn_quarter * 3 last = calendar.monthrange(self.year, q_last_month)[1] close = self.env['rsbu.period.close'].sudo().search([ ('company_id', '=', self.company_id.id), ('date_to', '=', fields.Date.to_date('%04d-%02d-%02d' % ( self.year, q_last_month, last))), ], limit=1) return round(close.usn_tax_amount, 2) if close else 0.0 def action_compute(self): """Пересобрать итоги взносов за месяц и автозаполнить строки.""" for rec in self: df, dt = rec._month_bounds() self.env['rsbu.rsv.summary'].sudo().recompute_period( df, dt, rec.company_id) return self.fill_lines() def fill_lines(self): """Автозаполнить строки из готовых итогов учёта за месяц. Отделено от action_compute, чтобы заполнять по существующим данным без принудительного пересбора итогов (тесты, повторный расчёт). """ for rec in self: rec.line_ids.unlink() company = rec.company_id oktmo = company.rsbu_oktmo or '' period = ENP_PERIOD_CODE[rec._quarter()] mq = rec._month_in_quarter() Line = self.env['rsbu.enp.notification.line'] vals = [] insurance = rec._insurance_amount() if insurance > 0: vals.append({ 'notification_id': rec.id, 'kind': 'insurance', 'kbk': company.rsbu_kbk_insurance or '', 'oktmo': oktmo, 'amount': insurance, 'period_code': period, 'month_in_quarter': mq, 'report_year': rec.year, }) ndfl = rec._ndfl_amount() if ndfl > 0: vals.append({ 'notification_id': rec.id, 'kind': 'ndfl', 'kbk': company.rsbu_kbk_ndfl or '', 'oktmo': oktmo, 'amount': ndfl, 'period_code': period, 'month_in_quarter': mq, 'report_year': rec.year, }) usn = rec._usn_advance_amount() if usn > 0: # Аванс УСН: период — квартал, за который платится аванс. usn_q = {4: 1, 7: 2, 10: 3}[rec.month] vals.append({ 'notification_id': rec.id, 'kind': 'usn', 'kbk': company.rsbu_kbk_usn or '', 'oktmo': oktmo, 'amount': usn, 'period_code': ENP_PERIOD_CODE[usn_q], 'month_in_quarter': '03', 'report_year': rec.year, }) if vals: Line.create(vals) rec.state = 'computed' return True class RsbuEnpNotificationLine(models.Model): _name = 'rsbu.enp.notification.line' _description = 'РСБУ — Строка уведомления ЕНП' _order = 'notification_id, kind' notification_id = fields.Many2one( 'rsbu.enp.notification', string='Уведомление', required=True, ondelete='cascade', index=True) currency_id = fields.Many2one( 'res.currency', related='notification_id.currency_id') kind = fields.Selection( selection=[('insurance', 'Страховые взносы'), ('ndfl', 'НДФЛ'), ('usn', 'Аванс УСН')], string='Вид платежа', required=True) kpp_decl = fields.Char(string='КПП (деклар.)') oktmo = fields.Char(string='ОКТМО', required=True) kbk = fields.Char(string='КБК', required=True) amount = fields.Monetary(string='Сумма', currency_field='currency_id') period_code = fields.Char(string='Период', required=True, help='Код периода: 21/31/33/34.') month_in_quarter = fields.Char( string='Номер месяца/квартала', required=True, help='Номер месяца в квартале (01-03) либо квартальный код (11-13).') report_year = fields.Integer(string='Год', required=True)