PocketAccountant: custom ledger UI + deterministic agent (engine, ledger, retrieval, classifier)
c55ab5e verified | """Tests for the double-entry ledger and its integration with the tax engine. | |
| Run from the project root: python -m unittest discover -s tests | |
| """ | |
| import unittest | |
| from decimal import Decimal | |
| from src.engine import ( | |
| isr_provisional_monthly, | |
| iva_monthly, | |
| resico_isr_monthly, | |
| taxable_base, | |
| ) | |
| from src.ledger import Ledger, Line | |
| def dec(s): | |
| return Decimal(s) | |
| class TestPosting(unittest.TestCase): | |
| def setUp(self): | |
| self.lg = Ledger(":memory:") | |
| def tearDown(self): | |
| self.lg.close() | |
| def test_unbalanced_entry_rejected(self): | |
| with self.assertRaises(ValueError): | |
| self.lg.post( | |
| "2024-05-01", "bad", | |
| [Line("102.01", debit=100), Line("401.01", credit=50)], | |
| ) | |
| def test_empty_entry_rejected(self): | |
| with self.assertRaises(ValueError): | |
| self.lg.post("2024-05-01", "empty", [Line("102.01", debit=0)]) | |
| def test_income_builds_balanced_entry(self): | |
| self.lg.record_income("2024-05-10", "Diseño de logo", 45000, iva_rate="0.16") | |
| # bank 52,200 ; income 45,000 ; iva trasladado 7,200 | |
| self.assertEqual(self.lg.account_balance("102.01"), dec("52200.00")) | |
| self.assertEqual(self.lg.account_balance("401.01"), dec("45000.00")) | |
| self.assertEqual(self.lg.account_balance("213.01"), dec("7200.00")) | |
| def test_deductible_expense_splits_iva(self): | |
| self.lg.record_expense("2024-05-15", "Licencia Adobe", 12000, iva_rate="0.16") | |
| self.assertEqual(self.lg.account_balance("601.01"), dec("12000.00")) # expense | |
| self.assertEqual(self.lg.account_balance("118.01"), dec("1920.00")) # iva acreditable | |
| self.assertEqual(self.lg.account_balance("102.01"), dec("-13920.00")) # bank out | |
| def test_income_with_retentions(self): | |
| # Persona física → persona moral: 10% ISR + 10.6667% IVA retained | |
| self.lg.record_income( | |
| "2024-06-10", "Consultoría", 10000, iva_rate="0.16", | |
| isr_retenido="1000", iva_retenido="1066.67", | |
| ) | |
| self.assertEqual(self.lg.account_balance("102.01"), dec("9533.33")) # net cash | |
| self.assertEqual(self.lg.account_balance("119.02"), dec("1000.00")) # isr retenido | |
| self.assertEqual(self.lg.account_balance("119.01"), dec("1066.67")) # iva retenido | |
| def test_zero_rated_income_has_no_iva(self): | |
| self.lg.record_income("2024-05-20", "Export", 5000, iva_rate="0") | |
| self.assertEqual(self.lg.account_balance("102.01"), dec("5000.00")) | |
| self.assertEqual(self.lg.account_balance("213.01"), dec("0.00")) | |
| def test_classifier_specific_expense_account(self): | |
| self.lg.ensure_account("613.01", "Gastos de viaje", "expense", "debit") | |
| self.lg.record_expense("2024-05-18", "Vuelo a cliente", 3000, | |
| iva_rate="0.16", expense_code="613.01") | |
| self.assertEqual(self.lg.account_balance("613.01"), dec("3000.00")) | |
| class TestMonthTotals(unittest.TestCase): | |
| def setUp(self): | |
| self.lg = Ledger(":memory:") | |
| self.lg.record_income("2024-05-10", "Diseño", 45000, iva_rate="0.16") | |
| self.lg.record_expense("2024-05-15", "Adobe", 12000, iva_rate="0.16") | |
| self.lg.record_expense("2024-05-20", "Multa", 500, iva_rate="0", deductible=False) | |
| # noise in another month must not leak in | |
| self.lg.record_income("2024-06-01", "Otro mes", 99999, iva_rate="0.16") | |
| def tearDown(self): | |
| self.lg.close() | |
| def test_aggregates(self): | |
| m = self.lg.month_totals(2024, 5) | |
| self.assertEqual(m.income, dec("45000.00")) | |
| self.assertEqual(m.deductible_expenses, dec("12000.00")) | |
| self.assertEqual(m.nondeductible_expenses, dec("500.00")) | |
| self.assertEqual(m.iva_trasladado, dec("7200.00")) | |
| self.assertEqual(m.iva_acreditable, dec("1920.00")) | |
| def test_month_isolation(self): | |
| self.assertEqual(self.lg.month_totals(2024, 6).income, dec("99999.00")) | |
| class TestStatements(unittest.TestCase): | |
| def setUp(self): | |
| self.lg = Ledger(":memory:") | |
| self.lg.record_income("2024-05-10", "Diseño", 45000, iva_rate="0.16") | |
| self.lg.record_expense("2024-05-15", "Adobe", 12000, iva_rate="0.16") | |
| def tearDown(self): | |
| self.lg.close() | |
| def test_income_statement(self): | |
| s = self.lg.income_statement(2024, 5) | |
| self.assertEqual(s.revenue, dec("45000.00")) | |
| self.assertEqual(s.expenses, dec("12000.00")) | |
| self.assertEqual(s.net_profit, dec("33000.00")) | |
| def test_balance_sheet_balances(self): | |
| bs = self.lg.balance_sheet("2024-05-31") | |
| # assets: bank 38,280 + iva acreditable 1,920 = 40,200 | |
| # liabilities: iva trasladado 7,200 ; equity = 33,000 | |
| self.assertEqual(bs.assets, dec("40200.00")) | |
| self.assertEqual(bs.liabilities, dec("7200.00")) | |
| self.assertEqual(bs.equity, dec("33000.00")) | |
| # the accounting identity must hold | |
| self.assertEqual(bs.assets, dec(str(bs.liabilities + bs.equity))) | |
| class TestLedgerEngineIntegration(unittest.TestCase): | |
| """The whole point: ledger aggregates flow into the deterministic engine.""" | |
| def setUp(self): | |
| self.lg = Ledger(":memory:") | |
| self.lg.record_income("2024-05-10", "Diseño", 45000, iva_rate="0.16") | |
| self.lg.record_expense("2024-05-15", "Adobe", 12000, iva_rate="0.16") | |
| def tearDown(self): | |
| self.lg.close() | |
| def test_resico_from_ledger(self): | |
| m = self.lg.month_totals(2024, 5) | |
| self.assertEqual(resico_isr_monthly(m.income).amount, dec("495.00")) | |
| def test_general_isr_from_ledger(self): | |
| m = self.lg.month_totals(2024, 5) | |
| base = taxable_base(m.income, m.deductible_expenses) | |
| self.assertEqual(base, dec("33000")) | |
| self.assertEqual(isr_provisional_monthly(base).amount, dec("5418.90")) | |
| def test_iva_from_ledger(self): | |
| m = self.lg.month_totals(2024, 5) | |
| r = iva_monthly(m.iva_trasladado, m.iva_acreditable, m.iva_retenido) | |
| self.assertEqual(r.amount, dec("5280.00")) | |
| if __name__ == "__main__": | |
| unittest.main() | |