2026-07-22 21:27:53 -04:00
|
|
|
import datetime
|
|
|
|
|
from decimal import Decimal
|
2026-07-23 18:42:45 -04:00
|
|
|
from pathlib import Path
|
2026-07-22 21:27:53 -04:00
|
|
|
from types import SimpleNamespace
|
|
|
|
|
import unittest
|
|
|
|
|
|
2026-07-22 21:52:32 -04:00
|
|
|
from trytond.exceptions import UserError
|
2026-07-22 21:27:53 -04:00
|
|
|
from trytond.modules.ncf_manager.sequence import Sequence
|
|
|
|
|
from trytond.modules.ncf_manager.invoice import Invoice
|
2026-08-16 14:09:42 -04:00
|
|
|
from trytond.modules.ncf_manager.importer import import_dgii_authorizations
|
2026-07-22 21:27:53 -04:00
|
|
|
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(
|
2026-08-16 13:36:51 -04:00
|
|
|
'account_do', 'do_account_root_en'))
|
2026-07-22 21:27:53 -04:00
|
|
|
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
|
|
|
|
|
|
2026-08-16 14:09:42 -04:00
|
|
|
@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()
|
|
|
|
|
|
2026-07-22 21:27:53 -04:00
|
|
|
@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))
|
|
|
|
|
|
2026-08-16 13:52:53 -04:00
|
|
|
@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)
|
2026-08-16 14:09:42 -04:00
|
|
|
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))
|
2026-08-16 13:52:53 -04:00
|
|
|
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-08-16 14:09:42 -04:00
|
|
|
@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))
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
2026-08-16 14:17:20 -04:00
|
|
|
@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, [])
|
|
|
|
|
|
2026-07-22 21:27:53 -04:00
|
|
|
|
|
|
|
|
del ModuleTestCase
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class NcfManagerUnitTestCase(unittest.TestCase):
|
|
|
|
|
|
2026-07-23 18:42:45 -04:00
|
|
|
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)
|
2026-08-16 13:36:51 -04:00
|
|
|
self.assertEqual(template.count('if test="invoice.tip'), 2)
|
|
|
|
|
self.assertEqual(template.count('format_currency(invoice.tip'), 2)
|
2026-07-23 18:42:45 -04:00
|
|
|
|
2026-07-22 21:27:53 -04:00
|
|
|
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)
|
|
|
|
|
|
2026-08-16 13:36:51 -04:00
|
|
|
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)
|
|
|
|
|
|
2026-07-22 21:52:32 -04:00
|
|
|
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)
|
|
|
|
|
|
2026-07-22 21:27:53 -04:00
|
|
|
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)
|
|
|
|
|
|
2026-08-08 22:20:31 -04:00
|
|
|
def test_credit_note_preparation_uses_physical_for_b_origin(self):
|
2026-07-22 21:27:53 -04:00
|
|
|
electronic_sequence = SimpleNamespace(ncf_type='34')
|
2026-08-08 22:20:31 -04:00
|
|
|
physical_sequence = SimpleNamespace(ncf_type='4')
|
2026-07-22 21:27:53 -04:00
|
|
|
original_find_active_sequence = Invoice._ncf_find_active_sequence
|
|
|
|
|
original_enabled = Invoice._ncf_electronic_invoicing_enabled
|
|
|
|
|
|
|
|
|
|
def find_sequence(cls, invoice, ncf_code):
|
2026-08-08 22:20:31 -04:00
|
|
|
if ncf_code == '34':
|
|
|
|
|
return electronic_sequence
|
|
|
|
|
if ncf_code == '4':
|
|
|
|
|
return physical_sequence
|
|
|
|
|
return None
|
2026-07-22 21:27:53 -04:00
|
|
|
|
|
|
|
|
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')
|
2026-08-08 22:20:31 -04:00
|
|
|
self.assertEqual(credit_note.ncf_type, '4')
|
|
|
|
|
self.assertEqual(credit_note.ncf_sequence, physical_sequence)
|
2026-07-22 21:27:53 -04:00
|
|
|
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',
|
|
|
|
|
])
|