Skip to content

Commit b2acf40

Browse files
[IMP] l10n_br_fiscal_dfe: DFe implementation
1 parent be49e28 commit b2acf40

19 files changed

+918
-222
lines changed

l10n_br_fiscal_dfe/README.rst

+8-4
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,15 @@ Authors
5656
Contributors
5757
------------
5858

59-
- `KMEE <https://www.kmee.com.br>`__:
59+
- `KMEE <https://www.kmee.com.br>`__:
6060

61-
- Luis Felipe Miléo <mileo@kmee.com.br>
62-
- Gabriel Cardoso <gabriel.cardoso@kmee.com.br>
63-
- Felipe Zago <felipe.zago@kmee.com.br>
61+
- Luis Felipe Miléo <mileo@kmee.com.br>
62+
- Gabriel Cardoso <gabriel.cardoso@kmee.com.br>
63+
- Felipe Zago <felipe.zago@kmee.com.br>
64+
65+
- `Engenere <https://engenere.one>`__:
66+
67+
- Cristiano Mafra Junior
6468

6569
Maintainers
6670
-----------

l10n_br_fiscal_dfe/__manifest__.py

+6
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@
1010
"website": "https://github.com/OCA/l10n-brazil",
1111
"depends": ["l10n_br_fiscal", "l10n_br_fiscal_certificate"],
1212
"data": [
13+
# Data
1314
"data/ir_cron.xml",
15+
# Security
16+
"security/dfe_security.xml",
1417
"security/ir.model.access.csv",
18+
# Views
19+
"views/dfe/dfe_monitor_views.xml",
1520
"views/dfe/dfe_views.xml",
21+
"views/dfe/dfe_access_key.xml",
1622
"views/l10n_br_fiscal_menu.xml",
1723
"views/res_company_view.xml",
1824
],

l10n_br_fiscal_dfe/constants/dfe.py

+11
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,14 @@
88
DFE_ENVIRONMENTS = [("1", "Produção"), ("2", "Homologação")]
99

1010
DFE_ENVIRONMENT_DEFAULT = "2"
11+
12+
OP_TYPE_ENTRADA = ("0", "Entrada")
13+
OP_TYPE_SAIDA = ("1", "Saída")
14+
15+
OPERATION_TYPE = [OP_TYPE_ENTRADA, OP_TYPE_SAIDA]
16+
17+
18+
SIT_NFE_AUTORIZADA = ("1", "Autorizada")
19+
SIT_NFE_CANCELADA = ("2", "Cancelada")
20+
SIT_NFE_DENEGADA = ("3", "Denegada")
21+
SITUACAO_NFE = [SIT_NFE_AUTORIZADA, SIT_NFE_CANCELADA, SIT_NFE_DENEGADA]

l10n_br_fiscal_dfe/models/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from . import dfe
2-
from . import document
32
from . import attachment
43
from . import res_company
4+
from . import dfe_monitor
5+
from . import dfe_access_key

l10n_br_fiscal_dfe/models/dfe.py

+116-122
Original file line numberDiff line numberDiff line change
@@ -1,157 +1,151 @@
1-
# Copyright (C) 2023 KMEE Informatica LTDA
2-
# License AGPL-3 or later (http://www.gnu.org/licenses/agpl)
1+
# Copyright (C) 2025-Today - Engenere (<https://engenere.one>).
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
import base64
34

4-
import logging
5-
import re
5+
from odoo import fields, models
66

7-
from erpbrasil.transmissao import TransmissaoSOAP
8-
from nfelib.nfe.ws.edoc_legacy import NFeAdapter as edoc_nfe
9-
from requests import Session
10-
11-
from odoo import _, api, fields, models
12-
13-
from ..tools import utils
14-
15-
_logger = logging.getLogger(__name__)
7+
from ..constants.dfe import (
8+
OPERATION_TYPE,
9+
SITUACAO_NFE,
10+
)
1611

1712

1813
class DFe(models.Model):
1914
_name = "l10n_br_fiscal.dfe"
15+
_description = "DF-e"
2016
_inherit = ["mail.thread", "mail.activity.mixin"]
21-
_description = "Consult DF-e"
2217
_order = "id desc"
2318
_rec_name = "display_name"
2419

25-
display_name = fields.Char(compute="_compute_display_name")
20+
dfe_access_key_id = fields.Many2one(
21+
comodel_name="l10n_br_fiscal.dfe_access_key", string="Chave de Acesso"
22+
)
23+
24+
key = fields.Char(string="Access Key", size=44, related="dfe_access_key_id.key")
2625

27-
company_id = fields.Many2one(comodel_name="res.company", string="Company")
26+
serie = fields.Char(size=3, index=True)
2827

29-
version = fields.Selection(related="company_id.dfe_version")
28+
document_number = fields.Float(index=True, digits=(18, 0))
3029

31-
environment = fields.Selection(related="company_id.dfe_environment")
30+
emitter = fields.Char(size=60)
3231

33-
last_nsu = fields.Char(string="Last NSU", size=25, default="0")
32+
cnpj_cpf = fields.Char(string="CNPJ/CPF", size=18)
3433

35-
last_query = fields.Datetime(string="Last query")
34+
nsu = fields.Char(string="NSU", size=25, index=True)
3635

37-
imported_document_ids = fields.One2many(
38-
comodel_name="l10n_br_fiscal.document",
39-
inverse_name="dfe_id",
40-
string="Imported Documents",
36+
operation_type = fields.Selection(
37+
selection=OPERATION_TYPE,
4138
)
4239

43-
use_cron = fields.Boolean(
44-
default=False,
45-
string="Download new documents automatically",
46-
help="If activated, allows new manifestations to be automatically "
47-
"searched with a Cron",
40+
document_amount = fields.Float(
41+
string="Document Total Value",
42+
readonly=True,
43+
digits=(18, 2),
4844
)
4945

50-
@api.depends("company_id.name", "last_nsu")
51-
def name_get(self):
52-
return self.mapped(lambda d: (d.id, f"{d.company_id.name} - NSU: {d.last_nsu}"))
53-
54-
@api.model
55-
def _get_processor(self):
56-
certificado = self.env.company._get_br_ecertificate()
57-
session = Session()
58-
session.verify = False
59-
return edoc_nfe(
60-
TransmissaoSOAP(certificado, session),
61-
self.company_id.state_id.ibge_code,
62-
versao=self.version,
63-
ambiente=self.environment,
64-
)
46+
ie = fields.Char(string="Inscrição estadual", size=18)
6547

66-
@api.model
67-
def validate_distribution_response(self, result):
68-
valid = False
69-
message = result.resposta.xMotivo
70-
if result.retorno.status_code != 200:
71-
code = result.retorno.status_code
72-
elif result.resposta.cStat != "138":
73-
code = result.resposta.cStat
74-
else:
75-
valid = True
76-
77-
if not valid:
78-
self.message_post(
79-
body=_(
80-
_(
81-
"Error validating document distribution:"
82-
"\n\n%(code)s - %(message)s",
83-
code=code,
84-
message=message,
85-
)
86-
)
87-
)
48+
partner_id = fields.Many2one(
49+
comodel_name="res.partner",
50+
string="Supplier (partner)",
51+
)
8852

89-
return valid
53+
company_id = fields.Many2one(
54+
comodel_name="res.company",
55+
string="Company",
56+
default=lambda self: self.env.company,
57+
readonly=True,
58+
)
9059

91-
@api.model
92-
def _document_distribution(self):
93-
maxNSU = ""
94-
while maxNSU != self.last_nsu:
95-
try:
96-
result = self._get_processor().consultar_distribuicao(
97-
cnpj_cpf=re.sub("[^0-9]", "", self.company_id.cnpj_cpf),
98-
ultimo_nsu=utils.format_nsu(self.last_nsu),
99-
)
100-
except Exception as e:
101-
self.message_post(
102-
body=_("Error on searching documents.\n%(error)s", error=e)
103-
)
104-
break
60+
emission_datetime = fields.Datetime(
61+
string="Emission Date",
62+
index=True,
63+
default=fields.Datetime.now,
64+
)
10565

106-
self.write(
107-
{
108-
"last_nsu": result.resposta.ultNSU,
109-
"last_query": fields.Datetime.now(),
110-
}
111-
)
66+
inclusion_datetime = fields.Datetime(
67+
string="Inclusion Date",
68+
index=True,
69+
default=fields.Datetime.now,
70+
)
11271

113-
if not self.validate_distribution_response(result):
114-
break
72+
inclusion_mode = fields.Char(size=255)
11573

116-
self._process_distribution(result)
74+
document_state = fields.Selection(
75+
selection=SITUACAO_NFE,
76+
index=True,
77+
)
11778

118-
maxNSU = result.resposta.maxNSU
79+
cfop_ids = fields.Many2many(
80+
comodel_name="l10n_br_fiscal.cfop",
81+
string="CFOPs",
82+
)
11983

120-
@api.model
121-
def _process_distribution(self, result):
122-
"""Method to process the distribution data."""
84+
dfe_nfe_document_type = fields.Selection(
85+
selection=[
86+
("dfe_nfe_complete", "NF-e Completa"),
87+
("dfe_nfe_summary", "Resumo da NF-e"),
88+
("dfe_nfe_event", "Evento da NF-e"),
89+
],
90+
string="DFe Document Type",
91+
)
12392

124-
@api.model
125-
def _parse_xml_document(self, document):
126-
schema_type = document.schema.split("_")[0]
127-
method = "parse_%s" % schema_type
128-
if not hasattr(self, method):
129-
return
93+
dfe_monitor_id = fields.Many2one(
94+
comodel_name="l10n_br_fiscal.dfe_monitor",
95+
string="DFe Monitor",
96+
)
13097

131-
xml = utils.parse_gzip_xml(document.valueOf_)
132-
return getattr(self, method)(xml)
98+
attachment_id = fields.Many2one(comodel_name="ir.attachment")
13399

134-
@api.model
135-
def _download_document(self, nfe_key):
136-
try:
137-
result = self._get_processor().consultar_distribuicao(
138-
chave=nfe_key, cnpj_cpf=re.sub("[^0-9]", "", self.company_id.cnpj_cpf)
100+
document_id = fields.Many2one(
101+
comodel_name="l10n_br_fiscal.document",
102+
string="Fiscal Document",
103+
)
104+
105+
def name_get(self):
106+
result = []
107+
for rec in self:
108+
document_type = dict(rec._fields["dfe_nfe_document_type"].selection).get(
109+
rec.dfe_nfe_document_type
139110
)
140-
except Exception as e:
141-
self.message_post(
142-
body=_("Error on searching documents.\n%(error)s", error=e)
111+
result.append(
112+
(
113+
rec.id,
114+
f"{rec.key} - {document_type}",
115+
)
143116
)
144-
return
145-
146-
if not self.validate_distribution_response(result):
147-
return
148-
149-
return result.resposta.loteDistDFeInt.docZip[0]
117+
return result
118+
119+
def create_xml_attachment(self, xml):
120+
file_name = "NFe%s.xml" % self.key
121+
self.attachment_id = self.env["ir.attachment"].create(
122+
{
123+
"name": file_name,
124+
"datas": base64.b64encode(xml),
125+
"store_fname": file_name,
126+
"description": "NFe via Manifesto",
127+
"res_model": self._name,
128+
"res_id": self.id,
129+
}
130+
)
150131

151-
@api.model
152-
def _cron_search_documents(self):
153-
self.search([("use_cron", "=", True)]).search_documents()
132+
def action_download_xml(self):
133+
if len(self) == 1:
134+
return self.download_attachment(self.attachment_id)
154135

155-
def search_documents(self):
156-
for record in self:
157-
record._document_distribution()
136+
compressed_attachment_id = (
137+
self.env["l10n_br_fiscal.attachment"]
138+
.create([])
139+
.build_compressed_attachment(self.mapped("attachment_id"))
140+
)
141+
return self.download_attachment(compressed_attachment_id)
142+
143+
def download_attachment(self, attachment_id):
144+
return {
145+
"type": "ir.actions.act_url",
146+
"url": (
147+
f"/web/content/{attachment_id.id}"
148+
f"/{attachment_id.name}?download=true"
149+
),
150+
"target": "self",
151+
}

0 commit comments

Comments
 (0)