import unittest from datetime import date from decimal import Decimal from types import SimpleNamespace from trytond.modules.account_invoice_do.invoice import Invoice, InvoiceLine class AccountInvoiceDoUnitTestCase(unittest.TestCase): class Currency: def round(self, amount): return amount.quantize(Decimal('0.01')) def _tax_line( self, code, amount, template_name=None, rate='0.10'): template = (SimpleNamespace(name=template_name) if template_name else None) tax = SimpleNamespace( group=SimpleNamespace(code=code), template=template, type='percentage', rate=Decimal(rate), amount=None) return SimpleNamespace(tax=tax, amount=Decimal(amount)) def _line(self, *tax_lines, **values): line = SimpleNamespace( id=1, type='line', currency=self.Currency(), invoice=None, amount=Decimal('100.00'), taxes=[tax.tax for tax in tax_lines], ) line._get_taxes = lambda: { index: tax for index, tax in enumerate(tax_lines)} for name, value in values.items(): setattr(line, name, value) return line def test_due_date_uses_last_payment_term_maturity(self): term = SimpleNamespace(compute=lambda amount, currency, start: [ (date(2026, 2, 10), Decimal('40.00')), (date(2026, 3, 10), Decimal('60.00')), ]) invoice = SimpleNamespace( lines_to_pay=[], payment_term=term, payment_term_date=None, invoice_date=date(2026, 1, 10), currency=self.Currency(), total_amount=Decimal('100.00')) self.assertEqual( Invoice._compute_due_date(invoice), date(2026, 3, 10)) def test_due_date_prefers_posted_maturity_dates(self): invoice = SimpleNamespace(lines_to_pay=[ SimpleNamespace(maturity_date=date(2026, 2, 10)), SimpleNamespace(maturity_date=date(2026, 4, 10)), ]) self.assertEqual( Invoice._compute_due_date(invoice), date(2026, 4, 10)) def test_amount_paid_preserves_credit_note_sign(self): credit = SimpleNamespace( id=1, state='posted', total_amount=Decimal('-118.00'), amount_to_pay=Decimal('-18.00'), currency=self.Currency()) self.assertEqual( Invoice.get_amount_paid([credit], 'amount_paid'), {1: Decimal('-100.00')}) def test_days_elapsed_does_not_require_payment_term(self): invoice = SimpleNamespace( state='posted', invoice_date=date(2026, 1, 1), due_date=None, _today=lambda: date(2026, 1, 11)) invoice._positive_age = lambda reference: Invoice._positive_age( invoice, reference) self.assertEqual(Invoice.get_days_elapsed(invoice), 10) self.assertIsNone(Invoice.get_days_overdue(invoice)) def test_tax_amounts_use_tryton_computed_tax_lines(self): line = self._line( self._tax_line('ITBIS', '18.00'), self._tax_line('ITBIS', '-5.40', rate='-0.054'), self._tax_line('ISC', '10.00'), self._tax_line('CDT', '2.00'), self._tax_line('ISR', '-10.00'), self._tax_line('OTROS', '10.00', 'Legal Tip 10%')) self.assertEqual(InvoiceLine._compute_amounts(line), { 'itbis': Decimal('18.00'), 'itbis_withholding': Decimal('5.40'), 'isc': Decimal('10.00'), 'cdt': Decimal('2.00'), 'isr_withholding': Decimal('10.00'), 'tip': Decimal('10.00'), 'line_total': Decimal('124.60'), }) def test_credit_note_itbis_is_not_classified_as_withholding(self): line = self._line(self._tax_line('ITBIS', '-18.00', rate='0.18')) self.assertEqual(InvoiceLine._compute_amounts(line), { 'itbis': Decimal('18.00'), 'itbis_withholding': Decimal('0.00'), 'isc': Decimal('0.00'), 'cdt': Decimal('0.00'), 'isr_withholding': Decimal('0.00'), 'tip': Decimal('0.00'), 'line_total': Decimal('82.00'), }) def test_tax_kind_ignores_incomplete_external_tax_lines(self): self.assertIsNone(InvoiceLine._tax_kind(SimpleNamespace(tax=None))) tax = SimpleNamespace( group=None, template=None, type='percentage', rate=Decimal('0.18'), amount=None) self.assertIsNone(InvoiceLine._tax_kind(SimpleNamespace(tax=tax))) def test_subtotal_resets_after_previous_subtotal(self): invoice = SimpleNamespace() line1 = self._line() subtotal1 = SimpleNamespace( id=1, type='subtotal', invoice=invoice, currency=None) line2 = self._line(amount=Decimal('50.00')) subtotal2 = SimpleNamespace( id=2, type='subtotal', invoice=invoice, currency=None) invoice.lines = [line1, subtotal1, line2, subtotal2] self.assertEqual( InvoiceLine._compute_amounts(subtotal2)['line_total'], Decimal('50.00'))