"""Unit tests for FinanceTools.""" import pytest from unittest.mock import AsyncMock, MagicMock from agent_service.tools.finance_tools import FinanceTools def _make_tools(): odoo = MagicMock() odoo.search_read = AsyncMock(return_value=[]) odoo.read = AsyncMock(return_value=[]) odoo.call = AsyncMock(return_value=True) odoo.post_chatter = AsyncMock(return_value=99) return FinanceTools(odoo) # ── get_invoices ────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_invoices_default(): t = _make_tools() result = await t.get_invoices() t._odoo.search_read.assert_awaited_once() assert isinstance(result, list) @pytest.mark.asyncio async def test_get_invoices_state_filter(): t = _make_tools() await t.get_invoices(state='posted') domain = t._odoo.search_read.call_args[0][1] assert ['state', '=', 'posted'] in domain @pytest.mark.asyncio async def test_get_invoices_move_type_filter(): t = _make_tools() await t.get_invoices(move_type='out_invoice') domain = t._odoo.search_read.call_args[0][1] assert ['move_type', '=', 'out_invoice'] in domain @pytest.mark.asyncio async def test_get_invoices_partner_filter(): t = _make_tools() await t.get_invoices(partner_id=5) domain = t._odoo.search_read.call_args[0][1] assert ['partner_id', '=', 5] in domain @pytest.mark.asyncio async def test_get_invoices_date_range(): t = _make_tools() await t.get_invoices(date_from='2026-01-01', date_to='2026-01-31') domain = t._odoo.search_read.call_args[0][1] assert ['invoice_date', '>=', '2026-01-01'] in domain assert ['invoice_date', '<=', '2026-01-31'] in domain # ── get_overdue_invoices ────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_overdue_invoices_default(): t = _make_tools() result = await t.get_overdue_invoices() domain = t._odoo.search_read.call_args[0][1] assert ['move_type', 'in', ['out_invoice', 'out_refund']] in domain assert ['state', '=', 'posted'] in domain assert ['payment_state', 'in', ['not_paid', 'partial']] in domain @pytest.mark.asyncio async def test_get_overdue_invoices_partner_filter(): t = _make_tools() await t.get_overdue_invoices(partner_id=7) domain = t._odoo.search_read.call_args[0][1] assert ['partner_id', '=', 7] in domain # ── get_unreconciled_statements ─────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_unreconciled_statements(): t = _make_tools() await t.get_unreconciled_statements(journal_id=3) domain = t._odoo.search_read.call_args[0][1] assert ['journal_id', '=', 3] in domain assert ['is_reconciled', '=', False] in domain @pytest.mark.asyncio async def test_get_unreconciled_statements_date_range(): t = _make_tools() await t.get_unreconciled_statements(journal_id=3, date_from='2026-01-01', date_to='2026-01-31') domain = t._odoo.search_read.call_args[0][1] assert ['date', '>=', '2026-01-01'] in domain # ── send_payment_reminder ───────────────────────────────────────────────────── @pytest.mark.asyncio async def test_send_payment_reminder_success(): t = _make_tools() t._odoo.read = AsyncMock(return_value=[{ 'name': 'INV/001', 'partner_id': [1, 'ACME'], 'amount_residual': 500.0, 'invoice_date_due': '2026-01-01', }]) result = await t.send_payment_reminder(invoice_id=1) assert result['success'] is True assert result['invoice'] == 'INV/001' t._odoo.post_chatter.assert_awaited_once() @pytest.mark.asyncio async def test_send_payment_reminder_invoice_not_found(): t = _make_tools() t._odoo.read = AsyncMock(return_value=[]) result = await t.send_payment_reminder(invoice_id=999) assert result['success'] is False assert 'error' in result @pytest.mark.asyncio async def test_send_payment_reminder_custom_message(): t = _make_tools() t._odoo.read = AsyncMock(return_value=[{ 'name': 'INV/001', 'partner_id': [1, 'ACME'], 'amount_residual': 500.0, 'invoice_date_due': '2026-01-01', }]) await t.send_payment_reminder(invoice_id=1, custom_message='Please pay!') call_args = t._odoo.post_chatter.call_args[0] assert call_args[2] == 'Please pay!' # ── get_financial_summary ───────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_financial_summary_current_period(): t = _make_tools() t._odoo.search_read = AsyncMock(return_value=[ {'amount_total': 1000.0, 'amount_residual': 200.0, 'payment_state': 'partial'}, {'amount_total': 2000.0, 'amount_residual': 0.0, 'payment_state': 'paid'}, ]) result = await t.get_financial_summary() assert result['total_invoiced'] == 3000.0 assert result['invoice_count'] == 2 assert result['paid_count'] == 1 assert 'collection_rate' in result @pytest.mark.asyncio async def test_get_financial_summary_explicit_period(): t = _make_tools() await t.get_financial_summary(period='2026-01') domain = t._odoo.search_read.call_args[0][1] assert ['invoice_date', '>=', '2026-01-01'] in domain assert ['invoice_date', '<=', '2026-01-31'] in domain @pytest.mark.asyncio async def test_get_financial_summary_empty(): t = _make_tools() result = await t.get_financial_summary() assert result['total_invoiced'] == 0 assert result['collection_rate'] == 0 # ── get_payment_history ─────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_get_payment_history(): t = _make_tools() t._odoo.search_read = AsyncMock(return_value=[ {'name': 'PAY/001', 'amount': 500.0, 'state': 'posted'} ]) result = await t.get_payment_history(partner_id=5) domain = t._odoo.search_read.call_args[0][1] assert ['partner_id', '=', 5] in domain assert ['state', '=', 'posted'] in domain assert len(result) == 1 # ── flag_for_review ─────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_flag_for_review(): t = _make_tools() result = await t.flag_for_review('account.move', 1, 'Overdue 90 days') assert result['flagged'] is True assert result['model'] == 'account.move' assert result['record_id'] == 1 t._odoo.post_chatter.assert_awaited_once() note = t._odoo.post_chatter.call_args[0][2] assert 'MEDIUM' in note @pytest.mark.asyncio async def test_flag_for_review_high_severity(): t = _make_tools() result = await t.flag_for_review('account.move', 1, 'reason', severity='high') assert result['severity'] == 'high' note = t._odoo.post_chatter.call_args[0][2] assert 'HIGH' in note # ── post_chatter_note ───────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_post_chatter_note(): t = _make_tools() result = await t.post_chatter_note('account.move', 1, 'Payment received') assert result['success'] is True assert result['message_id'] == 99 t._odoo.post_chatter.assert_awaited_once_with('account.move', 1, 'Payment received')