# -*- coding: utf-8 -*- """Регистр помесячных итогов страховых взносов по сотрудникам (для РСВ). Аналог rsbu.kudir.summary/rsbu.vat.summary: агрегирует проведённые расчётные листки (hr.payslip) по (сотрудник × месяц), вычисляя базу и взносы ОПС/ОМС/ВНиМ через hr.payslip.rsbu_insurance_breakdown (с учётом регрессии по годовой предельной базе и льготы МСП сверх 1.5 МРОТ). Служит источником данных для расчёта по страховым взносам (РСВ, NO_RASCHSV 5.08): раздел 1 (сводные обязательства и подраздел единого тарифа) и раздел 3 (персонифицированные сведения по месяцам). """ from odoo import api, fields, models class RsbuRsvSummary(models.Model): _name = 'rsbu.rsv.summary' _description = 'РСБУ — Итоги страховых взносов по сотрудникам (РСВ)' _order = 'year desc, month, employee_id' _check_company_auto = True company_id = fields.Many2one( 'res.company', string='Компания', required=True, index=True, default=lambda self: self.env.company) employee_id = fields.Many2one( 'hr.employee', string='Сотрудник', required=True, index=True) year = fields.Integer(string='Год', required=True, index=True) month = fields.Integer(string='Месяц', required=True, index=True) currency_id = fields.Many2one( 'res.currency', string='Валюта', compute='_compute_currency_id', store=True) # Код категории застрахованного (НР/МС) — из сотрудника на момент расчёта. insured_category = fields.Char(string='Код категории') payments = fields.Monetary(string='Выплаты (всего)', currency_field='currency_id') non_taxable = fields.Monetary(string='Необлагаемые', currency_field='currency_id') base = fields.Monetary(string='База', currency_field='currency_id') base_within_limit = fields.Monetary( string='База в пределах лимита ОПС', currency_field='currency_id') base_over_limit = fields.Monetary( string='База сверх лимита ОПС', currency_field='currency_id') ops = fields.Monetary(string='Взносы ОПС', currency_field='currency_id') ops_within = fields.Monetary(string='ОПС в пределах', currency_field='currency_id') ops_over = fields.Monetary(string='ОПС сверх', currency_field='currency_id') oms = fields.Monetary(string='Взносы ОМС', currency_field='currency_id') vnim = fields.Monetary(string='Взносы ВНиМ', currency_field='currency_id') _sql_uniq = models.Constraint( 'unique(company_id, employee_id, year, month)', 'Итог РСВ уникален по сотруднику и месяцу.') @api.depends('company_id') def _compute_currency_id(self): for rec in self: rec.currency_id = rec.company_id.currency_id # ------------------------------------------------------------------ @api.model def recompute_period(self, date_from, date_to, company): """Пересобрать помесячные итоги взносов за период из листков. Идемпотентно: удаляет итоги месяцев периода и заново агрегирует проведённые расчётные листки (state='done') по сотруднику и месяцу. """ Slip = self.env['hr.payslip'].sudo() # Удаляем прежние записи месяцев, попадающих в период. self.search([ ('company_id', '=', company.id), ('year', '>=', date_from.year), ('year', '<=', date_to.year), ]).filtered(lambda r: date_from <= fields.Date.to_date( '%04d-%02d-01' % (r.year, r.month)) <= date_to).unlink() slips = Slip.search([ ('company_id', '=', company.id), ('state', '=', 'done'), ('date_from', '>=', date_from), ('date_from', '<=', date_to), ]) acc = {} for slip in slips: key = (slip.employee_id.id, slip.date_from.year, slip.date_from.month) bd = slip.rsbu_insurance_breakdown() data = acc.setdefault(key, { 'payments': 0.0, 'non_taxable': 0.0, 'base': 0.0, 'base_within_limit': 0.0, 'base_over_limit': 0.0, 'ops': 0.0, 'ops_within': 0.0, 'ops_over': 0.0, 'oms': 0.0, 'vnim': 0.0, 'category': slip.employee_id.rsbu_insured_category or 'НР', }) gross = slip._rsbu_insurance_gross() data['payments'] += gross data['non_taxable'] += max(0.0, gross - bd['base']) data['base'] += bd['base'] data['base_within_limit'] += bd['base_within_limit'] data['base_over_limit'] += bd['base_over_limit'] data['ops'] += bd['ops'] data['ops_within'] += bd['ops_within'] data['ops_over'] += bd['ops_over'] data['oms'] += bd['oms'] data['vnim'] += bd['vnim'] vals_list = [] for (emp_id, year, month), d in acc.items(): vals_list.append({ 'company_id': company.id, 'employee_id': emp_id, 'year': year, 'month': month, 'insured_category': d['category'], 'payments': round(d['payments'], 2), 'non_taxable': round(d['non_taxable'], 2), 'base': round(d['base'], 2), 'base_within_limit': round(d['base_within_limit'], 2), 'base_over_limit': round(d['base_over_limit'], 2), 'ops': round(d['ops'], 2), 'ops_within': round(d['ops_within'], 2), 'ops_over': round(d['ops_over'], 2), 'oms': round(d['oms'], 2), 'vnim': round(d['vnim'], 2), }) return self.create(vals_list) if vals_list else self.browse() # ------------------------------------------------------------------ # Выборки для РСВ # ------------------------------------------------------------------ @staticmethod def _quarter_months(quarter): """Три календарных месяца квартала (1..4) → (m1, m2, m3).""" start = (quarter - 1) * 3 + 1 return (start, start + 1, start + 2) @api.model def records_for(self, company, year, month_from, month_to): """Записи итогов за интервал месяцев года (включительно).""" return self.search([ ('company_id', '=', company.id), ('year', '=', year), ('month', '>=', month_from), ('month', '<=', month_to), ]) @api.model def ytd_records(self, company, year, up_to_quarter): """Записи нарастающим итогом с начала года по конец квартала.""" _, _, last_month = self._quarter_months(up_to_quarter) return self.records_for(company, year, 1, last_month)