Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
# -*- 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),
])