Files
ncf_manager/tests/test_module.py
T

878 lines
36 KiB
Python
Raw Normal View History

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.modules.ncf_manager.importer import import_dgii_authorizations
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
def _create_active_e31_range(self, company, invoice_type,
from_number=1, until_number=100):
pool = Pool()
Manager = pool.get('ncf.manager')
ManagerSequence = pool.get('ncf.manager.sequence')
ModelData = pool.get('ir.model.data')
StrictSequence = pool.get('ir.sequence.strict')
SequenceType = pool.get('ir.sequence.type')
today = datetime.date.today()
sequence_type = SequenceType(ModelData.get_id(
'ncf_manager', 'sequence_type_out_invoice_31'))
strict_sequence, = StrictSequence.create([{
'name': f'E31 {invoice_type}',
'sequence_type': sequence_type.id,
'company': company.id,
'prefix': 'E31',
'padding': 10,
'number_next': from_number,
'number_increment': 1,
'from_number': from_number,
'until_number': until_number,
'expiration_date': datetime.date(today.year + 1, 12, 31),
}])
manager, = Manager.create([{
'company': company.id,
'app_date': today,
'app_number': 'APP0000001',
'auth_number': 'AUT0000001',
'state': 'active',
}])
line, = ManagerSequence.create([{
'pos': manager.id,
'ncf_type': '31',
'ncf_sequences': strict_sequence.id,
'invoice_type': invoice_type,
'state': 'active',
}])
return line
@staticmethod
def _dgii_xml(ncf_type='31', authorization='6005423851',
start='E310000000001', end='E310000000100', quantity='100'):
return f'''<SolicitudNCF>
<Solicitud_6009912073 xmlns="http://tempuri.org/dsCFiscales.xsd">
<NUM_SOL>6009912073</NUM_SOL>
<NUM_AUTOR>{authorization}</NUM_AUTOR>
<FECHA_AUTOR>2026-08-07T13:45:02-04:00</FECHA_AUTOR>
<COMP>FACTURA ELECTRÓNICA</COMP>
<OBSERVACIONES>Empresa NCF</OBSERVACIONES>
<NUMERO_DESDE>{start}</NUMERO_DESDE>
<NUMERO_HASTA>{end}</NUMERO_HASTA>
<CODTIPO>{ncf_type}</CODTIPO>
<FECHA_SOLICITUD>2026-08-07T13:36:23-04:00</FECHA_SOLICITUD>
<ESTATUS>APROBADA</ESTATUS>
<CANTIDAD_SOLICITADA>{quantity}</CANTIDAD_SOLICITADA>
<TIPO_USO>ELECTRÓNICA</TIPO_USO>
<CANTIDAD_APROBADA>{quantity}</CANTIDAD_APROBADA>
<FECHA_VIGENCIA>2027-12-31T00:00:00-04:00</FECHA_VIGENCIA>
<DETALLE_LOG>Autorizada por DGII</DETALLE_LOG>
</Solicitud_6009912073>
</SolicitudNCF>'''.encode()
@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.effective_from_number, 100)
self.assertEqual(line.effective_until_number, 109)
self.assertEqual(line.effective_number_next, 100)
self.assertEqual(
line.effective_expiration_date,
datetime.date(today.year, 12, 31))
self.assertEqual(line.tree_from_number, 100)
self.assertEqual(line.tree_until_number, 109)
self.assertEqual(line.tree_number_next, 100)
self.assertEqual(
line.tree_expiration_date,
datetime.date(today.year, 12, 31))
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)
2026-09-30 22:42:04 -04:00
@with_transaction()
def test_global_fiscal_sequence_can_be_assigned_to_manager(self):
'Global fiscal sequences can be attached to a company authorization'
pool = Pool()
Manager = pool.get('ncf.manager')
ManagerSequence = pool.get('ncf.manager.sequence')
ModelData = pool.get('ir.model.data')
StrictSequence = pool.get('ir.sequence.strict')
SequenceType = pool.get('ir.sequence.type')
today = datetime.date.today()
company = self._create_company()
sequence_type = SequenceType(ModelData.get_id(
'ncf_manager', 'sequence_type_out_invoice_01'))
strict_sequence, = StrictSequence.create([{
'name': 'Factura de Crédito Fiscal',
'sequence_type': sequence_type.id,
'prefix': 'B01',
'padding': 8,
'number_next': 1,
'number_increment': 1,
'from_number': 1,
'until_number': 100,
'expiration_date': datetime.date(today.year, 12, 31),
}])
manager, = Manager.create([{
'company': company.id,
'app_date': today,
'app_number': 'APP0000001',
'auth_number': 'AUT0000001',
}])
line, = ManagerSequence.create([{
'pos': manager.id,
'invoice_type': 'invoice_customer',
'ncf_type': '1',
'sequence_mode': 'existing',
'ncf_sequences': strict_sequence.id,
}])
self.assertEqual(line.ncf_sequences, strict_sequence)
@with_transaction()
def test_manual_sequence_creation_defaults_to_current_company(self):
'Manual sequence creation uses the current company from context'
StrictSequence = Pool().get('ir.sequence.strict')
company = self._create_company()
with Transaction().set_context(company=company.id):
defaults = StrictSequence.default_get(['company'])
self.assertEqual(defaults['company'], company.id)
@with_transaction()
def test_overlap_is_independent_by_invoice_direction(self):
'A supplier range must not block the company outgoing authorization'
pool = Pool()
Manager = pool.get('ncf.manager')
company = self._create_company()
today = datetime.date.today()
supplier_line = self._create_active_e31_range(
company, 'invoice_supplier', until_number=2_000_000_000)
manager, = Manager.create([{
'company': company.id,
'app_date': today,
'app_number': '6009912073',
'auth_number': '6005423851',
'ncf_sequences': [('create', [{
'invoice_type': 'invoice_customer',
'ncf_type': '31',
'sequence_mode': 'create',
'from_number': 1,
'number_next': 1,
'until_number': 100,
'expiration_date': datetime.date(
today.year + 1, 12, 31),
}])],
}])
Manager.activate([manager])
manager = Manager(manager.id)
self.assertEqual(manager.state, 'active')
self.assertEqual(manager.ncf_sequences[0].state, 'active')
self.assertEqual(supplier_line.state, 'active')
@with_transaction()
def test_overlap_still_blocks_same_invoice_direction(self):
'Two outgoing authorizations may not reserve the same E31 numbers'
pool = Pool()
Manager = pool.get('ncf.manager')
company = self._create_company()
today = datetime.date.today()
self._create_active_e31_range(
company, 'invoice_customer', from_number=1, until_number=100)
manager, = Manager.create([{
'company': company.id,
'app_date': today,
'app_number': '6009912073',
'auth_number': '6005423851',
'ncf_sequences': [('create', [{
'invoice_type': 'invoice_customer',
'ncf_type': '31',
'sequence_mode': 'create',
'from_number': 50,
'number_next': 50,
'until_number': 150,
'expiration_date': datetime.date(
today.year + 1, 12, 31),
}])],
}])
with self.assertRaises(UserError):
Manager.activate([manager])
@with_transaction()
def test_finish_preserves_bound_legacy_migration_sequence(self):
'Finishing must not invalidate an immutable historical sequence'
pool = Pool()
Manager = pool.get('ncf.manager')
StrictSequence = pool.get('ir.sequence.strict')
company = self._create_company()
line = self._create_ncf_sequence(company)
strict_sequence = line.ncf_sequences
StrictSequence.write([strict_sequence], {
'prefix': 'MIG1',
'padding': 9,
})
Manager.finish([line.pos])
line = line.__class__(line.id)
self.assertEqual(line.state, 'finished')
self.assertEqual(line.ncf_sequences.id, strict_sequence.id)
self.assertEqual(line.ncf_sequences.prefix, 'MIG1')
@with_transaction()
def test_import_dgii_authorization_is_idempotent(self):
pool = Pool()
Manager = pool.get('ncf.manager')
company = self._create_company()
data = self._dgii_xml()
managers, lines = import_dgii_authorizations(
company, data, 'authorization.xml')
self.assertEqual(len(managers), 1)
self.assertEqual(len(lines), 1)
manager = managers[0]
line = lines[0]
self.assertEqual(manager.app_number, '6009912073')
self.assertEqual(line.auth_number, '6005423851')
self.assertEqual(line.ncf_type, '31')
self.assertEqual(line.from_number, 1)
self.assertEqual(line.until_number, 100)
self.assertEqual(line.expiration_date, datetime.date(2027, 12, 31))
self.assertEqual(line.tree_from_number, 1)
self.assertEqual(line.tree_until_number, 100)
self.assertEqual(line.tree_number_next, 1)
self.assertEqual(
line.tree_expiration_date, datetime.date(2027, 12, 31))
managers, lines = import_dgii_authorizations(
company, data, 'authorization-again.xml')
self.assertEqual(managers, [])
self.assertEqual(lines, [])
self.assertEqual(Manager.search_count([
('app_number', '=', '6009912073')]), 1)
@with_transaction()
def test_import_new_authorization_for_active_request(self):
'A new NUM_AUTOR may reuse NUM_SOL without reopening the active one'
pool = Pool()
Manager = pool.get('ncf.manager')
Line = pool.get('ncf.manager.sequence')
company = self._create_company()
first_data = self._dgii_xml()
first_managers, _ = import_dgii_authorizations(
company, first_data, 'authorization-31.xml', activate=True)
first_manager, = first_managers
self.assertEqual(Manager(first_manager.id).state, 'active')
second_data = self._dgii_xml(
ncf_type='34', authorization='6005423852',
start='E340000000001', end='E340000001000', quantity='1000')
second_managers, second_lines = import_dgii_authorizations(
company, second_data, 'authorization-34.xml')
self.assertEqual(len(second_managers), 1)
self.assertEqual(len(second_lines), 1)
self.assertNotEqual(second_managers[0].id, first_manager.id)
self.assertEqual(second_managers[0].state, 'draft')
self.assertEqual(second_lines[0].auth_number, '6005423852')
self.assertEqual(Manager(first_manager.id).state, 'active')
self.assertEqual(Manager.search_count([
('company', '=', company.id),
('app_number', '=', '6009912073'),
]), 2)
self.assertEqual(Line.search_count([
('pos.company', '=', company.id),
('auth_number', 'in', ['6005423851', '6005423852']),
]), 2)
managers, lines = import_dgii_authorizations(
company, second_data, 'authorization-34-again.xml')
self.assertEqual(managers, [])
self.assertEqual(lines, [])
del ModuleTestCase
class NcfManagerUnitTestCase(unittest.TestCase):
@staticmethod
def invoice_with_identifier(code, type=None, code_compact=None):
identifier = SimpleNamespace(code=code, type=type)
if code_compact is not None:
identifier.code_compact = code_compact
return SimpleNamespace(
party=object(), party_tax_identifier=identifier)
def test_id_type_accepts_formatted_rnc(self):
invoice = self.invoice_with_identifier('1-01-01063-2')
self.assertEqual(Invoice.get_id_type(invoice, 'id_type'), 1)
def test_id_type_accepts_formatted_cedula(self):
invoice = self.invoice_with_identifier('001-1391820-5')
self.assertEqual(Invoice.get_id_type(invoice, 'id_type'), 2)
def test_id_type_uses_dominican_identifier_type(self):
rnc = self.invoice_with_identifier('formatted', type='do_vat')
cedula = self.invoice_with_identifier('formatted', type='do_cedula')
self.assertEqual(Invoice.get_id_type(rnc, 'id_type'), 1)
self.assertEqual(Invoice.get_id_type(cedula, 'id_type'), 2)
def test_id_type_normalizes_migrated_compact_code(self):
invoice = self.invoice_with_identifier(
'display value', code_compact='001-1391820-5')
self.assertEqual(Invoice.get_id_type(invoice, 'id_type'), 2)
def test_id_type_returns_none_without_identifier(self):
invoice = SimpleNamespace(party=object(), party_tax_identifier=None)
self.assertIsNone(Invoice.get_id_type(invoice, 'id_type'))
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=&quot;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',
])