Files
José Arturo García c83cf048ea
Publicar paquete PyPI / publish (push) Successful in 1m42s
Allow closing protected legacy NCF ranges
2026-08-27 10:49:35 -04:00

825 lines
34 KiB
Python

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)
@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',
])