146 lines
7.2 KiB
Python
146 lines
7.2 KiB
Python
# -*- 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),
|
||
])
|