251 lines
8.5 KiB
Python
251 lines
8.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Tests for l10n_ru_contract_sale — overdue debt check on sale order confirmation
|
|
and invoice preparation with contract fields.
|
|
|
|
Validates: Requirements 16.1, 16.2, 16.3
|
|
"""
|
|
from datetime import date, timedelta
|
|
|
|
from odoo.tests.common import TransactionCase
|
|
from odoo.exceptions import ValidationError
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _get_or_create_accounts(env):
|
|
"""Return a receivable and income account for the current company."""
|
|
receivable = env['account.account'].search([
|
|
('account_type', '=', 'asset_receivable'),
|
|
('company_ids', 'in', env.company.id),
|
|
], limit=1)
|
|
payable = env['account.account'].search([
|
|
('account_type', '=', 'liability_payable'),
|
|
('company_ids', 'in', env.company.id),
|
|
], limit=1)
|
|
income = env['account.account'].search([
|
|
('account_type', '=', 'income'),
|
|
('company_ids', 'in', env.company.id),
|
|
], limit=1)
|
|
return receivable, payable, income
|
|
|
|
|
|
def _get_or_create_journal(env):
|
|
"""Return or create a sales journal."""
|
|
journal = env['account.journal'].search([
|
|
('type', '=', 'sale'),
|
|
('company_id', '=', env.company.id),
|
|
], limit=1)
|
|
if not journal:
|
|
journal = env['account.journal'].create({
|
|
'name': 'Test Sales Journal CS',
|
|
'type': 'sale',
|
|
'code': 'TSCS',
|
|
'company_id': env.company.id,
|
|
})
|
|
return journal
|
|
|
|
|
|
def _get_or_create_payment_term(env):
|
|
"""Return or create a payment term."""
|
|
term = env['account.payment.term'].search([], limit=1)
|
|
if not term:
|
|
term = env['account.payment.term'].create({'name': 'Immediate CS'})
|
|
return term
|
|
|
|
|
|
def _make_profile(env, journal, payment_term, receivable, payable, max_receivable=10000.0):
|
|
"""Create a contract.profile."""
|
|
return env['contract.profile'].create({
|
|
'name': 'Test Profile CS',
|
|
'journal_id': journal.id,
|
|
'payment_term_id': payment_term.id,
|
|
'receivable_account_id': receivable.id,
|
|
'payable_account_id': payable.id,
|
|
'max_receivable_id': max_receivable,
|
|
})
|
|
|
|
|
|
def _make_partner(env):
|
|
"""Create a company partner."""
|
|
return env['res.partner'].create({
|
|
'name': 'Test Partner CS',
|
|
'is_company': True,
|
|
})
|
|
|
|
|
|
def _make_contract(env, partner, profile):
|
|
"""Create a minimal partner.contract.customer."""
|
|
return env['partner.contract.customer'].create({
|
|
'date_start': '2024-01-01',
|
|
'date_end': '2024-12-31',
|
|
'type': 'customer',
|
|
'profile_id': profile.id,
|
|
'partner_id': partner.id,
|
|
'partner_type': 'company',
|
|
'company_id': env.company.id,
|
|
})
|
|
|
|
|
|
def _make_product(env):
|
|
"""Return or create a storable product."""
|
|
product = env['product.product'].search([
|
|
('type', '=', 'service'),
|
|
], limit=1)
|
|
if not product:
|
|
product = env['product.product'].create({
|
|
'name': 'Test Product CS',
|
|
'type': 'service',
|
|
})
|
|
return product
|
|
|
|
|
|
def _make_sale_order(env, partner, contract, product):
|
|
"""Create a confirmed-ready sale order linked to a contract."""
|
|
order = env['sale.order'].create({
|
|
'partner_id': partner.id,
|
|
'mt_contract_id': contract.id,
|
|
'order_line': [(0, 0, {
|
|
'product_id': product.id,
|
|
'product_uom_qty': 1.0,
|
|
'price_unit': 500.0,
|
|
})],
|
|
})
|
|
return order
|
|
|
|
|
|
def _make_overdue_invoice(env, partner, journal, income_account, amount, days_overdue=10):
|
|
"""Create a posted out_invoice that is overdue and unpaid."""
|
|
due_date = date.today() - timedelta(days=days_overdue)
|
|
invoice = env['account.move'].create({
|
|
'move_type': 'out_invoice',
|
|
'partner_id': partner.id,
|
|
'journal_id': journal.id,
|
|
'invoice_date': due_date,
|
|
'invoice_date_due': due_date,
|
|
'invoice_line_ids': [(0, 0, {
|
|
'name': 'Overdue line',
|
|
'quantity': 1.0,
|
|
'price_unit': amount,
|
|
'account_id': income_account.id,
|
|
})],
|
|
})
|
|
invoice.action_post()
|
|
return invoice
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestContractSaleOrder
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestContractSaleOrder(TransactionCase):
|
|
"""
|
|
Tests for sale order confirmation with overdue debt check and
|
|
invoice preparation with contract fields.
|
|
|
|
Validates: Requirements 16.1, 16.2, 16.3
|
|
"""
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.receivable, self.payable, self.income = _get_or_create_accounts(self.env)
|
|
self.journal = _get_or_create_journal(self.env)
|
|
self.payment_term = _get_or_create_payment_term(self.env)
|
|
self.partner = _make_partner(self.env)
|
|
self.product = _make_product(self.env)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Requirement 16.1 — overdue debt > max_receivable raises ValidationError
|
|
# ------------------------------------------------------------------
|
|
|
|
def test_confirm_with_overdue_debt_raises(self):
|
|
"""
|
|
Req 16.1 — confirming a sale order when the partner has overdue debt
|
|
exceeding max_receivable raises ValidationError.
|
|
"""
|
|
profile = _make_profile(
|
|
self.env, self.journal, self.payment_term,
|
|
self.receivable, self.payable,
|
|
max_receivable=100.0,
|
|
)
|
|
contract = _make_contract(self.env, self.partner, profile)
|
|
|
|
# Create an overdue posted invoice with amount > max_receivable
|
|
_make_overdue_invoice(
|
|
self.env, self.partner, self.journal, self.income,
|
|
amount=500.0, # 500 > 100 (max_receivable)
|
|
)
|
|
|
|
order = _make_sale_order(self.env, self.partner, contract, self.product)
|
|
|
|
with self.assertRaises(ValidationError):
|
|
order.action_confirm()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Requirement 16.2 — debt within limit confirms without errors
|
|
# ------------------------------------------------------------------
|
|
|
|
def test_confirm_within_debt_limit_ok(self):
|
|
"""
|
|
Req 16.2 — confirming a sale order when overdue debt is within
|
|
max_receivable succeeds without errors.
|
|
"""
|
|
profile = _make_profile(
|
|
self.env, self.journal, self.payment_term,
|
|
self.receivable, self.payable,
|
|
max_receivable=10000.0,
|
|
)
|
|
contract = _make_contract(self.env, self.partner, profile)
|
|
|
|
# Create an overdue invoice with amount well below max_receivable
|
|
_make_overdue_invoice(
|
|
self.env, self.partner, self.journal, self.income,
|
|
amount=50.0, # 50 < 10000 (max_receivable)
|
|
)
|
|
|
|
order = _make_sale_order(self.env, self.partner, contract, self.product)
|
|
|
|
# Should not raise
|
|
order.action_confirm()
|
|
self.assertEqual(order.state, 'sale',
|
|
"Order should be confirmed (state='sale') when debt is within limit")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Requirement 16.3 — _prepare_invoice copies mt_contract_id and osnovanie
|
|
# ------------------------------------------------------------------
|
|
|
|
def test_prepare_invoice_copies_contract(self):
|
|
"""
|
|
Req 16.3 — _prepare_invoice transfers mt_contract_id and osnovanie
|
|
from the sale order's contract to the invoice values dict.
|
|
"""
|
|
profile = _make_profile(
|
|
self.env, self.journal, self.payment_term,
|
|
self.receivable, self.payable,
|
|
max_receivable=10000.0,
|
|
)
|
|
contract = _make_contract(self.env, self.partner, profile)
|
|
order = _make_sale_order(self.env, self.partner, contract, self.product)
|
|
order.action_confirm()
|
|
|
|
invoice_vals = order._prepare_invoice()
|
|
|
|
self.assertEqual(
|
|
invoice_vals.get('mt_contract_id'), contract.id,
|
|
"_prepare_invoice should copy mt_contract_id from the contract",
|
|
)
|
|
self.assertIn(
|
|
'osnovanie', invoice_vals,
|
|
"_prepare_invoice should set 'osnovanie' in invoice values",
|
|
)
|
|
self.assertTrue(
|
|
invoice_vals['osnovanie'],
|
|
"'osnovanie' should be a non-empty string",
|
|
)
|
|
self.assertIn(
|
|
contract.name, invoice_vals['osnovanie'],
|
|
"'osnovanie' should contain the contract name",
|
|
)
|