import datetime from decimal import Decimal from pathlib import Path from types import SimpleNamespace import unittest from trytond.exceptions import UserError from trytond.modules.ncf_manager.sequence import Sequence from trytond.modules.ncf_manager.invoice import Invoice from trytond.pool import Pool from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.transaction import Transaction class NcfManagerTestCase(ModuleTestCase): "Test Ncf Manager module" module = 'ncf_manager' def _create_company(self): pool = Pool() Company = pool.get('company.company') Currency = pool.get('currency.currency') Party = pool.get('party.party') currency, = Currency.create([{ 'name': 'Dominican Peso', 'code': 'DOP', 'symbol': 'RD$', 'digits': 2, 'rounding': '0.01', }]) party, = Party.create([{ 'name': 'Empresa NCF', 'addresses': [('create', [{}])], }]) company, = Company.create([{ 'party': party.id, 'currency': currency.id, }]) return company def _create_chart(self, company): pool = Pool() Account = pool.get('account.account') AccountTemplate = pool.get('account.account.template') CreateChart = pool.get('account.create_chart', type='wizard') Journal = pool.get('account.journal') ModelData = pool.get('ir.model.data') session_id, _start, _end = CreateChart.create() chart = CreateChart(session_id) chart.account.account_template = AccountTemplate(ModelData.get_id( 'account_do', 'do_account_root_en')) chart.account.company = company chart.transition_create_account() receivable, = Account.search([ ('company', '=', company.id), ('code', '=', '110201'), ], limit=1) payable, = Account.search([ ('company', '=', company.id), ('code', '=', '210101'), ], limit=1) revenue, = Account.search([ ('company', '=', company.id), ('code', '=', '4102'), ], limit=1) chart.properties.company = company chart.properties.account_receivable = receivable chart.properties.account_payable = payable with Transaction().set_context(company=company.id): chart.transition_create_properties() CreateChart.delete(session_id) journal, = Journal.search([('type', '=', 'revenue')], limit=1) return receivable, revenue, journal def _create_fiscalyear(self, company, today=None): pool = Pool() FiscalYear = pool.get('account.fiscalyear') InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence') StrictSequence = pool.get('ir.sequence.strict') SequenceType = pool.get('ir.sequence.type') today = today or datetime.date.today() year = today.year move_type, = SequenceType.search([ ('name', '=', 'Account Move'), ], limit=1) invoice_type, = SequenceType.search([ ('name', '=', 'Invoice'), ], limit=1) move_sequence, = StrictSequence.create([{ 'name': 'FY 2026 Moves', 'sequence_type': move_type.id, 'company': company.id, }]) invoice_sequence, = StrictSequence.create([{ 'name': 'FY 2026 Invoices', 'sequence_type': invoice_type.id, 'company': company.id, }]) fiscalyear, = FiscalYear.create([{ 'name': str(year), 'start_date': datetime.date(year, 1, 1), 'end_date': datetime.date(year, 12, 31), 'company': company.id, 'move_sequence': move_sequence.id, }]) fiscalyear.invoice_sequences = [InvoiceSequence( company=company, in_invoice_sequence=invoice_sequence, in_credit_note_sequence=invoice_sequence, out_invoice_sequence=invoice_sequence, out_credit_note_sequence=invoice_sequence)] fiscalyear.save() FiscalYear.create_period([fiscalyear]) def _create_payment_term(self): PaymentTerm = Pool().get('account.invoice.payment_term') term, = PaymentTerm.create([{ 'name': 'Contado', 'lines': [('create', [{'type': 'remainder'}])], }]) return term def _create_ncf_sequence(self, company, today=None): pool = Pool() ModelData = pool.get('ir.model.data') Manager = pool.get('ncf.manager') ManagerSequence = pool.get('ncf.manager.sequence') StrictSequence = pool.get('ir.sequence.strict') SequenceType = pool.get('ir.sequence.type') today = today or datetime.date.today() sequence_type = SequenceType(ModelData.get_id( 'ncf_manager', 'sequence_type_out_invoice_01')) strict_sequence, = StrictSequence.create([{ 'name': 'B01 Factura Credito Fiscal', 'sequence_type': sequence_type.id, 'company': company.id, 'prefix': 'B01', 'padding': 8, 'number_next': 1, 'number_increment': 1, 'from_number': 1, 'until_number': 2, 'expiration_date': datetime.date(today.year, 12, 31), }]) manager, = Manager.create([{ 'company': company.id, 'app_date': datetime.date(today.year, 1, 1), 'app_number': '0000000001', 'auth_number': '0000000001', 'state': 'active', }]) with Transaction().set_context(company=company.id): sequence, = ManagerSequence.create([{ 'pos': manager.id, 'ncf_type': '1', 'ncf_sequences': strict_sequence.id, 'invoice_type': 'invoice_customer', 'state': 'active', }]) return sequence @with_transaction() def test_customer_invoice_post_assigns_b01_ncf(self): 'Test full NCF assignment workflow on posted customer invoice' pool = Pool() Invoice = pool.get('account.invoice') Party = pool.get('party.party') today = datetime.date.today() company = self._create_company() receivable, revenue, journal = self._create_chart(company) self._create_fiscalyear(company, today=today) payment_term = self._create_payment_term() ncf_sequence = self._create_ncf_sequence(company, today=today) party, = Party.create([{ 'name': 'Cliente NCF', 'addresses': [('create', [{}])], }]) with Transaction().set_context( company=company.id, date=today): invoice, = Invoice.create([{ 'type': 'out', 'company': company.id, 'party': party.id, 'invoice_address': party.addresses[0].id, 'account': receivable.id, 'currency': company.currency.id, 'invoice_date': today, 'accounting_date': today, 'journal': journal.id, 'payment_term': payment_term.id, 'ncf_type': '1', 'ncf_sequence': ncf_sequence.id, 'revenue_type': '01', 'payment_method': '04', 'lines': [('create', [{ 'type': 'line', 'description': 'Servicio', 'quantity': 1, 'unit_price': Decimal('500.00'), 'account': revenue.id, }])], }]) Invoice.post([invoice]) invoice = Invoice(invoice.id) self.assertEqual(invoice.state, 'posted') self.assertEqual(invoice.ncf_number, 'B0100000001') self.assertEqual(invoice.ncf_sequence, ncf_sequence) self.assertEqual( invoice.expiration_date, datetime.date(today.year, 12, 31)) @with_transaction() def test_assisted_sequence_workflow(self): pool = Pool() Manager = pool.get('ncf.manager') today = datetime.date.today() company = self._create_company() manager, = Manager.create([{ 'company': company.id, 'app_date': today, 'app_number': 'APP0000001', 'auth_number': 'AUT0000001', 'ncf_sequences': [('create', [{ 'invoice_type': 'invoice_customer', 'ncf_type': '1', 'sequence_mode': 'create', 'from_number': 100, 'number_next': 100, 'until_number': 109, 'expiration_date': datetime.date(today.year, 12, 31), }])], }]) Manager.activate([manager]) manager = Manager(manager.id) line, = manager.ncf_sequences self.assertEqual(manager.state, 'active') self.assertEqual(line.state, 'active') self.assertEqual(line.sequence_mode, 'existing') self.assertEqual(line.ncf_sequences.prefix, 'B01') self.assertEqual(line.ncf_sequences.padding, 8) self.assertEqual(line.ncf_sequences.number_next, 100) self.assertEqual(line.availability, 'low') Manager.finish([manager]) manager = Manager(manager.id) self.assertEqual(manager.state, 'finished') self.assertEqual(manager.ncf_sequences[0].state, 'finished') self.assertIsNotNone(manager.finished_at) del ModuleTestCase class NcfManagerUnitTestCase(unittest.TestCase): def test_invoice_report_uses_current_tax_field_names(self): path = Path(__file__).parents[1] / 'invoice.fodt' template = path.read_text(encoding='utf-8') self.assertNotIn('line.itbis_line', template) self.assertEqual(template.count('line.itbis_invoiced_line'), 2) self.assertEqual(template.count('if test="invoice.tip'), 2) self.assertEqual(template.count('format_currency(invoice.tip'), 2) def test_sequence_defaults_match_ncf_type(self): sequence = SimpleNamespace( sequence_type=SimpleNamespace(name='Factura de Crédito Fiscal')) self.assertEqual(Sequence.on_change_with_prefix(sequence), 'B01') self.assertEqual(Sequence.on_change_with_padding(sequence), 8) def test_sequence_approved_and_remaining_quantities(self): sequence = SimpleNamespace( from_number=1, until_number=10, number_next=4) self.assertEqual(Sequence.on_change_with_approved_qty(sequence), 10) self.assertEqual(Sequence.on_change_with_remaining_qty(sequence), 7) def test_ncf_structure_and_normalization(self): invoice = SimpleNamespace( ncf_number=' b0100000001 ', ncf_type='1') Invoice._ncf_validate_number(invoice) self.assertEqual(invoice.ncf_number, 'B0100000001') invoice.ncf_number = 'E310000000001' invoice.ncf_type = '31' Invoice._ncf_validate_number(invoice) def test_ncf_rejects_invalid_structure_or_type(self): invoice = SimpleNamespace(ncf_number='B010001', ncf_type='1') with self.assertRaises(UserError): Invoice._ncf_validate_number(invoice) def test_legacy_supplier_ncf_is_preserved_for_migration(self): invoice = SimpleNamespace( type='in', ncf_number='A010010010100000001', ncf_type='1') Invoice._ncf_validate_number(invoice) self.assertEqual(invoice.ncf_number, 'A010010010100000001') invoice.ncf_number = 'B0200000001' with self.assertRaises(UserError): Invoice._ncf_validate_number(invoice) def test_fiscal_cancellation_requires_ncf_number(self): fiscal_invoice = SimpleNamespace(type='out', ncf_number='B0100000001') non_fiscal_invoice = SimpleNamespace(type='out', ncf_number=None) supplier_invoice = SimpleNamespace(type='in', ncf_number='B1100000001') self.assertTrue( Invoice._ncf_is_fiscal_cancellation_required(fiscal_invoice)) self.assertFalse( Invoice._ncf_is_fiscal_cancellation_required(non_fiscal_invoice)) self.assertFalse( Invoice._ncf_is_fiscal_cancellation_required(supplier_invoice)) def test_fiscal_cancellation_information_is_required(self): invoice = SimpleNamespace( type='out', rec_name='INV/1', ncf_number='B0100000001', ncf_cancellation_type=None, ncf_cancellation_reason=None) with self.assertRaises(UserError): Invoice._ncf_check_cancellation_information(invoice) invoice.ncf_cancellation_type = '04' with self.assertRaises(UserError): Invoice._ncf_check_cancellation_information(invoice) invoice.ncf_cancellation_reason = 'Corrección de la información' Invoice._ncf_check_cancellation_information(invoice) def test_debit_note_code_follows_origin_ncf_type(self): original_enabled = Invoice._ncf_electronic_invoicing_enabled Invoice._ncf_electronic_invoicing_enabled = classmethod( lambda cls: False) try: physical = SimpleNamespace(ncf_type='1') electronic = SimpleNamespace(ncf_type='31') self.assertEqual( Invoice._ncf_resolve_debit_note_code(physical), '3') self.assertEqual( Invoice._ncf_resolve_debit_note_code(electronic), '3') finally: Invoice._ncf_electronic_invoicing_enabled = original_enabled def test_debit_note_preparation_uses_physical_without_electronic_module(self): electronic_sequence = SimpleNamespace(ncf_type='33') physical_sequence = SimpleNamespace(ncf_type='3') original_find_active_sequence = Invoice._ncf_find_active_sequence original_enabled = Invoice._ncf_electronic_invoicing_enabled def find_sequence(cls, invoice, ncf_code): if ncf_code == '33': return electronic_sequence if ncf_code == '3': return physical_sequence return None Invoice._ncf_find_active_sequence = classmethod(find_sequence) Invoice._ncf_electronic_invoicing_enabled = classmethod( lambda cls: False) try: origin = SimpleNamespace( ncf_number='B0100000001', ncf_type='1', payment_method='04', revenue_type='01', isr_withholding_type=None) debit_note = SimpleNamespace( company=SimpleNamespace(rec_name='Solutema'), type='out', ncf_number_modified=None, ncf_type=None, ncf_sequence=None, payment_method=None, revenue_type=None, isr_withholding_type=None) Invoice._ncf_prepare_debit_note_from_origin(origin, debit_note) finally: Invoice._ncf_find_active_sequence = original_find_active_sequence Invoice._ncf_electronic_invoicing_enabled = original_enabled self.assertEqual(debit_note.ncf_number_modified, 'B0100000001') self.assertEqual(debit_note.ncf_type, '3') self.assertEqual(debit_note.ncf_sequence, physical_sequence) self.assertEqual(debit_note.payment_method, '04') self.assertEqual(debit_note.revenue_type, '01') def test_debit_note_preparation_prefers_electronic_when_module_is_active(self): electronic_sequence = SimpleNamespace(ncf_type='33') original_find_active_sequence = Invoice._ncf_find_active_sequence original_enabled = Invoice._ncf_electronic_invoicing_enabled def find_sequence(cls, invoice, ncf_code): return electronic_sequence if ncf_code == '33' else None Invoice._ncf_find_active_sequence = classmethod(find_sequence) Invoice._ncf_electronic_invoicing_enabled = classmethod( lambda cls: True) try: origin = SimpleNamespace( ncf_number='B0100000001', ncf_type='1', payment_method='04', revenue_type='01', isr_withholding_type=None) debit_note = SimpleNamespace( company=SimpleNamespace(rec_name='Solutema'), type='out', ncf_number_modified=None, ncf_type=None, ncf_sequence=None, payment_method=None, revenue_type=None, isr_withholding_type=None) Invoice._ncf_prepare_debit_note_from_origin(origin, debit_note) finally: Invoice._ncf_find_active_sequence = original_find_active_sequence Invoice._ncf_electronic_invoicing_enabled = original_enabled self.assertEqual(debit_note.ncf_number_modified, 'B0100000001') self.assertEqual(debit_note.ncf_type, '33') self.assertEqual(debit_note.ncf_sequence, electronic_sequence) def test_debit_note_preparation_falls_back_to_physical_sequence(self): physical_sequence = SimpleNamespace(ncf_type='3') original_find_active_sequence = Invoice._ncf_find_active_sequence original_enabled = Invoice._ncf_electronic_invoicing_enabled def find_sequence(cls, invoice, ncf_code): return physical_sequence if ncf_code == '3' else None Invoice._ncf_find_active_sequence = classmethod(find_sequence) Invoice._ncf_electronic_invoicing_enabled = classmethod( lambda cls: True) try: origin = SimpleNamespace( ncf_number='B0100000001', ncf_type='1', payment_method='04', revenue_type='01', isr_withholding_type=None) debit_note = SimpleNamespace( company=SimpleNamespace(rec_name='Solutema'), type='out', ncf_number_modified=None, ncf_type=None, ncf_sequence=None, payment_method=None, revenue_type=None, isr_withholding_type=None) Invoice._ncf_prepare_debit_note_from_origin(origin, debit_note) finally: Invoice._ncf_find_active_sequence = original_find_active_sequence Invoice._ncf_electronic_invoicing_enabled = original_enabled self.assertEqual(debit_note.ncf_number_modified, 'B0100000001') self.assertEqual(debit_note.ncf_type, '3') self.assertEqual(debit_note.ncf_sequence, physical_sequence) def test_credit_note_preparation_uses_physical_for_b_origin(self): electronic_sequence = SimpleNamespace(ncf_type='34') physical_sequence = SimpleNamespace(ncf_type='4') original_find_active_sequence = Invoice._ncf_find_active_sequence original_enabled = Invoice._ncf_electronic_invoicing_enabled def find_sequence(cls, invoice, ncf_code): if ncf_code == '34': return electronic_sequence if ncf_code == '4': return physical_sequence return None Invoice._ncf_find_active_sequence = classmethod(find_sequence) Invoice._ncf_electronic_invoicing_enabled = classmethod( lambda cls: True) try: origin = SimpleNamespace( ncf_number='B0100000001', ncf_type='1') credit_note = SimpleNamespace( company=SimpleNamespace(rec_name='Solutema'), type='out', ncf_number_modified=None, ncf_type=None, ncf_sequence=None, payment_method=None, revenue_type=None) Invoice._ncf_prepare_credit_note_from_origin(origin, credit_note) finally: Invoice._ncf_find_active_sequence = original_find_active_sequence Invoice._ncf_electronic_invoicing_enabled = original_enabled self.assertEqual(credit_note.ncf_number_modified, 'B0100000001') self.assertEqual(credit_note.ncf_type, '4') self.assertEqual(credit_note.ncf_sequence, physical_sequence) self.assertEqual(credit_note.payment_method, '06') def test_modified_ncf_validation_applies_to_debit_notes(self): debit_note = SimpleNamespace( ncf_type='3', ncf_number_modified='B0100000001', type='out') Invoice.check_ncf_number_modified(debit_note) def test_fiscal_classification_validation_order_for_customer_invoice(self): calls = [] invoice = SimpleNamespace( type='out', check_revenue_type=lambda: calls.append('revenue_type'), check_expense_type=lambda: calls.append('expense_type'), check_isr_withholding_type=lambda: calls.append( 'isr_withholding_type'), check_payment_method=lambda: calls.append('payment_method')) Invoice._ncf_check_fiscal_classification(invoice) self.assertEqual(calls, [ 'revenue_type', 'isr_withholding_type', 'payment_method', ]) def test_fiscal_classification_validation_order_for_supplier_invoice(self): calls = [] invoice = SimpleNamespace( type='in', check_revenue_type=lambda: calls.append('revenue_type'), check_expense_type=lambda: calls.append('expense_type'), check_isr_withholding_type=lambda: calls.append( 'isr_withholding_type'), check_payment_method=lambda: calls.append('payment_method')) Invoice._ncf_check_fiscal_classification(invoice) self.assertEqual(calls, [ 'expense_type', 'isr_withholding_type', 'payment_method', ])