Skip to content

Commit 7cc5356

Browse files
trabalho em desenvolvimento
1 parent be49e28 commit 7cc5356

37 files changed

+1352
-792
lines changed

l10n_br_fiscal/views/cfop_view.xml

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
name="tax_definition_ids"
8888
nolabel="1"
8989
context="{'tree_view_ref': 'l10n_br_fiscal.tax_definition_tree','form_view_ref': 'l10n_br_fiscal.tax_definition_form', 'default_cfop_id': id}"
90+
colspan="2"
9091
/>
9192
</group>
9293
</sheet>

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
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
from . import document
33
from . import attachment
44
from . import res_company
5+
from . import dfe_monitor
6+
from . import dfe_access_key

l10n_br_fiscal_dfe/models/dfe.py

+164-119
Original file line numberDiff line numberDiff line change
@@ -1,157 +1,202 @@
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
4+
from io import BytesIO
35

4-
import logging
5-
import re
6+
from brazilfiscalreport.danfe import Danfe
67

7-
from erpbrasil.transmissao import TransmissaoSOAP
8-
from nfelib.nfe.ws.edoc_legacy import NFeAdapter as edoc_nfe
9-
from requests import Session
8+
from odoo import _, fields, models
9+
from odoo.exceptions import UserError
1010

11-
from odoo import _, api, fields, models
12-
13-
from ..tools import utils
14-
15-
_logger = logging.getLogger(__name__)
11+
from ..constants.dfe import (
12+
OPERATION_TYPE,
13+
SITUACAO_NFE,
14+
)
1615

1716

1817
class DFe(models.Model):
1918
_name = "l10n_br_fiscal.dfe"
19+
_description = "DF-e"
2020
_inherit = ["mail.thread", "mail.activity.mixin"]
21-
_description = "Consult DF-e"
2221
_order = "id desc"
2322
_rec_name = "display_name"
2423

25-
display_name = fields.Char(compute="_compute_display_name")
24+
dfe_access_key_id = fields.Many2one(
25+
comodel_name="l10n_br_fiscal.dfe_access_key", string="Chave de Acesso"
26+
)
2627

27-
company_id = fields.Many2one(comodel_name="res.company", string="Company")
28+
key = fields.Char(string="Access Key", size=44, related="dfe_access_key_id.key")
2829

29-
version = fields.Selection(related="company_id.dfe_version")
30+
serie = fields.Char(size=3, index=True)
3031

31-
environment = fields.Selection(related="company_id.dfe_environment")
32+
number = fields.Float(string="Document Number", index=True, digits=(18, 0))
3233

33-
last_nsu = fields.Char(string="Last NSU", size=25, default="0")
34+
emitter = fields.Char(size=60)
3435

35-
last_query = fields.Datetime(string="Last query")
36+
cnpj_cpf = fields.Char(string="CNPJ/CPF", size=18)
3637

37-
imported_document_ids = fields.One2many(
38-
comodel_name="l10n_br_fiscal.document",
39-
inverse_name="dfe_id",
40-
string="Imported Documents",
38+
nsu = fields.Char(string="NSU", size=25, index=True)
39+
40+
operation_type = fields.Selection(
41+
selection=OPERATION_TYPE,
4142
)
4243

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",
44+
document_value = fields.Float(
45+
string="Document Total Value",
46+
readonly=True,
47+
digits=(18, 2),
4848
)
4949

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-
)
50+
ie = fields.Char(string="Inscrição estadual", size=18)
6551

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-
)
52+
partner_id = fields.Many2one(
53+
comodel_name="res.partner",
54+
string="Supplier (partner)",
55+
)
8856

89-
return valid
57+
company_id = fields.Many2one(
58+
comodel_name="res.company",
59+
string="Company",
60+
default=lambda self: self.env.company,
61+
readonly=True,
62+
)
9063

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
64+
emission_datetime = fields.Datetime(
65+
string="Emission Date",
66+
index=True,
67+
default=fields.Datetime.now,
68+
)
10569

106-
self.write(
107-
{
108-
"last_nsu": result.resposta.ultNSU,
109-
"last_query": fields.Datetime.now(),
110-
}
111-
)
70+
inclusion_datetime = fields.Datetime(
71+
string="Inclusion Date",
72+
index=True,
73+
default=fields.Datetime.now,
74+
)
11275

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

116-
self._process_distribution(result)
78+
document_state = fields.Selection(
79+
selection=SITUACAO_NFE,
80+
index=True,
81+
)
11782

118-
maxNSU = result.resposta.maxNSU
83+
cfop_ids = fields.Many2many(
84+
comodel_name="l10n_br_fiscal.cfop",
85+
string="CFOPs",
86+
)
11987

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

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
97+
dfe_monitor_id = fields.Many2one(
98+
comodel_name="l10n_br_fiscal.dfe_monitor",
99+
string="DFe Monitor",
100+
)
130101

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

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)
104+
document_id = fields.Many2one(
105+
comodel_name="l10n_br_fiscal.document",
106+
string="Fiscal Document",
107+
)
108+
109+
def name_get(self):
110+
result = []
111+
for rec in self:
112+
document_type = dict(rec._fields["dfe_nfe_document_type"].selection).get(
113+
rec.dfe_nfe_document_type
139114
)
115+
result.append(
116+
(
117+
rec.id,
118+
f"{rec.key} - {document_type}",
119+
)
120+
)
121+
return result
122+
123+
def create_xml_attachment(self, xml):
124+
file_name = "NFe%s.xml" % self.key
125+
self.attachment_id = self.env["ir.attachment"].create(
126+
{
127+
"name": file_name,
128+
"datas": base64.b64encode(xml),
129+
"store_fname": file_name,
130+
"description": "NFe via Manifesto",
131+
"res_model": self._name,
132+
"res_id": self.id,
133+
}
134+
)
135+
136+
def action_download_xml(self):
137+
if len(self) == 1:
138+
return self.download_attachment(self.attachment_id)
139+
140+
compressed_attachment_id = (
141+
self.env["l10n_br_fiscal.attachment"]
142+
.create([])
143+
.build_compressed_attachment(self.mapped("attachment_id"))
144+
)
145+
return self.download_attachment(compressed_attachment_id)
146+
147+
def download_attachment(self, attachment_id):
148+
return {
149+
"type": "ir.actions.act_url",
150+
"url": (
151+
f"/web/content/{attachment_id.id}"
152+
f"/{attachment_id.name}?download=true"
153+
),
154+
"target": "self",
155+
}
156+
157+
def import_document(self):
158+
self.ensure_one()
159+
try:
160+
document = self.dfe_monitor_id._download_document(self.key)
161+
document_id = self.dfe_monitor_id._parse_xml_document(document)
140162
except Exception as e:
141163
self.message_post(
142-
body=_("Error on searching documents.\n%(error)s", error=e)
164+
body=_("Error importing document: \n\n %(error)s", error=e)
143165
)
144166
return
167+
if document_id:
168+
document_id = self.id
169+
self.document_id = document_id
170+
171+
def import_document_multi(self):
172+
for rec in self:
173+
rec.import_document()
174+
175+
def make_pdf(self):
176+
if self.dfe_nfe_document_type != "dfe_nfe_complete":
177+
raise UserError(_("Can only generate DANFE when DF-e is complete."))
178+
nfe_xml = base64.b64decode(self.attachment_id.datas)
179+
180+
danfe = Danfe(xml=nfe_xml)
181+
182+
tmpDanfe = BytesIO()
183+
danfe.output(tmpDanfe)
184+
danfe_file = tmpDanfe.getvalue()
185+
tmpDanfe.close()
186+
187+
pdf_attachment = self.env["ir.attachment"].create(
188+
{
189+
"name": "DANFE.pdf",
190+
"type": "binary",
191+
"datas": base64.b64encode(danfe_file),
192+
"res_model": self._name,
193+
"res_id": self.id,
194+
"mimetype": "application/pdf",
195+
}
196+
)
145197

146-
if not self.validate_distribution_response(result):
147-
return
148-
149-
return result.resposta.loteDistDFeInt.docZip[0]
150-
151-
@api.model
152-
def _cron_search_documents(self):
153-
self.search([("use_cron", "=", True)]).search_documents()
154-
155-
def search_documents(self):
156-
for record in self:
157-
record._document_distribution()
198+
return {
199+
"type": "ir.actions.act_url",
200+
"url": f"/web/content/{pdf_attachment.id}?download=true",
201+
"target": "self",
202+
}

0 commit comments

Comments
 (0)