From c01ab2e54ce8ab6705c422acb086b8679ad86868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Arturo=20Garci=CC=81a?= Date: Thu, 27 Aug 2026 10:37:04 -0400 Subject: [PATCH] Fix NCF overlap validation by document direction --- README.rst | 7 ++- ncf_manager.py | 1 + tests/test_module.py | 104 +++++++++++++++++++++++++++++++++++++++++++ tryton.cfg | 2 +- 4 files changed, 111 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 75affed..288b941 100644 --- a/README.rst +++ b/README.rst @@ -37,8 +37,11 @@ Finished``. Create the authorization and its fiscal lines in draft, then use the **Activate** button. A line can select an existing strict sequence or ask the module to create one on activation from its authorized range. Activation checks company, fiscal type, prefix, padding, validity, availability and range -overlap. The last issued number automatically finishes the line. Finishing -an authorization closes every remaining active line without reusing numbers. +overlap. Incoming supplier documents and outgoing customer documents use +independent numbering authorities, so overlap is enforced only within the +same document direction. The last issued number automatically finishes the +line. Finishing an authorization closes every remaining active line without +reusing numbers. Electronic fiscal numbers are only reserved by this module. Activating an E-series authorization never signs or transmits an e-CF to DGII. diff --git a/ncf_manager.py b/ncf_manager.py index 43678a4..2a07300 100644 --- a/ncf_manager.py +++ b/ncf_manager.py @@ -568,6 +568,7 @@ class NCFManagerSequences(Workflow, ModelSQL, ModelView): ('id', '!=', self.id), ('pos.company', '=', self.pos.company.id), ('ncf_type', '=', self.ncf_type), + ('invoice_type', '=', self.invoice_type), ('state', '=', 'active'), ('ncf_sequences', '!=', None), ]) diff --git a/tests/test_module.py b/tests/test_module.py index 874a0b2..584ef09 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -165,6 +165,46 @@ class NcfManagerTestCase(ModuleTestCase): }]) 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'): @@ -292,6 +332,70 @@ class NcfManagerTestCase(ModuleTestCase): 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_import_dgii_authorization_is_idempotent(self): pool = Pool() diff --git a/tryton.cfg b/tryton.cfg index a211bbb..e10f630 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,5 +1,5 @@ [tryton] -version=8.0.1 +version=8.0.2 depends: account_do account_invoice_do