diff --git a/l10n_br_fiscal/models/document.py b/l10n_br_fiscal/models/document.py index f89d3ffb1711..f639de980223 100644 --- a/l10n_br_fiscal/models/document.py +++ b/l10n_br_fiscal/models/document.py @@ -291,19 +291,27 @@ def _check_number(self): ("document_number", "=", record.document_number), ] - invalid_number = False - if record.issuer == DOCUMENT_ISSUER_PARTNER: domain.append(("partner_id", "=", record.partner_id.id)) else: if record.document_serie_id: - invalid_number = record.document_serie_id._is_invalid_number( + invalid_number = record.document_serie_id.is_invalid_number( record.document_number ) - documents = record.env["l10n_br_fiscal.document"].search_count(domain) + if invalid_number: + raise ValidationError( + _( + "The %(document_type)s, serie %(serie)s, " + "number %(number)s is invalidated!", + document_type=record.document_type_id.name, + serie=record.document_serie, + number=record.document_number, + ) + ) - if documents or invalid_number: + documents = record.env["l10n_br_fiscal.document"].search_count(domain) + if documents: raise ValidationError( _( "There is already a fiscal document with this " diff --git a/l10n_br_fiscal/models/document_serie.py b/l10n_br_fiscal/models/document_serie.py index 83cea2c71a02..b95375973e8c 100644 --- a/l10n_br_fiscal/models/document_serie.py +++ b/l10n_br_fiscal/models/document_serie.py @@ -81,24 +81,23 @@ def create(self, vals_list): def name_get(self): return [(r.id, f"{r.name}") for r in self] - def _is_invalid_number(self, document_number): + def is_invalid_number(self, document_number): self.ensure_one() - is_invalid_number = True - # TODO Improve this implementation! - invalids = self.env["l10n_br_fiscal.invalidate.number"].search( - [("state", "=", "done"), ("document_serie_id", "=", self.id)] + return self.env["l10n_br_fiscal.invalidate.number"].search( + [ + ("state", "=", "done"), + ("document_type_id", "=", self.document_type_id.id), + ("document_serie_id", "=", self.id), + ("number_start", "<=", document_number), + ("number_end", ">=", document_number), + ], + limit=1, ) - invalid_numbers = [] - for invalid in invalids: - invalid_numbers += range(invalid.number_start, invalid.number_end + 1) - if int(document_number) not in invalid_numbers: - is_invalid_number = False - return is_invalid_number def next_seq_number(self): self.ensure_one() document_number = self.internal_sequence_id._next() - if self._is_invalid_number(document_number) or self.check_number_in_use( + if self.is_invalid_number(document_number) or self.check_number_in_use( document_number ): document_number = self.next_seq_number() diff --git a/l10n_br_fiscal/models/invalidate_number.py b/l10n_br_fiscal/models/invalidate_number.py index fc4486ac18d2..4f0778fa0548 100644 --- a/l10n_br_fiscal/models/invalidate_number.py +++ b/l10n_br_fiscal/models/invalidate_number.py @@ -5,8 +5,6 @@ from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError -from ..constants.fiscal import SITUACAO_EDOC_INUTILIZADA - class InvalidateNumber(models.Model): _name = "l10n_br_fiscal.invalidate.number" @@ -39,7 +37,9 @@ class InvalidateNumber(models.Model): ) document_electronic = fields.Boolean( - related="document_type_id.electronic", string="Electronic?", readonly=True + related="document_type_id.electronic", + string="Electronic?", + readonly=True, ) document_serie_id = fields.Many2one( @@ -117,31 +117,4 @@ def unlink(self): return super().unlink() def action_invalidate(self): - for record in self: - record._invalidate() - - def _create_invalidate_document(self, document_number): - self.env["l10n_br_fiscal.document"].create( - { - "document_serie_id": self.document_serie_id.id, - "document_type_id": self.document_serie_id.document_type_id.id, - "company_id": self.company_id.id, - "state_edoc": SITUACAO_EDOC_INUTILIZADA, - "issuer": "company", - "document_number": str(document_number), - "invalidate_event_id": self.authorization_event_id.id, - } - ) - - def _update_document_status(self, document_id=None): - if document_id: - document_id.state_edoc = SITUACAO_EDOC_INUTILIZADA - document_id.invalidate_event_id = self.authorization_event_id - else: - for document_number in range(self.number_start, self.number_end + 1): - self._create_invalidate_document(document_number) - - def _invalidate(self, document_id=None): - self.ensure_one() - self._update_document_status(document_id) - self.state = "done" + self.write({"state": "done"}) diff --git a/l10n_br_fiscal/security/fiscal_security.xml b/l10n_br_fiscal/security/fiscal_security.xml index 4bf523de5be4..84741a64d341 100644 --- a/l10n_br_fiscal/security/fiscal_security.xml +++ b/l10n_br_fiscal/security/fiscal_security.xml @@ -56,6 +56,13 @@ [('company_id', 'in', company_ids + [False])] + + Fiscal Invalidate Number multi-company + + + [('company_id', 'in', company_ids + [False])] + + Fiscal Document multi-company diff --git a/l10n_br_fiscal/tests/__init__.py b/l10n_br_fiscal/tests/__init__.py index 1f74cd2c8fa8..d02f393a4060 100644 --- a/l10n_br_fiscal/tests/__init__.py +++ b/l10n_br_fiscal/tests/__init__.py @@ -3,6 +3,7 @@ from . import ( test_cnae, test_fiscal_document_generic, + test_fiscal_invalidate_number, test_fiscal_document_nfse, test_fiscal_tax, test_tax_benefit, diff --git a/l10n_br_fiscal/tests/test_fiscal_invalidate_number.py b/l10n_br_fiscal/tests/test_fiscal_invalidate_number.py new file mode 100644 index 000000000000..d89a41eb9353 --- /dev/null +++ b/l10n_br_fiscal/tests/test_fiscal_invalidate_number.py @@ -0,0 +1,56 @@ +# Copyright (C) 2025 Renato Lima - Akretion +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestFiscalInvalidateNumber(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + cls.company = cls.env.ref("l10n_br_base.empresa_simples_nacional") + cls.document_type_nfe = cls.env.ref("l10n_br_fiscal.document_55") + cls.document_serie_nfe = cls.env.ref( + "l10n_br_fiscal.empresa_sn_document_55_serie_1" + ) + + invalidate_number_100_200 = cls.env["l10n_br_fiscal.invalidate.number"].create( + { + "company_id": cls.company.id, + "document_type_id": cls.document_type_nfe.id, + "document_serie_id": cls.document_serie_nfe.id, + "number_start": 100, + "number_end": 200, + "justification": "Just a invalidate numbers test", + } + ) + invalidate_number_100_200.action_invalidate() + + def test_fiscal_invalidate_number_overlap(self): + """Test Invalidate Number Overlap.""" + with self.assertRaises(ValidationError): + self.env["l10n_br_fiscal.invalidate.number"].create( + { + "company_id": self.company.id, + "document_type_id": self.document_type_nfe.id, + "document_serie_id": self.document_serie_nfe.id, + "number_start": 100, + "number_end": 200, + "justification": "Just a invalidate numbers test", + } + ) + + def test_fiscal_document_with_invalidated_number(self): + """Test Fiscal Document with Invalidate Number.""" + with self.assertRaises(ValidationError): + self.env["l10n_br_fiscal.document"].create( + { + "company_id": self.company.id, + "document_type_id": self.document_type_nfe.id, + "document_serie_id": self.document_serie_nfe.id, + "document_number": 150, + } + ) diff --git a/l10n_br_fiscal_edi/wizards/invalidate_number_wizard.py b/l10n_br_fiscal_edi/wizards/invalidate_number_wizard.py index d6fbc9e840a0..99cc80d5cc02 100644 --- a/l10n_br_fiscal_edi/wizards/invalidate_number_wizard.py +++ b/l10n_br_fiscal_edi/wizards/invalidate_number_wizard.py @@ -21,7 +21,7 @@ def do_invalidate(self): "justification": self.justification, } ) - invalidate._invalidate(self.document_id) + invalidate.action_invalidate() if hasattr(self.document_id, "cancel_move_ids"): # cancel moves if l10n_br_account is installed # (thus l10n_br_account doesn't need to depend on l10n_br_fiscal_edi) diff --git a/l10n_br_nfe/models/invalidate_number.py b/l10n_br_nfe/models/invalidate_number.py index fe45271ae024..75d1b863ffed 100644 --- a/l10n_br_nfe/models/invalidate_number.py +++ b/l10n_br_nfe/models/invalidate_number.py @@ -37,7 +37,7 @@ def _edoc_processor(self): return edoc_nfe(**params) - def _invalidate(self, document_id=False): + def action_invalidate(self): processador = self._edoc_processor() evento = processador.inutilizacao( cnpj=punctuation_rm(self.company_id.cnpj_cpf), @@ -62,8 +62,6 @@ def _invalidate(self, document_id=False): invalidate_number_id=self, ) - if document_id: - event_id.document_id = document_id self.event_ids |= event_id self.authorization_event_id = event_id @@ -83,4 +81,4 @@ def _invalidate(self, document_id=False): ) if processo.resposta.infInut.cStat == "102": - return super()._invalidate(document_id) + return super().action_invalidate() diff --git a/l10n_br_nfe/tests/test_nfce.py b/l10n_br_nfe/tests/test_nfce.py index 384a8a9e44ec..17f362245a94 100644 --- a/l10n_br_nfe/tests/test_nfce.py +++ b/l10n_br_nfe/tests/test_nfce.py @@ -15,7 +15,6 @@ SITUACAO_EDOC_AUTORIZADA, SITUACAO_EDOC_CANCELADA, SITUACAO_EDOC_DENEGADA, - SITUACAO_EDOC_INUTILIZADA, SITUACAO_EDOC_REJEITADA, ) @@ -118,7 +117,17 @@ def test_inutilizar(self): ) inutilizar_wizard.doit() - self.assertEqual(self.document_id.state_edoc, SITUACAO_EDOC_INUTILIZADA) + invalidate = self.env["l10n_br_fiscal.invalidate.number"].search( + [ + ("state", "=", "done"), + ("company_id", "=", self.document_id.company_id.id), + ("document_type_id", "=", self.document_id.document_type_id.id), + ("document_serie_id", "=", self.document_id.document_serie_id.id), + ("number_start", "=", self.document_id.document_number), + ("number_end", "=", self.document_id.document_number), + ] + ) + self.assertEqual(len(invalidate), 1) def test_atualiza_status_nfce(self): self.document_id._compute_nfe40_dhSaiEnt()