# -*- coding: utf-8 -*- """Расчёт по страховым взносам (РСВ, КНД 1151111, формат NO_RASCHSV 5.08). Строится поквартально нарастающим итогом с начала года из помесячных итогов взносов (rsbu.rsv.summary). Профиль: УСН + субъект МСП, единый пониженный тариф (п.2.5 ст.427 НК). Разделы (первый релиз): * Раздел 1 (ОбязПлатСВ) — сводные суммы взносов к уплате по единому тарифу за расчётный период и помесячно (3 месяца квартала); * подраздел 1 (РасчСВ_ОПСОМС) — расчёт по единому тарифу: число застрахованных, база (в пределах/сверх предельной базы ОПС), исчисленные взносы (нарастающим итогом + 3 месяца); * Приложение 3.1 (ПравТариф13.2.427) — расчёт соответствия условиям применения единого пониженного тарифа МСП (доходы — реквизиты); * Раздел 3 (ПерсСвСтрахЛиц) — персонифицированные сведения по каждому сотруднику (выплаты/база/взносы по месяцам квартала). Расчёт и хранение — здесь; XML-выгрузка (5.08, windows-1251) — в rsbu_rsv_xml. Реквизиты — из настроек компании (rsbu_reporting_base, rsbu_payroll, l10n_ru). """ from odoo import api, fields, models # Код отчётного периода РСВ по номеру квартала (нарастающим итогом). RSV_PERIOD_CODE = {1: '21', 2: '31', 3: '33', 4: '34'} class RsbuRsvDeclaration(models.Model): _name = 'rsbu.rsv.declaration' _description = 'РСБУ — Расчёт по страховым взносам (РСВ)' _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', help='Расчётный (отчётный) период нарастающим итогом.') 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='ОКТМО') avg_headcount = fields.Integer( string='Среднесписочная численность', help='Среднесписочная численность работников (атрибут СрЧисл).') # --- Раздел 1 / подраздел 1: расчёт по единому тарифу (нараст. итогом) --- insured_count = fields.Integer(string='Кол-во застрахованных лиц') charged_count = fields.Integer( string='Кол-во лиц с начисленными взносами') payments_total = fields.Monetary(string='Выплаты (всего)', currency_field='currency_id') non_taxable_total = fields.Monetary(string='Необлагаемые выплаты', currency_field='currency_id') base_total = fields.Monetary(string='База для исчисления', currency_field='currency_id') base_within_total = fields.Monetary( string='База не превышающая предел (ОПС)', currency_field='currency_id') base_over_total = fields.Monetary( string='База сверх предела (ОПС)', currency_field='currency_id') charged_total = fields.Monetary(string='Исчислено взносов (всего)', currency_field='currency_id') charged_within_total = fields.Monetary( string='Исчислено с базы в пределах', currency_field='currency_id') charged_over_total = fields.Monetary( string='Исчислено с базы сверх', currency_field='currency_id') oms_total = fields.Monetary(string='Взносы ОМС', currency_field='currency_id') vnim_total = fields.Monetary(string='Взносы ВНиМ', currency_field='currency_id') # Помесячная разбивка последних 3 месяцев квартала (к уплате, единый тариф). contrib_month1 = fields.Monetary(string='Взносы 1-й мес.', currency_field='currency_id') contrib_month2 = fields.Monetary(string='Взносы 2-й мес.', currency_field='currency_id') contrib_month3 = fields.Monetary(string='Взносы 3-й мес.', currency_field='currency_id') contrib_period = fields.Monetary(string='Взносы за период (единый тариф)', currency_field='currency_id') # --- Приложение 3.1 (МСП, п.13.2 ст.427): доходы для проверки условий --- msp_income_total = fields.Monetary( string='Доходы всего (ст.26.2)', currency_field='currency_id', help='Доходы для проверки соответствия условиям МСП (заполняются ' 'по данным УСН/бухучёта). Приложение 3.1 РСВ.') msp_income_main = fields.Monetary( string='Доходы по основному виду деятельности', currency_field='currency_id') pers_line_ids = fields.One2many( 'rsbu.rsv.pers.line', 'declaration_id', string='Персонифицированные сведения (раздел 3)') @api.depends('year', 'quarter') def _compute_name(self): labels = dict(self._fields['quarter'].selection) for rec in self: rec.name = 'РСВ за %s, %s' % ( labels.get(rec.quarter, ''), rec.year or '?') def _period_bounds(self): """Границы периода: (year_start, quarter_last_day, q1..q_last).""" self.ensure_one() q = int(self.quarter) last_month = q * 3 import calendar 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): """Пересобрать итоги взносов за год и рассчитать РСВ.""" Summary = self.env['rsbu.rsv.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): """Рассчитать показатели РСВ из готовых помесячных итогов.""" Summary = self.env['rsbu.rsv.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 месяца квартала (для помесячной разбивки). last3 = Summary.records_for(company, rec.year, m1, m3) rec.payments_total = round(sum(ytd.mapped('payments')), 2) rec.non_taxable_total = round(sum(ytd.mapped('non_taxable')), 2) rec.base_total = round(sum(ytd.mapped('base')), 2) rec.base_within_total = round( sum(ytd.mapped('base_within_limit')), 2) rec.base_over_total = round( sum(ytd.mapped('base_over_limit')), 2) rec.charged_total = round(sum(ytd.mapped('ops')), 2) rec.charged_within_total = round(sum(ytd.mapped('ops_within')), 2) rec.charged_over_total = round(sum(ytd.mapped('ops_over')), 2) rec.oms_total = round(sum(ytd.mapped('oms')), 2) rec.vnim_total = round(sum(ytd.mapped('vnim')), 2) # Число застрахованных / с начислениями (нарастающим итогом). rec.insured_count = len(ytd.mapped('employee_id')) rec.charged_count = len(ytd.filtered( lambda r: r.base > 0).mapped('employee_id')) # Помесячно за последние 3 месяца — взносы единого тарифа # (ОПС+ОМС+ВНиМ) к уплате. def month_contrib(month): recs = last3.filtered(lambda r: r.month == month) return round(sum(recs.mapped('ops')) + sum(recs.mapped('oms')) + sum(recs.mapped('vnim')), 2) rec.contrib_month1 = month_contrib(m1) rec.contrib_month2 = month_contrib(m2) rec.contrib_month3 = month_contrib(m3) rec.contrib_period = round( rec.charged_total + rec.oms_total + rec.vnim_total, 2) if not rec.oktmo: rec.oktmo = company.rsbu_oktmo or '' # --- Раздел 3: строки по сотрудникам за 3 месяца квартала --- rec.pers_line_ids.unlink() PersLine = self.env['rsbu.rsv.pers.line'] by_emp = {} for r in last3: by_emp.setdefault(r.employee_id, []).append(r) pers_vals = [] for emp, recs in by_emp.items(): def m_sum(month, field): mr = [x for x in recs if x.month == month] return round(sum(x[field] for x in mr), 2) pers_vals.append({ 'declaration_id': rec.id, 'employee_id': emp.id, 'category': recs[0].insured_category or 'НР', 'pay_m1': m_sum(m1, 'payments'), 'pay_m2': m_sum(m2, 'payments'), 'pay_m3': m_sum(m3, 'payments'), 'base_m1': m_sum(m1, 'base'), 'base_m2': m_sum(m2, 'base'), 'base_m3': m_sum(m3, 'base'), 'ops_m1': m_sum(m1, 'ops'), 'ops_m2': m_sum(m2, 'ops'), 'ops_m3': m_sum(m3, 'ops'), }) if pers_vals: PersLine.create(pers_vals) rec.state = 'computed' return True class RsbuRsvPersLine(models.Model): _name = 'rsbu.rsv.pers.line' _description = 'РСБУ — Перс. сведения РСВ (раздел 3)' _order = 'declaration_id, employee_id' declaration_id = fields.Many2one( 'rsbu.rsv.declaration', string='Расчёт', required=True, ondelete='cascade', index=True) employee_id = fields.Many2one('hr.employee', string='Сотрудник', required=True) currency_id = fields.Many2one( 'res.currency', related='declaration_id.currency_id') category = fields.Char(string='Код категории') pay_m1 = fields.Monetary(string='Выплаты 1-й мес.', currency_field='currency_id') pay_m2 = fields.Monetary(string='Выплаты 2-й мес.', currency_field='currency_id') pay_m3 = fields.Monetary(string='Выплаты 3-й мес.', currency_field='currency_id') base_m1 = fields.Monetary(string='База 1-й мес.', currency_field='currency_id') base_m2 = fields.Monetary(string='База 2-й мес.', currency_field='currency_id') base_m3 = fields.Monetary(string='База 3-й мес.', currency_field='currency_id') ops_m1 = fields.Monetary(string='ОПС 1-й мес.', currency_field='currency_id') ops_m2 = fields.Monetary(string='ОПС 2-й мес.', currency_field='currency_id') ops_m3 = fields.Monetary(string='ОПС 3-й мес.', currency_field='currency_id')