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

328
rsbu_vat/tests/test_vat.py Normal file
View File

@@ -0,0 +1,328 @@
from freezegun import freeze_time
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class TestRsbuVat(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company = cls.env.company
cls.partner = cls.env['res.partner'].create({'name': 'Vat Клиент'})
cls.journal_bank = cls.env['account.journal'].search(
[('type', '=', 'bank'), ('company_id', '=', cls.company.id)],
limit=1)
cls.BookLine = cls.env['rsbu.vat.book.line']
cls.Summary = cls.env['rsbu.vat.summary']
def _acc(self, code):
return self.env['rsbu.account'].search([
('code', '=', code),
('company_id', '=', self.env.company.id)], limit=1)
def _advance_template(self):
return self.env['rsbu.operation.template'].create({
'code': 'ADV_V',
'name': 'Аванс',
'resolver_key': 'advance_received',
'usn_recognition': 'income',
'role_ids': [
(0, 0, {'role': 'bank', 'account_id': self._acc('51').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}),
],
})
def _matrix(self, template):
return self.env['rsbu.operation.matrix'].create({
'name': 'Входящий → аванс',
'doc_kind': 'payment_inbound',
'template_id': template.id,
})
def _post_advance(self, amount=1050.0, vat=50.0):
pay = self.env['account.payment'].create({
'payment_type': 'inbound',
'partner_type': 'customer',
'partner_id': self.partner.id,
'amount': amount,
'rsbu_vat_amount': vat,
'journal_id': self.journal_bank.id,
})
pay.action_post()
return pay
def test_invoice_creation_and_sequence(self):
inv = self.env['rsbu.invoice'].create({
'invoice_type': 'advance',
'partner_id': self.partner.id,
'base_amount': 1000.0,
'vat_amount': 50.0,
})
self.assertNotEqual(inv.name, '/')
self.assertEqual(inv.invoice_type, 'advance')
def test_sales_book_from_advance_vat(self):
tmpl = self._advance_template()
self._matrix(tmpl)
pay = self._post_advance(amount=1050.0, vat=50.0)
move = pay.rsbu_move_id
self.BookLine.recompute_period(move.date, move.date, self.company)
sales = self.BookLine.search([
('company_id', '=', self.company.id),
('book', '=', 'sales')])
self.assertTrue(sales)
# Авансовый НДС начислен по Кт 68.НДС = 50.
self.assertEqual(sum(sales.mapped('vat_amount')), 50.0)
def test_summary_payable(self):
tmpl = self._advance_template()
self._matrix(tmpl)
pay = self._post_advance(amount=1050.0, vat=50.0)
move = pay.rsbu_move_id
self.BookLine.recompute_period(move.date, move.date, self.company)
self.Summary.recompute_period(move.date, move.date, self.company)
summary = self.Summary.search([
('company_id', '=', self.company.id)], limit=1)
self.assertTrue(summary)
# Начислено 50, вычетов нет -> к уплате 50.
self.assertEqual(summary.vat_charged, 50.0)
self.assertEqual(summary.vat_payable, 50.0)
def test_recompute_idempotent(self):
tmpl = self._advance_template()
self._matrix(tmpl)
pay = self._post_advance()
move = pay.rsbu_move_id
self.BookLine.recompute_period(move.date, move.date, self.company)
first = self.BookLine.search_count(
[('company_id', '=', self.company.id)])
self.BookLine.recompute_period(move.date, move.date, self.company)
second = self.BookLine.search_count(
[('company_id', '=', self.company.id)])
self.assertEqual(first, second)
# ------------------------------------------------------------------
# Расчёт налога УСН (rsbu.period.close.compute_usn_tax)
# ------------------------------------------------------------------
def test_usn_tax_computation(self):
"""Налог УСН = база(доход КУДиР) × ставка, база с исключённым НДС."""
self.company.rsbu_usn_mode = 'usn_income'
self.company.rsbu_usn_rate = 6.0
tmpl = self._advance_template()
self._matrix(tmpl)
# Аванс 1050 c НДС 50 → доход КУДиР 1000 (НДС исключён).
pay = self._post_advance(amount=1050.0, vat=50.0)
# Закрытие за декабрь года платежа (охватывает весь год нараст. итогом).
year = pay.rsbu_move_id.date.year
pc = self.env['rsbu.period.close'].create({
'period_month': '%s-12-01' % year,
'company_id': self.company.id})
pc.compute_usn_tax()
# База = 1000 (доход без НДС), налог = 1000 × 6% = 60 (взносов нет).
self.assertEqual(pc.usn_tax_base, 1000.0)
self.assertEqual(pc.usn_tax_amount, 60.0)
def test_advance_invoice_autocreated(self):
"""При поступлении аванса облагаемой сделки авансовый СФ создаётся
автоматически (Дт 76.АВ — Кт 68.02)."""
self.company.rsbu_vat_mode = 'vat_5'
# Облагаемая реализация того же партнёра (инвойс источник правды).
self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner.id,
'company_id': self.company.id,
'invoice_line_ids': [(0, 0, {
'name': 'Услуги', 'quantity': 1, 'price_unit': 1000.0,
'tax_ids': [(6, 0, self._vat5_tax().ids)]})],
})
tmpl = self._advance_template()
self._matrix(tmpl)
pay = self._post_advance(amount=1050.0, vat=0.0)
invoices = self.env['rsbu.invoice'].search([
('partner_id', '=', self.partner.id),
('invoice_type', '=', 'advance'),
('company_id', '=', self.company.id)])
self.assertTrue(invoices, 'Авансовый СФ должен создаться автоматически')
self.assertEqual(invoices[0].state, 'confirmed')
def _vat5_tax(self):
tax = self.env['account.tax'].search([
('amount', '=', 5.0), ('type_tax_use', '=', 'sale'),
('company_id', '=', self.company.id)], limit=1)
if not tax:
tax = self.env['account.tax'].create({
'name': 'НДС 5%', 'amount': 5.0, 'type_tax_use': 'sale',
'company_id': self.company.id})
return tax
@tagged('post_install', '-at_install')
@freeze_time('2027-01-15')
class TestPeriodClosePl(TransactionCase):
"""Закрытие месяца: 26→90.08, 90→99, 91→99, реформация 99→84 (декабрь).
Дата заморожена на 2027-01-15: все месяцы 2026 завершены, поэтому
проверка «месяц завершён» в _check_can_close не мешает тестам.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.company = cls.env.company
# Изолируем тесты финрезультата от налога УСН (ставка 0 → УСН=0,
# проводок начисления на 99 не будет).
cls.company.rsbu_usn_rate = 0.0
cls.Account = cls.env['rsbu.account']
cls.Move = cls.env['rsbu.move']
# Догрузить типовой план (post_init_hook отрабатывает только при
# установке; в тестовой БД гарантируем наличие 90.08/90.09/91.09/84).
cls.env['rsbu.account.load.wizard'].create({
'company_id': cls.company.id}).action_load()
def _acc(self, code):
return self.Account.search([
('code', '=', code),
('company_id', '=', self.company.id)], limit=1)
def _post(self, date, lines):
"""Создать и провести rsbu.move из списка (code, debit, credit)."""
move = self.Move.create({
'date': date, 'company_id': self.company.id,
'line_ids': [(0, 0, {
'account_id': self._acc(code).id,
'debit': d, 'credit': c,
'name': code}) for code, d, c in lines],
})
move.action_post()
return move
def _close(self, month_date):
pc = self.env['rsbu.period.close'].create({
'period_month': month_date, 'company_id': self.company.id})
pc.action_close()
return pc
def _turnover(self, code, date_from, date_to):
return self._acc(code).get_turnover(
date_from=date_from, date_to=date_to,
company_id=self.company.id, include_children=False)
def test_close_expenses_26_to_9008(self):
"""Директ-костинг: 26 закрывается на 90.08, обнуляется."""
# Расходы месяца: Дт 26 — Кт 51 (60 000). Контрсчёт 51 — без субконто.
self._post('2026-02-15', [('26', 60000.0, 0.0), ('51', 0.0, 60000.0)])
self._close('2026-02-01')
# Оборот 26 за месяц: дебет 60 000 (расход) и кредит 60 000 (закрытие).
t26 = self._turnover('26', '2026-02-01', '2026-02-28')
self.assertEqual(round(t26['debit'] - t26['credit'], 2), 0.0,
'Счёт 26 должен обнулиться закрытием')
# 90.08 получил расходы по дебету.
t9008 = self._turnover('90.08', '2026-02-01', '2026-02-28')
self.assertEqual(t9008['debit'], 60000.0)
def test_close_90_profit_to_99(self):
"""Прибыль от продаж переносится на 99 (Дт 90.09 — Кт 99.01.1)."""
profit_code = '99.01.1' if self._acc('99.01.1') else '99'
# Выручка 100 000 (Кт 90.01), НДС 5000 (Дт 90.03), расходы 40 000 (26).
# Контрсчёт 51 — без обязательных субконто.
self._post('2026-03-31', [
('90.01', 0.0, 100000.0), ('51', 100000.0, 0.0)])
self._post('2026-03-31', [
('90.03', 5000.0, 0.0), ('68.НДС', 0.0, 5000.0)])
self._post('2026-03-15', [('26', 40000.0, 0.0), ('51', 0.0, 40000.0)])
self._close('2026-03-01')
# Финрезультат = 100000 − 5000 − 40000 = 55000 прибыль на 99 (кредит).
bal99 = self._acc(profit_code).get_balance(
date_to='2026-03-31', company_id=self.company.id,
include_children=True)
self.assertEqual(bal99['side'], 'credit')
self.assertEqual(round(bal99['balance'], 2), 55000.0)
# Субсчёт 90.09 обнулён (весь финрезультат ушёл на 99).
b9009 = self._acc('90.09').get_balance(
date_to='2026-03-31', company_id=self.company.id,
include_children=False)
self.assertEqual(round(b9009['balance'], 2), 0.0)
def test_reform_balance_december(self):
"""Реформация декабря: сальдо 99 переносится на 84."""
profit_code = '99.01.1' if self._acc('99.01.1') else '99'
# Прибыль декабря: выручка 80 000, расходов нет.
self._post('2026-12-31', [
('90.01', 0.0, 80000.0), ('51', 80000.0, 0.0)])
self._close('2026-12-01')
# После реформации 99 обнулён, 84 — кредитовое сальдо 80 000 (прибыль).
bal99 = self._acc('99').get_balance(
date_to='2026-12-31', company_id=self.company.id,
include_children=True)
self.assertEqual(round(bal99['balance'], 2), 0.0,
'Счёт 99 должен обнулиться реформацией')
bal84 = self._acc('84').get_balance(
date_to='2026-12-31', company_id=self.company.id)
self.assertEqual(bal84['side'], 'credit')
self.assertEqual(round(bal84['balance'], 2), 80000.0)
def test_reopen_reverts_closing(self):
"""Переоткрытие удаляет все регламентные проводки закрытия."""
self._post('2026-04-15', [('26', 30000.0, 0.0), ('51', 0.0, 30000.0)])
pc = self._close('2026-04-01')
self.assertTrue(pc.move_ids)
self.assertEqual(pc.state, 'done')
pc.action_reopen()
self.assertFalse(pc.move_ids)
self.assertEqual(pc.state, 'draft')
# 26 снова с дебетовым оборотом (закрытие откатилось).
t26 = self._turnover('26', '2026-04-01', '2026-04-30')
self.assertEqual(round(t26['debit'] - t26['credit'], 2), 30000.0)
def test_accounting_start_allows_first_month(self):
"""Месяц начала учёта закрывается свободно (предыдущий не нужен)."""
# Есть проводки и в мае, и в июне, но учёт ведётся с июня.
self.company.rsbu_accounting_start = '2026-06-01'
self._post('2026-05-20', [('26', 10000.0, 0.0), ('51', 0.0, 10000.0)])
self._post('2026-06-20', [('26', 20000.0, 0.0), ('51', 0.0, 20000.0)])
# Июнь (месяц начала учёта) закрывается без требования закрыть май.
pc = self._close('2026-06-01')
self.assertEqual(pc.state, 'done')
def test_accounting_start_blocks_earlier_month(self):
"""Нельзя закрыть месяц раньше даты начала учёта."""
from odoo.exceptions import UserError
self.company.rsbu_accounting_start = '2026-06-01'
self._post('2026-05-20', [('26', 10000.0, 0.0), ('51', 0.0, 10000.0)])
pc = self.env['rsbu.period.close'].create({
'period_month': '2026-05-01', 'company_id': self.company.id})
with self.assertRaises(UserError):
pc.action_close()
def test_cannot_close_future_month(self):
"""Нельзя закрыть незавершённый/будущий месяц (заморожен 2027-01-15)."""
from odoo.exceptions import UserError
pc = self.env['rsbu.period.close'].create({
'period_month': '2027-02-01', 'company_id': self.company.id})
with self.assertRaises(UserError):
pc.action_close()
def test_close_requires_previous_month(self):
"""Нельзя закрыть месяц, если предыдущий (с проводками) не закрыт."""
from odoo.exceptions import UserError
# Проводки в марте и апреле.
self._post('2026-03-15', [('26', 10000.0, 0.0), ('51', 0.0, 10000.0)])
self._post('2026-04-15', [('26', 20000.0, 0.0), ('51', 0.0, 20000.0)])
# Апрель без закрытого марта — блокируется.
pc_apr = self.env['rsbu.period.close'].create({
'period_month': '2026-04-01', 'company_id': self.company.id})
with self.assertRaises(UserError):
pc_apr.action_close()
# Закрываем март, затем апрель проходит.
self._close('2026-03-01')
pc_apr.action_close()
self.assertEqual(pc_apr.state, 'done')