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()