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,270 @@
# -*- coding: utf-8 -*-
"""Приёмочные тесты рефакторинга «Документ реализации» (Stage 6).
Воплощают критерии владельца (вариант 1 из эталона 1С): документ реализации
создаётся для ЛЮБОЙ отгрузки; при наличии НДС хотя бы в одной строке он
порождает подчинённый отгрузочный счёт-фактуру; из реализации печатается и
выгружается УПД (Функция СЧФДОП при наличии СФ, иначе ДОП); авансовый СФ
по-прежнему рождается из платежа; книга продаж не зависит от наличия СФ.
Эти тесты требуют и rsbu_vat (модель rsbu.realization, книги), и
rsbu_fns_reporting (кнопка action_create_realization на инвойсе, XML/DOCX
на реализации) — обе части устанавливаются в прогоне свиты.
"""
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestRealizationAcceptance(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company = cls.env.company
cls.company.rsbu_vat_mode = 'vat_5'
# Реквизиты для прохождения _upd_validate (XML/XSD).
cls.company.partner_id.write({
'inn': '7704567896', 'kpp': '770401001',
'city': 'Москва', 'street': 'ул. Тестовая, 1'})
cls.user = cls.env['res.users'].search([], limit=1)
cls.company.rsbu_signatory_id = cls.user.id
cls.user.partner_id.function = 'Директор'
cls.company.rsbu_edo_operator_code = '2BM'
cls.company.rsbu_edo_participant_id = '2BM-SENDER-1'
cls.buyer = cls.env['res.partner'].create({
'name': 'ООО Покупатель', 'inn': '7730179829',
'city': 'Москва', 'street': 'ул. Клиент, 2',
'rsbu_edo_participant_id': '2BM-BUYER-1'})
cls.product = cls.env['product.product'].create({
'name': 'Услуга А', 'type': 'service'})
cls.product.uom_id.okei = '796'
cls.journal_bank = cls.env['account.journal'].search(
[('type', '=', 'bank'), ('company_id', '=', cls.company.id)],
limit=1)
# -- helpers --------------------------------------------------------
def _acc(self, code):
return self.env['rsbu.account'].search([
('code', '=', code),
('company_id', '=', self.company.id)], limit=1)
def _tax(self, amount=5.0):
name = 'НДС %g%%' % amount
tax = self.env['account.tax'].search([
('name', '=', name), ('type_tax_use', '=', 'sale'),
('company_id', '=', self.company.id)], limit=1)
if not tax:
tax = self.env['account.tax'].create({
'name': name, 'amount': amount, 'amount_type': 'percent',
'type_tax_use': 'sale', 'company_id': self.company.id})
return tax
def _shipment_matrix(self):
tmpl = self.env['rsbu.operation.template'].create({
'code': 'SHIP_UPD_A', 'name': 'Отгрузка',
'resolver_key': 'shipment_upd', 'usn_recognition': 'income',
'role_ids': [
(0, 0, {'role': 'receivable',
'account_id': self._acc('62.01').id}),
(0, 0, {'role': 'revenue',
'account_id': self._acc('90.01').id}),
(0, 0, {'role': 'revenue_vat',
'account_id': self._acc('90.03').id}),
(0, 0, {'role': 'vat_output',
'account_id': self._acc('68.НДС').id}),
(0, 0, {'role': 'advance_received',
'account_id': self._acc('62.АВ').id}),
]})
self.env['rsbu.operation.matrix'].create({
'name': 'Исходящий инвойс → отгрузка (acceptance)',
'doc_kind': 'out_invoice', 'template_id': tmpl.id})
return tmpl
def _advance_matrix(self):
tmpl = self.env['rsbu.operation.template'].create({
'code': 'ADV_A', 'name': 'Аванс',
'resolver_key': 'customer_payment', 'usn_recognition': 'income',
'role_ids': [
(0, 0, {'role': 'bank', 'account_id': self._acc('51').id}),
(0, 0, {'role': 'receivable',
'account_id': self._acc('62.01').id}),
(0, 0, {'role': 'advance_received',
'account_id': self._acc('62.АВ').id}),
(0, 0, {'role': 'vat_advance',
'account_id': self._acc('76.АВ').id}),
(0, 0, {'role': 'vat_output',
'account_id': self._acc('68.НДС').id}),
]})
self.env['rsbu.operation.matrix'].create({
'name': 'Входящий → аванс (acceptance)',
'doc_kind': 'payment_inbound', 'template_id': tmpl.id})
return tmpl
def _post_invoice(self, price=100000.0, qty=1, tax=None):
line = {'name': 'Услуга А', 'product_id': self.product.id,
'quantity': qty, 'price_unit': price}
line['tax_ids'] = [(6, 0, tax.ids)] if tax else [(5, 0, 0)]
move = self.env['account.move'].create({
'move_type': 'out_invoice', 'partner_id': self.buyer.id,
'company_id': self.company.id,
'invoice_line_ids': [(0, 0, line)]})
move.action_post()
return move
# -- 1. Реализация с НДС порождает отгрузочный СФ -------------------
def test_realization_with_vat_creates_sf(self):
"""Инвойс с НДС 5% → реализация + отгрузочный СФ; vat_rate = факт."""
self._shipment_matrix()
move = self._post_invoice(price=100000.0, tax=self._tax(5.0))
move.action_create_realization()
real = move.rsbu_realization_id
self.assertTrue(real)
self.assertEqual(real.state, 'confirmed')
sf = real.invoice_id
self.assertTrue(sf, 'При наличии НДС должен появиться отгрузочный СФ')
self.assertEqual(sf.invoice_type, 'shipment')
# Ставка СФ — по фактическому налогу строки (5%), не форсированная.
self.assertEqual(sf.vat_rate, 'vat_5')
self.assertEqual(real.upd_function, '1')
# -- 2. Реализация без НДС: СФ нет, УПД Функция=ДОП ------------------
def test_realization_without_vat_no_sf(self):
"""Инвойс без НДС → только реализация; XML Функция=ДОП, валиден."""
self._shipment_matrix()
move = self._post_invoice(price=100000.0, tax=None)
move.action_create_realization()
real = move.rsbu_realization_id
self.assertTrue(real)
self.assertFalse(real.invoice_id, 'Без НДС отгрузочный СФ не создаётся')
self.assertEqual(real.upd_function, '2')
xml_bytes = real._upd_build_xml(filename='T')
ok, errors = real._upd_validate_xml(xml_bytes)
self.assertTrue(ok, 'ДОП-УПД не прошёл XSD: %s'
% '; '.join(errors[:5]))
xml = xml_bytes.decode('cp1251')
self.assertIn('Функция="ДОП"', xml)
self.assertIn('<БезНДС>без НДС</БезНДС>', xml)
# -- 3. XSD валиден для обоих случаев («1» и «2») -------------------
def test_upd_xml_valid_both_cases(self):
"""УПД валиден по XSD и для СЧФДОП (с НДС), и для ДОП (без НДС)."""
self._shipment_matrix()
move_vat = self._post_invoice(price=100000.0, tax=self._tax(5.0))
move_vat.action_create_realization()
real_vat = move_vat.rsbu_realization_id
xml_vat = real_vat._upd_build_xml(filename='T')
ok_vat, err_vat = real_vat._upd_validate_xml(xml_vat)
self.assertTrue(ok_vat, 'СЧФДОП не прошёл XSD: %s'
% '; '.join(err_vat[:5]))
self.assertIn('Функция="СЧФДОП"', xml_vat.decode('cp1251'))
move_nov = self._post_invoice(price=80000.0, tax=None)
move_nov.action_create_realization()
real_nov = move_nov.rsbu_realization_id
xml_nov = real_nov._upd_build_xml(filename='T')
ok_nov, err_nov = real_nov._upd_validate_xml(xml_nov)
self.assertTrue(ok_nov, 'ДОП не прошёл XSD: %s'
% '; '.join(err_nov[:5]))
self.assertIn('Функция="ДОП"', xml_nov.decode('cp1251'))
# -- 4. Авансовый СФ рождается из платежа, без реализации -----------
def test_advance_sf_still_from_payment(self):
"""Авансовый СФ создаётся из платежа, realization_id пуст."""
self._advance_matrix()
# Облагаемая сделка того же контрагента — условие авто-создания
# авансового СФ (_rsbu_payment_is_taxable).
self.env['account.move'].create({
'move_type': 'out_invoice', 'partner_id': self.buyer.id,
'company_id': self.company.id,
'invoice_line_ids': [(0, 0, {
'name': 'Услуга А', 'product_id': self.product.id,
'quantity': 1, 'price_unit': 100000.0,
'tax_ids': [(6, 0, self._tax(5.0).ids)]})]})
pay = self.env['account.payment'].create({
'payment_type': 'inbound', 'partner_type': 'customer',
'partner_id': self.buyer.id, 'amount': 105000.0,
'rsbu_vat_amount': 5000.0,
'journal_id': self.journal_bank.id})
pay.action_post()
advance = self.env['rsbu.invoice'].search([
('partner_id', '=', self.buyer.id),
('invoice_type', '=', 'advance'),
('company_id', '=', self.company.id)], limit=1)
self.assertTrue(advance, 'Авансовый СФ должен создаться из платежа')
self.assertEqual(advance.state, 'confirmed')
self.assertFalse(advance.realization_id,
'У авансового СФ нет документа реализации')
# -- 5. Книга продаж не зависит от наличия отгрузочного СФ ----------
def test_sales_book_independent_of_sf(self):
"""Создание/удаление отгрузочного СФ не меняет число строк книги.
Книга продаж строится на РСБУ-проводках реализации (резолвер), а не
на rsbu.invoice. Поэтому её наполнение не зависит от того, выписан ли
отгрузочный СФ.
"""
self._shipment_matrix()
move = self._post_invoice(price=100000.0, tax=self._tax(5.0))
rsbu_move = move.rsbu_move_id
self.assertTrue(rsbu_move, 'Проводка реализации должна сформироваться')
BookLine = self.env['rsbu.vat.book.line']
BookLine.recompute_period(rsbu_move.date, rsbu_move.date, self.company)
before = BookLine.search_count([
('company_id', '=', self.company.id), ('book', '=', 'sales')])
move.action_create_realization()
real = move.rsbu_realization_id
sf = real.invoice_id
self.assertTrue(sf)
BookLine.recompute_period(rsbu_move.date, rsbu_move.date, self.company)
after_create = BookLine.search_count([
('company_id', '=', self.company.id), ('book', '=', 'sales')])
self.assertEqual(after_create, before,
'Книга продаж не должна зависеть от создания СФ')
sf.unlink()
real.unlink()
BookLine.recompute_period(rsbu_move.date, rsbu_move.date, self.company)
after_delete = BookLine.search_count([
('company_id', '=', self.company.id), ('book', '=', 'sales')])
self.assertEqual(after_delete, before,
'Книга продаж не должна зависеть от удаления СФ')
# -- 6. Отображение ставки vat_rate по фактическим налогам ----------
def test_vat_rate_mapping(self):
"""_upd_vat_rate: 20/10/7/5/0 → корректный vat_rate; mixed → max."""
Realization = self.env['rsbu.realization']
cases = [
(20.0, 'vat_20'),
(10.0, 'vat_10'),
(7.0, 'vat_7'),
(5.0, 'vat_5'),
(0.0, 'vat_exempt'),
]
for pct, expected in cases:
move = self._post_invoice(price=10000.0, tax=self._tax(pct))
self.assertEqual(Realization._upd_vat_rate(move), expected,
'Ставка %g%% → %s' % (pct, expected))
# Строка без налогов → vat_exempt.
move_none = self._post_invoice(price=10000.0, tax=None)
self.assertEqual(Realization._upd_vat_rate(move_none), 'vat_exempt')
# Смешанный инвойс (0% + 20%) → максимальная ненулевая = vat_20.
move_mix = self.env['account.move'].create({
'move_type': 'out_invoice', 'partner_id': self.buyer.id,
'company_id': self.company.id,
'invoice_line_ids': [
(0, 0, {'name': 'A', 'product_id': self.product.id,
'quantity': 1, 'price_unit': 1000.0,
'tax_ids': [(6, 0, self._tax(0.0).ids)]}),
(0, 0, {'name': 'B', 'product_id': self.product.id,
'quantity': 1, 'price_unit': 1000.0,
'tax_ids': [(6, 0, self._tax(20.0).ids)]}),
]})
self.assertEqual(Realization._upd_vat_rate(move_mix), 'vat_20',
'Смешанные ставки → максимальная ненулевая')
# _has_vat: с ненулевым налогом True, без налога False.
self.assertTrue(Realization._has_vat(move_mix))
self.assertFalse(Realization._has_vat(move_none))