2026-08-14 20:54:23 -04:00
|
|
|
import unittest
|
2026-07-22 22:29:04 -04:00
|
|
|
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'))
|
|
|
|
|
|
2026-08-14 20:54:23 -04:00
|
|
|
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))
|
2026-07-22 22:29:04 -04:00
|
|
|
|
2026-08-14 20:54:23 -04:00
|
|
|
def _line(self, *tax_lines, **values):
|
2026-07-22 22:29:04 -04:00
|
|
|
line = SimpleNamespace(
|
|
|
|
|
id=1,
|
|
|
|
|
type='line',
|
2026-08-14 20:54:23 -04:00
|
|
|
currency=self.Currency(),
|
|
|
|
|
invoice=None,
|
2026-07-22 22:29:04 -04:00
|
|
|
amount=Decimal('100.00'),
|
2026-08-14 20:54:23 -04: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'))
|
2026-07-22 22:29:04 -04:00
|
|
|
self.assertEqual(
|
2026-08-14 20:54:23 -04:00
|
|
|
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)),
|
|
|
|
|
])
|
2026-07-22 22:29:04 -04:00
|
|
|
self.assertEqual(
|
2026-08-14 20:54:23 -04:00
|
|
|
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())
|
2026-07-22 22:29:04 -04:00
|
|
|
self.assertEqual(
|
2026-08-14 20:54:23 -04:00
|
|
|
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)))
|
2026-07-22 22:29:04 -04:00
|
|
|
|
|
|
|
|
def test_subtotal_resets_after_previous_subtotal(self):
|
|
|
|
|
invoice = SimpleNamespace()
|
2026-08-14 20:54:23 -04:00
|
|
|
line1 = self._line()
|
2026-07-22 22:29:04 -04:00
|
|
|
subtotal1 = SimpleNamespace(
|
|
|
|
|
id=1, type='subtotal', invoice=invoice, currency=None)
|
2026-08-14 20:54:23 -04:00
|
|
|
line2 = self._line(amount=Decimal('50.00'))
|
2026-07-22 22:29:04 -04:00
|
|
|
subtotal2 = SimpleNamespace(
|
|
|
|
|
id=2, type='subtotal', invoice=invoice, currency=None)
|
|
|
|
|
invoice.lines = [line1, subtotal1, line2, subtotal2]
|
|
|
|
|
self.assertEqual(
|
2026-08-14 20:54:23 -04:00
|
|
|
InvoiceLine._compute_amounts(subtotal2)['line_total'],
|
2026-07-22 22:29:04 -04:00
|
|
|
Decimal('50.00'))
|