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,258 @@
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestRsbuPayroll(TransactionCase):
"""Тесты кадрово-расчётного блока РСБУ (Этап 7)."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company = cls.env.company
cls.company.write({
'rsbu_ndfl_rate': 13.0,
'rsbu_insurance_rate': 30.0,
'rsbu_injury_rate': 0.2,
})
cls.struct = cls.env.ref('rsbu_payroll.rsbu_structure_ru')
def _acc(self, code):
return self.env['rsbu.account'].search([
('code', '=', code),
('company_id', '=', self.company.id)], limit=1)
def _employee(self, name, wage):
return self.env['hr.employee'].create({
'name': name,
'company_id': self.company.id,
'date_version': '2026-04-01',
'contract_date_start': '2026-04-01',
'struct_id': self.struct.id,
'wage': wage,
})
def _payslip(self, employee, run=None):
vals = {
'name': 'ЗП %s' % employee.name,
'employee_id': employee.id,
'struct_id': self.struct.id,
'date_from': '2026-04-01',
'date_to': '2026-04-30',
}
if run:
vals['payslip_run_id'] = run.id
slip = self.env['hr.payslip'].create(vals)
slip.compute_sheet()
return slip
# ------------------------------------------------------------------
def test_01_compute_amounts(self):
"""Расчёт: оклад, НДФЛ 13%, взносы 30%, травматизм 0.2%, NET."""
slip = self._payslip(self._employee('Расчёт', 60000.0))
by_code = {l.code: l.total for l in slip.line_ids}
self.assertAlmostEqual(by_code['GROSS'], 60000.0, 2)
self.assertAlmostEqual(by_code['NDFL'], -7800.0, 2)
self.assertAlmostEqual(by_code['INSURANCE'], 18000.0, 2)
self.assertAlmostEqual(by_code['INJURY'], 120.0, 2)
self.assertAlmostEqual(by_code['NET'], 52200.0, 2)
def test_02_posting_creates_balanced_move(self):
"""Проведение payslip → сбалансированная РСБУ-проводка с employee."""
emp = self._employee('Проводка', 60000.0)
slip = self._payslip(emp)
slip.action_payslip_done()
move = slip.rsbu_move_id
self.assertTrue(move, 'РСБУ-проводка не создана')
self.assertEqual(move.state, 'posted')
self.assertAlmostEqual(move.total_debit, move.total_credit, 2)
# Все строки несут субконто сотрудника.
self.assertTrue(all(l.employee_id == emp for l in move.line_ids))
# Направление НДФЛ: Дт 70 / Кт 68.НДФЛ.
ndfl = move.line_ids.filtered(lambda l: l.account_id.code == '68.НДФЛ')
self.assertTrue(ndfl and ndfl.credit > 0 and not ndfl.debit)
acc70_debit = move.line_ids.filtered(
lambda l: l.account_id.code == '70' and l.debit > 0)
self.assertAlmostEqual(sum(acc70_debit.mapped('debit')), 7800.0, 2)
def test_03_idempotent_posting(self):
"""Повторное проведение не создаёт дубль проводки."""
slip = self._payslip(self._employee('Идемпотент', 50000.0))
slip.action_payslip_done()
move_id = slip.rsbu_move_id.id
slip._rsbu_generate()
self.assertEqual(slip.rsbu_move_id.id, move_id)
def test_04_draft_removes_move(self):
"""Возврат расчётного листка в черновик удаляет РСБУ-проводку."""
slip = self._payslip(self._employee('Отмена', 50000.0))
slip.action_payslip_done()
self.assertTrue(slip.rsbu_move_id)
slip.action_payslip_draft()
self.assertFalse(slip.rsbu_move_id)
def test_05_payroll_register_payout(self):
"""Реестр: кнопка выплаты → Дт70-Кт51 по сотрудникам + комиссия."""
bank = self.env['res.partner'].create({'name': 'Т-Банк', 'is_company': True})
run = self.env['hr.payslip.run'].create({
'name': 'Реестр тест',
'company_id': self.company.id,
'payment_bank_partner_id': bank.id,
'payment_date': '2026-05-05',
'bank_fee_amount': 300.0,
})
e1 = self._employee('Иванов', 60000.0)
e2 = self._employee('Сидоров', 40000.0)
s1 = self._payslip(e1, run=run)
s2 = self._payslip(e2, run=run)
# OCA payroll: action_payslip_done рассчитан на singleton — проводим
# каждый расчётный листок отдельно (как в UI).
s1.action_payslip_done()
s2.action_payslip_done()
self.assertAlmostEqual(run.total_net, 87000.0, 2)
run.action_rsbu_pay()
move = run.rsbu_payout_move_id
self.assertTrue(move)
self.assertEqual(run.payout_state, 'paid')
self.assertAlmostEqual(move.total_debit, move.total_credit, 2)
# Кт 51 = Σ NET + комиссия.
acc51_credit = move.line_ids.filtered(
lambda l: l.account_id.code == '51').credit
self.assertAlmostEqual(acc51_credit, 87300.0, 2)
# Дт 70 по каждому сотруднику.
emp70 = move.line_ids.filtered(
lambda l: l.account_id.code == '70' and l.debit > 0)
self.assertEqual(len(emp70), 2)
self.assertEqual(set(emp70.mapped('employee_id')), {e1, e2})
# Идемпотентность.
mid = run.rsbu_payout_move_id.id
run.action_rsbu_pay()
self.assertEqual(run.rsbu_payout_move_id.id, mid)
# Отмена.
run.action_rsbu_pay_cancel()
self.assertFalse(run.rsbu_payout_move_id)
self.assertEqual(run.payout_state, 'to_pay')
# ------------------------------------------------------------------
# НДФЛ: прогрессия, вычеты, нерезидент
# ------------------------------------------------------------------
def test_07_ndfl_progressive_scale(self):
"""Прогрессивная шкала: пример из НК (база 6М → 882 000)."""
P = self.env['hr.payslip']
self.assertAlmostEqual(P._ndfl_progressive_tax(2_400_000), 312000.0, 2)
self.assertAlmostEqual(P._ndfl_progressive_tax(5_000_000), 702000.0, 2)
self.assertAlmostEqual(P._ndfl_progressive_tax(6_000_000), 882000.0, 2)
def test_08_ndfl_ytd_threshold_crossing(self):
"""НДФЛ нарастающим итогом: переход порога 2.4М между месяцами.
Зарплата 2 000 000/мес. Месяц 1: база 2М < 2.4М → 13% = 260 000.
Месяц 2: YTD 4М, налог YTD = 2.4М×13% + 1.6М×15% = 312000+240000
= 552 000; за месяц = 552 000 − 260 000 = 292 000.
"""
emp = self._employee('Крупный', 2_000_000.0)
s1 = self._payslip(emp)
self.assertAlmostEqual(
{l.code: l.total for l in s1.line_ids}['NDFL'], -260000.0, 2)
s1.action_payslip_done()
# Месяц 2 (май).
s2 = self.env['hr.payslip'].create({
'name': 'ЗП2', 'employee_id': emp.id, 'struct_id': self.struct.id,
'date_from': '2026-05-01', 'date_to': '2026-05-31'})
s2.compute_sheet()
self.assertAlmostEqual(
{l.code: l.total for l in s2.line_ids}['NDFL'], -292000.0, 2)
def test_09_ndfl_child_deduction(self):
"""Детский вычет уменьшает базу НДФЛ (1 ребёнок = 1400 ₽/мес)."""
emp = self._employee('Родитель', 60000.0)
self.env['rsbu.employee.child'].create({
'employee_id': emp.id, 'name': 'Ребёнок', 'order': 1,
'birthdate': '2015-06-01'})
slip = self._payslip(emp)
# База 60000 − 1400 = 58600 × 13% = 7618.
self.assertAlmostEqual(
{l.code: l.total for l in slip.line_ids}['NDFL'], -7618.0, 2)
def test_10_ndfl_nonresident_flat_30(self):
"""Обычный нерезидент: 30% на доход без вычетов и прогрессии."""
emp = self._employee('Нерезидент', 100000.0)
emp.rsbu_taxpayer_status = '2'
slip = self._payslip(emp)
self.assertAlmostEqual(
{l.code: l.total for l in slip.line_ids}['NDFL'], -30000.0, 2)
def test_11_insurance_breakdown(self):
"""Разбивка единого тарифа на ОПС/ОМС/ВНиМ (22/5.1/2.9 из 30%)."""
slip = self._payslip(self._employee('Взносы', 100000.0))
b = slip.rsbu_insurance_breakdown(base=100000.0)
self.assertAlmostEqual(b['ops'], 22000.0, 2)
self.assertAlmostEqual(b['oms'], 5100.0, 2)
self.assertAlmostEqual(b['vnim'], 2900.0, 2)
self.assertAlmostEqual(b['total'], 30000.0, 2)
def test_12_insurance_msp_reduced(self):
"""МСП: выплата сверх 1.5 МРОТ по льготному тарифу 15%.
МРОТ 20000, порог 1.5×20000=30000. База 100000: 30000 по единому
30% = 9000; 70000 по МСП 15% = 10500; итого 19500.
"""
self.company.write({
'rsbu_msp_enabled': True,
'rsbu_msp_rate': 15.0,
'rsbu_min_wage': 20000.0,
})
slip = self._payslip(self._employee('МСП', 100000.0))
b = slip.rsbu_insurance_breakdown(base=100000.0)
self.assertAlmostEqual(b['base_regular'], 30000.0, 2)
self.assertAlmostEqual(b['base_msp'], 70000.0, 2)
self.assertAlmostEqual(b['total'], 19500.0, 2)
def test_13_insurance_regression_base_limit(self):
"""Регрессия: база сверх годовой предельной базы ОПС — отдельно.
Годовая предельная база 150 000. Первый листок база 100 000 —
весь в пределах (YTD до него = 0). Второй листок база 100 000 —
YTD уже 100 000, до лимита осталось 50 000: 50 000 в пределах,
50 000 сверх.
"""
self.company.write({'rsbu_insurance_base_limit': 150000.0})
emp = self._employee('Регресс', 100000.0)
s1 = self._payslip(emp)
s1.action_payslip_done()
b1 = s1.rsbu_insurance_breakdown(base=100000.0)
self.assertAlmostEqual(b1['base_within_limit'], 100000.0, 2)
self.assertAlmostEqual(b1['base_over_limit'], 0.0, 2)
# Второй месяц — YTD база 100 000, лимит 150 000.
s2 = self.env['hr.payslip'].create({
'name': 'ЗП2', 'employee_id': emp.id, 'struct_id': self.struct.id,
'date_from': '2026-05-01', 'date_to': '2026-05-31'})
s2.compute_sheet()
b2 = s2.rsbu_insurance_breakdown(base=100000.0)
self.assertAlmostEqual(b2['base_within_limit'], 50000.0, 2)
self.assertAlmostEqual(b2['base_over_limit'], 50000.0, 2)
def test_06_kudir_recognizes_insurance_payment(self):
"""Уплата взносов (Дт 69 — Кт 51) попадает в раздел IV КУДиР."""
acc_69et = self._acc('69.ЕТ')
acc_51 = self._acc('51')
move = self.env['rsbu.move'].create({
'date': '2026-05-10',
'company_id': self.company.id,
'usn_recognition': 'expense',
'line_ids': [
(0, 0, {'account_id': acc_69et.id, 'debit': 18000.0}),
(0, 0, {'account_id': acc_51.id, 'credit': 18000.0}),
],
})
move.action_post()
self.env['rsbu.kudir.line'].recompute_period(
'2026-04-01', '2026-06-30', self.company)
lines = self.env['rsbu.kudir.line'].search([
('company_id', '=', self.company.id),
('section', '=', 'section_4'),
('move_id', '=', move.id)])
self.assertTrue(lines, 'Уплата взносов не попала в раздел IV КУДиР')
self.assertAlmostEqual(sum(lines.mapped('expense_amount')), 18000.0, 2)