from odoo.tests import TransactionCase, tagged @tagged('post_install', '-at_install') class TestUsnDeclaration(TransactionCase): """Декларация УСН «Доходы»: расчёт разделов 1.1/2.1.1 и XML.""" @classmethod def setUpClass(cls): super().setUpClass() cls.company = cls.env.company cls.company.write({ 'rsbu_usn_rate': 6.0, 'rsbu_tax_office_code': '7704', 'rsbu_oktmo': '45301000', }) cls.company.partner_id.write({'inn': '7704567896', 'kpp': '770401001'}) cls.Decl = cls.env['rsbu.usn.declaration'] cls.Summary = cls.env['rsbu.kudir.summary'] def _summary(self, quarter, income, insurance=0.0): """Создать запись итогов КУДиР за квартал года.""" return self.Summary.create({ 'company_id': self.company.id, 'period_key': '2026-Q%s' % quarter, 'income': income, 'expense': 0.0, 'insurance': insurance, }) def test_01_progression_of_advances(self): """Авансы по срокам считаются нарастающим итогом (без взносов). Квартальные доходы (summary хранит за квартал, ytd суммирует): Q1 1 000 000, Q2 500 000, Q3 500 000, Q4 1 000 000. Нарастающим: Q1 1М, H1 1.5М, 9м 2М, год 3М. Ставка 6%. Исчислено: 60000/90000/120000/180000. Взносов нет. Авансы: 020=60000, 040=30000, 070=30000, 100=60000. """ self._summary(1, 1_000_000) self._summary(2, 500_000) self._summary(3, 500_000) self._summary(4, 1_000_000) dec = self.Decl.create({ 'year': 2026, 'company_id': self.company.id, 'taxpayer_sign': '1', 'rate': 6.0}) dec.compute_from_summary() self.assertAlmostEqual(dec.tax_q1, 60000.0, 2) self.assertAlmostEqual(dec.tax_year, 180000.0, 2) self.assertAlmostEqual(dec.adv_q1, 60000.0, 2) # стр.020 self.assertAlmostEqual(dec.adv_h1, 30000.0, 2) # стр.040 self.assertAlmostEqual(dec.adv_9m, 30000.0, 2) # стр.070 self.assertAlmostEqual(dec.tax_due_year, 60000.0, 2) # стр.100 def test_02_reduction_limited_50pct(self): """Вычет взносов для признака «1» ограничен 50% исчисленного.""" self._summary(1, 1_000_000, insurance=40000.0) dec = self.Decl.create({ 'year': 2026, 'company_id': self.company.id, 'taxpayer_sign': '1', 'rate': 6.0}) dec.compute_from_summary() # Исчислено 60000; взносы 40000, но лимит 50% = 30000. self.assertAlmostEqual(dec.tax_q1, 60000.0, 2) self.assertAlmostEqual(dec.ded_q1, 30000.0, 2) self.assertAlmostEqual(dec.adv_q1, 30000.0, 2) # 60000 − 30000 def test_03_advance_reduce_when_income_drops(self): """Аванс к уменьшению (050), если исчисленное за полугодие < аванса Q1. Q1 доход 1М (аванс 60000). Полугодие доход тот же 1М (нет прироста) → (131−141)−020 = 60000 − 60000 = 0; при возврате дохода строка 050. Здесь проверяем неотрицательность и отсутствие двойного счёта. """ self._summary(1, 1_000_000) self._summary(2, 0.0) # прироста во II квартале нет → H1 = Q1 dec = self.Decl.create({ 'year': 2026, 'company_id': self.company.id, 'taxpayer_sign': '1', 'rate': 6.0}) dec.compute_from_summary() self.assertAlmostEqual(dec.adv_q1, 60000.0, 2) self.assertAlmostEqual(dec.adv_h1, 0.0, 2) self.assertAlmostEqual(dec.adv_h1_reduce, 0.0, 2) def test_04_xml_valid_against_xsd(self): """Сгенерированный XML проходит проверку по XSD-схеме ФНС.""" self._summary(1, 1_000_000) self._summary(2, 500_000) self._summary(3, 500_000) self._summary(4, 1_000_000) dec = self.Decl.create({ 'year': 2026, 'company_id': self.company.id, 'taxpayer_sign': '1', 'rate': 6.0}) dec.compute_from_summary() fname = dec._usn_xml_filename() xml_bytes = dec._usn_build_xml(filename=fname) ok, errors = dec._usn_validate_xml(xml_bytes) self.assertTrue(ok, 'XML не прошёл XSD: %s' % '; '.join(errors[:5])) # Кодировка windows-1251 в декларации XML. self.assertIn(b'windows-1251', xml_bytes[:60]) # Префикс имени файла обмена — NO_USN (Порядок п.3). self.assertTrue(fname.startswith('NO_USN_')) # ИдФайл в XML совпадает с именем файла обмена. from lxml import etree root = etree.fromstring(xml_bytes) self.assertEqual(root.get('ИдФайл'), fname) def test_05_export_always_recomputes(self): """Выгрузка XML всегда пересчитывает данные (свежесть гарантирована). Даже если декларация уже в состоянии «Рассчитана», action_export_xml обязан вызвать action_compute перед формированием файла — иначе выгрузятся устаревшие суммы после изменения КУДиР. """ dec = self.Decl.create({ 'year': 2026, 'company_id': self.company.id, 'taxpayer_sign': '1', 'rate': 6.0}) dec.action_compute() self.assertEqual(dec.state, 'computed') calls = [] orig = type(dec).action_compute def _spy(self2): calls.append(1) return orig(self2) self.patch(type(dec), 'action_compute', _spy) dec.action_export_xml() self.assertTrue(calls, 'action_export_xml должен вызвать пересчёт')