# -*- coding: utf-8 -*- """Регистр помесячных итогов НДФЛ по сотрудникам (для 6-НДФЛ). Аналог rsbu.rsv.summary: агрегирует проведённые расчётные листки по (сотрудник × месяц начисления × ставка), вычисляя начисленный доход, вычеты, базу, исчисленный и удержанный НДФЛ. Дополнительно фиксирует дату удержания (= дату выплаты) и номер срока перечисления внутри квартала (1..6), что требуется для раздела 1 формы 6-НДФЛ (КНД 1151100, формат 5.05): НДФЛ разбивается по шести срокам квартала (каждый месяц — два периода: дни 1–22 и 23–конец). """ from odoo import api, fields, models class RsbuNdflSummary(models.Model): _name = 'rsbu.ndfl.summary' _description = 'РСБУ — Итоги НДФЛ по сотрудникам (6-НДФЛ)' _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) rate = fields.Integer(string='Ставка НДФЛ, %', required=True, index=True) currency_id = fields.Many2one( 'res.currency', string='Валюта', compute='_compute_currency_id', store=True) 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') # Удержание: дата удержания (= дата выплаты) и срок квартала (1..6). withholding_date = fields.Date(string='Дата удержания') withholding_month = fields.Integer( string='Месяц удержания', help='Календарный месяц даты удержания (для разбивки по срокам).') deadline_term = fields.Integer( string='Срок квартала (1..6)', help='Номер срока перечисления внутри квартала удержания: по два ' 'на месяц (дни 1–22 → нечётный, 23–конец → чётный).') @api.depends('company_id') def _compute_currency_id(self): for rec in self: rec.currency_id = rec.company_id.currency_id # ------------------------------------------------------------------ @staticmethod def _deadline_term(wh_date): """Номер срока перечисления внутри КВАРТАЛА (1..6) по дате удержания. Квартал делится на 6 сроков: по два на каждый месяц квартала — дни 1–22 (первый срок месяца) и 23–конец (второй срок месяца). Для месяца-в-квартале k (1..3): срок = (k-1)*2 + (1 если день<=22 иначе 2). """ month_in_quarter = (wh_date.month - 1) % 3 + 1 half = 1 if wh_date.day <= 22 else 2 return (month_in_quarter - 1) * 2 + half @api.model def recompute_period(self, date_from, date_to, company): """Пересобрать помесячные итоги НДФЛ за период из листков. Идемпотентно: удаляет итоги месяцев периода и заново агрегирует проведённые листки (state='done') по сотруднику, месяцу начисления и ставке. Срок удержания берётся по дате удержания листка. """ 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 = self.env['hr.payslip'].sudo().search([ ('company_id', '=', company.id), ('state', '=', 'done'), ('date_from', '>=', date_from), ('date_from', '<=', date_to), ]) acc = {} for slip in slips: v = slip._ndfl_report_values() if not (v['gross'] or v['withheld']): continue key = (slip.employee_id.id, slip.date_from.year, slip.date_from.month, v['rate']) wh = v['withholding_date'] data = acc.setdefault(key, { 'accrued': 0.0, 'deduction': 0.0, 'base': 0.0, 'computed': 0.0, 'withheld': 0.0, 'wh_date': wh, 'wh_month': wh.month if wh else 0, 'term': self._deadline_term(wh) if wh else 0, }) data['accrued'] += v['gross'] data['deduction'] += v['deduction'] data['base'] += v['base'] data['computed'] += v['computed'] data['withheld'] += v['withheld'] vals = [] for (emp, y, m, rate), d in acc.items(): vals.append({ 'company_id': company.id, 'employee_id': emp, 'year': y, 'month': m, 'rate': rate, 'accrued': round(d['accrued'], 2), 'deduction': round(d['deduction'], 2), 'base': round(d['base'], 2), 'computed_tax': round(d['computed'], 2), 'withheld_tax': round(d['withheld'], 2), 'withholding_date': d['wh_date'], 'withholding_month': d['wh_month'], 'deadline_term': d['term'], }) return self.create(vals) if vals else self.browse() # ------------------------------------------------------------------ @staticmethod def _quarter_months(quarter): start = (quarter - 1) * 3 + 1 return (start, start + 1, start + 2) @api.model def ytd_records(self, company, year, up_to_quarter): """Записи нарастающим итогом с начала года по конец квартала.""" _, _, last = self._quarter_months(up_to_quarter) return self.search([ ('company_id', '=', company.id), ('year', '=', year), ('month', '>=', 1), ('month', '<=', last), ])