306 lines
14 KiB
Python
306 lines
14 KiB
Python
from odoo.tests import TransactionCase, tagged
|
||
|
||
|
||
@tagged('post_install', '-at_install')
|
||
class TestRsbuMapping(TransactionCase):
|
||
|
||
@classmethod
|
||
def setUpClass(cls):
|
||
super().setUpClass()
|
||
cls.company = cls.env.company
|
||
cls.partner = cls.env['res.partner'].create({'name': 'Map Клиент'})
|
||
cls.journal_bank = cls.env['account.journal'].search(
|
||
[('type', '=', 'bank'), ('company_id', '=', cls.company.id)],
|
||
limit=1)
|
||
cls.Template = cls.env['rsbu.operation.template']
|
||
cls.Matrix = cls.env['rsbu.operation.matrix']
|
||
# Роли счетов из типового плана.
|
||
cls.acc = lambda self, code: self.env['rsbu.account'].search([
|
||
('code', '=', code),
|
||
('company_id', '=', self.env.company.id)], limit=1)
|
||
|
||
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.Template.create({
|
||
'code': 'ADV_IN',
|
||
'name': 'Получение аванса',
|
||
'resolver_key': 'advance_received',
|
||
'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_advance(self, template):
|
||
return self.Matrix.create({
|
||
'name': 'Входящий платёж → аванс',
|
||
'doc_kind': 'payment_inbound',
|
||
'has_advance': 'any',
|
||
'template_id': template.id,
|
||
'priority': 10,
|
||
})
|
||
|
||
def _payment(self, amount=1050.0, vat=50.0):
|
||
return 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,
|
||
})
|
||
|
||
def test_detect_template_by_matrix(self):
|
||
tmpl = self._advance_template()
|
||
self._matrix_advance(tmpl)
|
||
pay = self._payment()
|
||
self.assertEqual(pay._rsbu_detect_template(), tmpl)
|
||
self.assertTrue(pay._rsbu_is_applicable())
|
||
|
||
def test_no_matrix_not_applicable(self):
|
||
# Без матрицы документ не в РСБУ-периметре.
|
||
pay = self._payment()
|
||
self.assertFalse(pay._rsbu_detect_template())
|
||
self.assertFalse(pay._rsbu_is_applicable())
|
||
|
||
def test_advance_resolver_lines_balanced(self):
|
||
tmpl = self._advance_template()
|
||
self._matrix_advance(tmpl)
|
||
pay = self._payment(amount=1050.0, vat=50.0)
|
||
lines = pay._rsbu_build_lines()
|
||
total_debit = sum(l.get('debit', 0.0) for l in lines)
|
||
total_credit = sum(l.get('credit', 0.0) for l in lines)
|
||
self.assertEqual(total_debit, total_credit)
|
||
# Дт 51 = 1050, авансовый НДС 50
|
||
self.assertEqual(total_debit, 1050.0 + 50.0)
|
||
|
||
def test_full_flow_generate_rsbu_move(self):
|
||
tmpl = self._advance_template()
|
||
self._matrix_advance(tmpl)
|
||
pay = self._payment(amount=1050.0, vat=50.0)
|
||
pay.action_post()
|
||
move = pay.rsbu_move_id
|
||
self.assertTrue(move)
|
||
self.assertEqual(move.state, 'posted')
|
||
# Проверим сальдо: 51 дебет 1050
|
||
acc51 = self._acc('51')
|
||
bal = acc51.get_balance(company_id=self.company.id)
|
||
self.assertEqual(bal['debit'], 1050.0)
|
||
# 68.НДС кредит 50 (начислен авансовый НДС)
|
||
acc68 = self._acc('68.НДС')
|
||
bal68 = acc68.get_balance(company_id=self.company.id)
|
||
self.assertEqual(bal68['credit'], 50.0)
|
||
|
||
def _sale_template(self, with_advance_roles=False):
|
||
roles = [
|
||
(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}),
|
||
]
|
||
if with_advance_roles:
|
||
roles += [
|
||
(0, 0, {'role': 'advance_received',
|
||
'account_id': self._acc('62.АВ').id}),
|
||
(0, 0, {'role': 'vat_advance',
|
||
'account_id': self._acc('76.АВ').id}),
|
||
]
|
||
return self.Template.create({
|
||
'code': 'SALE_T',
|
||
'name': 'Продажа',
|
||
'resolver_key': 'shipment_upd',
|
||
'role_ids': roles,
|
||
})
|
||
|
||
def _sale_invoice_with_vat(self):
|
||
"""Инвойс с НДС: untaxed 147.00, tax 22.05, total 169.05."""
|
||
tax = self.env['account.tax'].create({
|
||
'name': 'НДС 15% demo',
|
||
'amount': 15.0,
|
||
'amount_type': 'percent',
|
||
'type_tax_use': 'sale',
|
||
})
|
||
product = self.env['product.product'].create({'name': 'Demo P'})
|
||
return self.env['account.move'].create({
|
||
'move_type': 'out_invoice',
|
||
'partner_id': self.partner.id,
|
||
'invoice_line_ids': [(0, 0, {
|
||
'product_id': product.id,
|
||
'name': 'Demo',
|
||
'quantity': 1,
|
||
'price_unit': 147.0,
|
||
'tax_ids': [(6, 0, tax.ids)],
|
||
})],
|
||
})
|
||
|
||
def test_sale_with_vat_balanced(self):
|
||
# Регрессия: реализация с НДС должна давать сбалансированную проводку.
|
||
tmpl = self._sale_template()
|
||
inv = self._sale_invoice_with_vat()
|
||
inv.rsbu_template_id = tmpl.id
|
||
lines = inv._rsbu_build_lines()
|
||
total_debit = sum(l.get('debit', 0.0) for l in lines)
|
||
total_credit = sum(l.get('credit', 0.0) for l in lines)
|
||
self.assertAlmostEqual(total_debit, total_credit, places=2)
|
||
# Дт 62.01 = total 169.05, Дт 90.03 = НДС -> дебет = total + tax.
|
||
self.assertAlmostEqual(total_debit, 169.05 + 22.05, places=2)
|
||
|
||
def test_advance_offset_on_shipment(self):
|
||
"""Р1: при отгрузке после аванса зачёт 62.АВ и вычет авансового НДС."""
|
||
# 1) Аванс клиента 105 (НДС 5) через резолвер advance_received.
|
||
adv_tmpl = self._advance_template()
|
||
self._matrix_advance(adv_tmpl)
|
||
pay = self._payment(amount=105.0, vat=5.0)
|
||
pay.action_post()
|
||
self.assertTrue(pay.rsbu_move_id)
|
||
|
||
# Остатки после аванса: 62.АВ кредит 105, 76.АВ дебет 5.
|
||
acc_av = self._acc('62.АВ')
|
||
acc_vatav = self._acc('76.АВ')
|
||
bal_av = acc_av.get_balance(
|
||
company_id=self.company.id,
|
||
subconto={'partner': self.partner.id})
|
||
self.assertEqual(bal_av['balance'], 105.0)
|
||
self.assertEqual(bal_av['side'], 'credit')
|
||
|
||
# 2) Отгрузка тому же клиенту, шаблон с ролями аванса.
|
||
sale_tmpl = self._sale_template(with_advance_roles=True)
|
||
inv = self._sale_invoice_with_vat() # total 169.05
|
||
inv.rsbu_template_id = sale_tmpl.id
|
||
lines = inv._rsbu_build_lines()
|
||
# Проводка должна содержать зачёт аванса и вычет авансового НДС.
|
||
d = sum(l.get('debit', 0.0) for l in lines)
|
||
c = sum(l.get('credit', 0.0) for l in lines)
|
||
self.assertAlmostEqual(d, c, places=2)
|
||
inv.action_post()
|
||
|
||
# 3) После отгрузки зачёт аванса: 62.АВ по партнёру закрыт (=0).
|
||
# Вычет авансового НДС (76.АВ) при отгрузке НЕ делается — он вынесен
|
||
# в регламент конца периода (rsbu.period.close), поэтому 76.АВ здесь
|
||
# остаётся ненулевым (закрывается регламентом «книга покупок»).
|
||
bal_av2 = acc_av.get_balance(
|
||
company_id=self.company.id,
|
||
subconto={'partner': self.partner.id})
|
||
self.assertAlmostEqual(bal_av2['balance'], 0.0, places=2)
|
||
|
||
# ------------------------------------------------------------------
|
||
# customer_payment: аванс / постоплата / переплата
|
||
# ------------------------------------------------------------------
|
||
def _customer_payment_template(self):
|
||
tmpl = self.Template.create({
|
||
'code': 'CUST_PAY',
|
||
'name': 'Оплата от клиента',
|
||
'resolver_key': 'customer_payment',
|
||
'company_id': self.company.id,
|
||
'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.Matrix.create({
|
||
'name': 'Оплата клиента',
|
||
'doc_kind': 'payment_inbound',
|
||
'partner_kind': 'customer',
|
||
'has_advance': 'any',
|
||
'template_id': tmpl.id,
|
||
'company_id': self.company.id,
|
||
})
|
||
return tmpl
|
||
|
||
def _debt(self, partner, amount, date):
|
||
"""Создать дебиторку покупателя (реализация) для теста постоплаты."""
|
||
move = self.env['rsbu.move'].create({
|
||
'date': date, 'company_id': self.company.id,
|
||
'line_ids': [
|
||
(0, 0, {'account_id': self._acc('62.01').id, 'debit': amount,
|
||
'partner_id': partner.id}),
|
||
(0, 0, {'account_id': self._acc('90.01').id, 'credit': amount}),
|
||
],
|
||
})
|
||
move.action_post()
|
||
return move
|
||
|
||
def _inbound(self, partner, amount, date):
|
||
pay = self.env['account.payment'].create({
|
||
'payment_type': 'inbound', 'partner_type': 'customer',
|
||
'partner_id': partner.id, 'amount': amount,
|
||
'journal_id': self.journal_bank.id, 'company_id': self.company.id,
|
||
'date': date,
|
||
})
|
||
pay.action_post()
|
||
return pay
|
||
|
||
def test_customer_payment_advance(self):
|
||
"""Платёж без долга → аванс (Кт 62.АВ), только деньги.
|
||
|
||
Авансовый НДС в платеже НЕ начисляется: он живёт в отдельном
|
||
авансовом счёте-фактуре (rsbu.invoice, invoice_type='advance'),
|
||
рождаемом из платежа и не связанном с документом реализации
|
||
(realization_id пуст). Модель-источник авансового НДС — rsbu.invoice,
|
||
без изменений. Платёж двигает только деньги.
|
||
"""
|
||
self.company.rsbu_vat_mode = 'vat_5'
|
||
self._customer_payment_template()
|
||
p = self.env['res.partner'].create({'name': 'Аванс-клиент'})
|
||
pay = self._inbound(p, 105000.0, '2026-03-01')
|
||
acc_av = self._acc('62.АВ')
|
||
bal = acc_av.get_balance(company_id=self.company.id,
|
||
subconto={'partner': p.id})
|
||
self.assertAlmostEqual(bal['balance'], 105000.0, places=2)
|
||
# Авансовый НДС в платеже не начисляется (68.НДС не затронут платежом).
|
||
# дебиторка 62.01 не затронута
|
||
bal01 = self._acc('62.01').get_balance(
|
||
company_id=self.company.id, subconto={'partner': p.id})
|
||
self.assertAlmostEqual(bal01['balance'], 0.0, places=2)
|
||
|
||
def test_customer_payment_postpaid(self):
|
||
"""Платёж при наличии долга → погашение 62.01, без аванса и НДС."""
|
||
self.company.rsbu_vat_mode = 'vat_5'
|
||
self._customer_payment_template()
|
||
p = self.env['res.partner'].create({'name': 'Постоплата-клиент'})
|
||
self._debt(p, 50000.0, '2026-03-05')
|
||
pay = self._inbound(p, 50000.0, '2026-03-10')
|
||
# 62.01 закрыт (0), 62.АВ не создавался, авансовый НДС не начислен
|
||
bal01 = self._acc('62.01').get_balance(
|
||
company_id=self.company.id, subconto={'partner': p.id})
|
||
self.assertAlmostEqual(bal01['balance'], 0.0, places=2)
|
||
bal_av = self._acc('62.АВ').get_balance(
|
||
company_id=self.company.id, subconto={'partner': p.id})
|
||
self.assertAlmostEqual(bal_av['balance'], 0.0, places=2)
|
||
|
||
def test_customer_payment_overpaid(self):
|
||
"""Переплата → долг гасится (62.01), остаток в аванс (62.АВ).
|
||
|
||
Авансовый НДС в платеже не начисляется (вынесен в СФ на аванс).
|
||
"""
|
||
self.company.rsbu_vat_mode = 'vat_5'
|
||
self._customer_payment_template()
|
||
p = self.env['res.partner'].create({'name': 'Переплата-клиент'})
|
||
self._debt(p, 30000.0, '2026-03-06')
|
||
pay = self._inbound(p, 100000.0, '2026-03-11')
|
||
# долг 62.01 закрыт
|
||
bal01 = self._acc('62.01').get_balance(
|
||
company_id=self.company.id, subconto={'partner': p.id})
|
||
self.assertAlmostEqual(bal01['balance'], 0.0, places=2)
|
||
# аванс на остаток 70000
|
||
bal_av = self._acc('62.АВ').get_balance(
|
||
company_id=self.company.id, subconto={'partner': p.id})
|
||
self.assertAlmostEqual(bal_av['balance'], 70000.0, places=2)
|
||
# Авансовый НДС в платеже не начисляется (76.АВ платежом не затронут).
|