Files
public/rsbu_payroll/models/rsbu_ndfl_summary.py

146 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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),
])