# -*- coding: utf-8 -*- """ЕФС-1 подраздел 1.1 (СЗВ-ТД): автозаполнение, построение XML, XSD. Сверка с живыми образцами 1С (ref_1c/new), принятыми СФР: * ПФР_..._20260401 — ПРИЕМ (вид 1); * ПФР_..._20260803 — УВОЛЬНЕНИЕ (вид 5, с основанием увольнения). """ from xml.etree import ElementTree as ET from odoo.tests import TransactionCase, tagged NS_EFS1 = 'http://пф.рф/ЕФС-1/2024-01-01' NS_EFS = 'http://пф.рф/ВС/ЕФС/2024-01-01' NS_UT2 = 'http://пф.рф/УТ/2017-08-21' NS_AF5 = 'http://пф.рф/АФ/2018-12-07' def _q(ns, tag): return '{%s}%s' % (ns, tag) @tagged('post_install', '-at_install') class TestEfs1Szvtd(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.company = cls.env.company cls.company.write({ 'rsbu_sfr_reg_number': '060-054-063199', 'rsbu_okved': '62.01', 'rsbu_ogrn': '1245000059622', 'rsbu_signatory_title': 'ГЕНЕРАЛЬНЫЙ ДИРЕКТОР', }) cls.company.partner_id.write({ 'inn': '5050164162', 'kpp': '505001001'}) user = cls.env['res.users'].search([], limit=1) user.partner_id.write({'name': 'Коробков Сергей Васильевич'}) cls.company.rsbu_signatory_id = user.id cls.emp_hire = cls.env['hr.employee'].create({ 'name': 'Горский Алексей Алексеевич', 'company_id': cls.company.id, 'rsbu_snils': '195-652-650 11', 'rsbu_inn': '780604730641', 'rsbu_citizenship_code': '643', 'birthday': '2003-09-03'}) cls.emp_fire = cls.env['hr.employee'].create({ 'name': 'Зимина Мария Михайловна', 'company_id': cls.company.id, 'rsbu_snils': '171-732-575 70', 'rsbu_inn': '671405753608', 'rsbu_citizenship_code': '643', 'birthday': '1997-08-11'}) cls.Event = cls.env['rsbu.hr.event'] cls.Efs1 = cls.env['rsbu.efs1.szvtd'] def _hire_event(self): return self.Event.create({ 'company_id': self.company.id, 'employee_id': self.emp_hire.id, 'event_date': '2026-04-01', 'kind': '1', 'job_title': 'Младший менеджер проектов', 'employment_type': '0', 'department_name': 'Отдел разработки программного обеспечения', 'func_code': 'ДИСТ', 'okz_code': '2421.9', 'basis_name': 'Приказ', 'basis_date': '2026-04-01', 'basis_number': '1', }) def _fire_event(self): return self.Event.create({ 'company_id': self.company.id, 'employee_id': self.emp_fire.id, 'event_date': '2026-08-03', 'kind': '5', 'job_title': 'Разработчик', 'employment_type': '0', 'work_kind': 'Разработка программного обеспечения', 'department_name': 'Отдел разработки программного обеспечения', 'okz_code': '2512.1', 'dismissal_reason_code': 'п3ч1с77тк', 'dismissal_document': 'Трудовой кодекс Российской Федерации', 'dismissal_article': '77', 'dismissal_part': '1', 'dismissal_point': '3', 'dismissal_reason': 'Расторжение трудового договора по инициативе работника', 'basis_name': 'Приказ', 'basis_date': '2026-08-03', 'basis_number': '1', }) # ------------------------------------------------------------------ def test_01_autofill_period(self): """action_compute собирает draft-мероприятия за период.""" self._hire_event() self._fire_event() doc = self.Efs1.create({ 'company_id': self.company.id, 'date_from': '2026-04-01', 'date_to': '2026-08-31', 'date_fill': '2026-08-31'}) doc.action_compute() self.assertEqual(doc.state, 'computed') self.assertEqual(doc.event_count, 2) self.assertEqual(doc.employee_count, 2) def test_02_hire_xml_valid_and_structure(self): """XML приёма проходит XSD и совпадает по структуре с образцом.""" self._hire_event() doc = self.Efs1.create({ 'company_id': self.company.id, 'date_from': '2026-04-01', 'date_to': '2026-04-30', 'date_fill': '2026-04-01'}) doc.action_compute() xml_bytes = doc._efs1_build_xml(guid='311086e3-d18b-4794-a0ea-033f23c1edb2') ok, errors = doc._efs1_validate_xml(xml_bytes) self.assertTrue(ok, 'ЕФС-1 приём не прошёл XSD: %s' % '; '.join(errors[:5])) root = ET.fromstring(xml_bytes) self.assertEqual(root.tag, _q(NS_EFS1, 'ЭДПФР')) efs1 = root.find(_q(NS_EFS1, 'ЕФС-1')) self.assertIsNotNone(efs1) strah = efs1.find(_q(NS_EFS1, 'Страхователь')) self.assertEqual(strah.find(_q(NS_EFS, 'РегНомер')).text, '060-054-063199') self.assertEqual(strah.find(_q(NS_UT2, 'ИНН')).text, '5050164162') self.assertEqual(strah.find(_q(NS_EFS, 'ОГРН')).text, '1245000059622') # ЗЛ → СЗВ-ТД → ТД → Мероприятие zl = efs1.find(_q(NS_EFS1, 'СЗВ')).find(_q(NS_EFS1, 'ЗЛ')) self.assertEqual(zl.find(_q(NS_UT2, 'СНИЛС')).text, '195-652-650 11') self.assertEqual(zl.find(_q(NS_EFS1, 'СтатусЗЛ')).text, 'ГРФ') self.assertEqual(zl.find(_q(NS_EFS1, 'Гражданство')).text, '643') td = zl.find(_q(NS_EFS1, 'СЗВ-ТД')).find(_q(NS_EFS1, 'ТД')) m = td.find(_q(NS_EFS1, 'Мероприятие')) self.assertEqual(m.find(_q(NS_EFS, 'Вид')).text, '1') self.assertEqual(m.find(_q(NS_EFS, 'КодВФпоОКЗ')).text, '2421.9') self.assertEqual(m.find(_q(NS_EFS, 'КодВФ')).text, 'ДИСТ') basis = m.find(_q(NS_EFS, 'Основание')) self.assertEqual(basis.find(_q(NS_EFS, 'Номер')).text, '1') # Руководитель + СлужебнаяИнформация ruk = efs1.find(_q(NS_EFS1, 'Руководитель')) self.assertEqual(ruk.find(_q(NS_UT2, 'Должность')).text, 'ГЕНЕРАЛЬНЫЙ ДИРЕКТОР') svc = root.find(_q(NS_EFS1, 'СлужебнаяИнформация')) self.assertEqual(svc.find(_q(NS_AF5, 'GUID')).text, '311086e3-d18b-4794-a0ea-033f23c1edb2') def test_03_fire_xml_valid_and_dismissal(self): """XML увольнения проходит XSD, блок ОснованиеУвольнения заполнен.""" self._fire_event() doc = self.Efs1.create({ 'company_id': self.company.id, 'date_from': '2026-08-01', 'date_to': '2026-08-31', 'date_fill': '2026-08-03'}) doc.action_compute() xml_bytes = doc._efs1_build_xml() ok, errors = doc._efs1_validate_xml(xml_bytes) self.assertTrue(ok, 'ЕФС-1 увольнение не прошло XSD: %s' % '; '.join(errors[:5])) root = ET.fromstring(xml_bytes) m = (root.find(_q(NS_EFS1, 'ЕФС-1')) .find(_q(NS_EFS1, 'СЗВ')).find(_q(NS_EFS1, 'ЗЛ')) .find(_q(NS_EFS1, 'СЗВ-ТД')).find(_q(NS_EFS1, 'ТД')) .find(_q(NS_EFS1, 'Мероприятие'))) self.assertEqual(m.find(_q(NS_EFS, 'Вид')).text, '5') ou = m.find(_q(NS_EFS, 'ОснованиеУвольнения')) self.assertIsNotNone(ou) self.assertEqual(ou.find(_q(NS_EFS, 'КодПричиныУвольнения')).text, 'п3ч1с77тк') self.assertEqual(ou.find(_q(NS_EFS, 'Статья')).text, '77') self.assertEqual(ou.find(_q(NS_EFS, 'Часть')).text, '1') self.assertEqual(ou.find(_q(NS_EFS, 'Пункт')).text, '3') self.assertEqual(m.find(_q(NS_EFS, 'Причина')).text, 'Расторжение трудового договора по инициативе работника') def test_04_multi_zl(self): """Два сотрудника → два блока ЗЛ в одном документе.""" self._hire_event() self._fire_event() doc = self.Efs1.create({ 'company_id': self.company.id, 'date_from': '2026-04-01', 'date_to': '2026-08-31', 'date_fill': '2026-08-31'}) doc.action_compute() xml_bytes = doc._efs1_build_xml() ok, errors = doc._efs1_validate_xml(xml_bytes) self.assertTrue(ok, 'ЕФС-1 (2 ЗЛ) не прошёл XSD: %s' % '; '.join(errors[:5])) root = ET.fromstring(xml_bytes) zls = root.find(_q(NS_EFS1, 'ЕФС-1')).find( _q(NS_EFS1, 'СЗВ')).findall(_q(NS_EFS1, 'ЗЛ')) self.assertEqual(len(zls), 2) def test_05_okz_required_validation(self): """Отсутствие ОКЗ для приёма блокирует выгрузку (UserError).""" from odoo.exceptions import UserError ev = self._hire_event() ev.okz_code = False self.emp_hire.rsbu_okz_code = False doc = self.Efs1.create({ 'company_id': self.company.id, 'date_from': '2026-04-01', 'date_to': '2026-04-30', 'date_fill': '2026-04-01'}) doc.action_compute() with self.assertRaises(UserError): doc._efs1_build_xml()