diff --git a/README.md b/README.md index 23b1c54..3ffac80 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Python library for generating Brazilian auxiliary fiscal documents in PDF from X - DANFE - Documento Auxiliar da Nota Fiscal Eletrônica (NF-e) - DACCe - Documento Auxiliar da Carta de Correção Eletrônica (CC-e ) - DACTE - Documento Auxiliar do Conhecimento de Transporte Eletrônico (CT-e) +- DAMDFE - Documento Auxiliar do Manifesto Eletrônico de Documentos Fiscais (MDF-e) ## Beta Stage Notice 🚧 @@ -95,6 +96,25 @@ dacte = Dacte(xml=xml_content) dacte.output('dacte.pdf') ``` +### DAMDFE + +```python +from brazilfiscalreport.damdfe import Damdfe + +# Path to the XML file +xml_file_path = 'damdfe.xml' + +# Load XML Content +with open(xml_file_path, "r", encoding="utf8") as file: + xml_content = file.read() + +# Instantiate the DAMDFE object with the loaded XML content +damdfe = Damdfe(xml=xml_content) + +# Save the generated PDF to a file +damdfe.output('damdfe.pdf') +``` + ## Samples 📝 Some sample PDFs generated by our unit tests are available for viewing in the [tests/generated](https://github.com/Engenere/BrazilFiscalReport/tree/main/tests/generated) directory. @@ -125,7 +145,7 @@ Here is a breakdown of all the configuration options available in ``DanfeConfig` ```python config.margins = Margins(top=5, right=5, bottom=5, left=5) ``` - - **Default**: top, right, bottom and left is 2 mm. + - **Default**: top, right, bottom and left is 5 mm. 3. **Receipt Position** - **Type**: ``ReceiptPosition`` (Enum) @@ -253,7 +273,7 @@ Here is a breakdown of all the configuration options available in ``DanfeConfig` ```python config.margins = Margins(top=5, right=5, bottom=5, left=5) ``` - - **Default**: top, right, bottom and left is 2 mm. + - **Default**: top, right, bottom and left is 5 mm. 3. **Font Type** - **Type**: ``FontType`` (Enum) @@ -296,6 +316,75 @@ dacte = Dacte(xml_content, config=config) dacte.output('output_dacte.pdf') ``` +## Customizing DAMDFE + +This section describes how to customize the PDF output of the DAMDFE using the DamdfeConfig class. You can adjust various settings such as margins, fonts, and tax configurations according to your needs. + +### Configuration Options + +Here is a breakdown of all the configuration options available in ``DamdfeConfig``: + +1. **Logo** + - **Type**: ``str``, ``BytesIO``, or ``bytes`` + - **Description**: Path to the logo file or binary image data to be included in the PDF. You can use a file path string or pass image data directly. + - **Example**: + ```python + config.logo = "path/to/logo.jpg" # Using a file path + ``` + - **Default**: No logo. + + +2. **Margins** + - **Type**: ``Margins`` + - **Fields**: ``top``, ``right``, ``bottom``, ``left`` (all of type ``Number``) + - **Description**: Sets the page margins for the PDF document. + - **Example**: + ```python + config.margins = Margins(top=10, right=10, bottom=10, left=10) + ``` + - **Default**: top, right, bottom and left is 5 mm. + +3. **Font Type** + - **Type**: ``FontType`` (Enum) + - **Values**: ``COURIER``, ``TIMES`` + - **Description**: Font style used throughout the PDF document. + - **Example**:: + ```python + config.font_type = FontType.COURIER + ``` + - **Default**: ``TIMES`` + +### Usage Example with Customization + +Here’s how to set up a DamdfeConfig object with a full set of customizations: + +```python +from brazilfiscalreport.damdfe import ( + Damdfe, + DacteConfig, + FontType, + Margins, +) + +# Path to the XML file +xml_file_path = 'mdf-e.xml' + +# Load XML Content +with open(xml_file_path, "r", encoding="utf8") as file: + xml_content = file.read() + +# Create a configuration instance +config = DamdfeConfig( + logo='path/to/logo.png', + margins=Margins(top=10, right=10, bottom=10, left=10), + font_type=FontType.TIMES +) + +# Use this config when creating a Damdfe instance +damdfe = Damdfe(xml_content, config=config) +damdfe.output('output_dacte.pdf') +``` + ## Credits 🙌 This is a fork of the [nfe_utils](https://github.com/edsonbernar/nfe_utils) project, originally created by [Edson Bernardino](https://github.com/edsonbernar). diff --git a/brazilfiscalreport/dacte/dacte.py b/brazilfiscalreport/dacte/dacte.py index c7bef3a..6cac0c5 100644 --- a/brazilfiscalreport/dacte/dacte.py +++ b/brazilfiscalreport/dacte/dacte.py @@ -512,11 +512,9 @@ def _draw_header(self): qr_code = extract_text(self.inf_cte_supl, "qrCodCTe") x_offset = 88 # Ajuste se necessário y_offset = 32 # Ajuste se necessário - width = 40 - height = 40 # Chamada correta para o método - draw_qr_code(self, qr_code, y_margin_ret, x_offset, y_offset, width, height) + draw_qr_code(self, qr_code, y_margin_ret, x_offset, y_offset, box_size=38) def _draw_recipient_sender(self, config): self.mun_ini = extract_text(self.ide, "xMunIni") diff --git a/brazilfiscalreport/dacte/generate_qrcode.py b/brazilfiscalreport/dacte/generate_qrcode.py index 2747ad5..92df22e 100644 --- a/brazilfiscalreport/dacte/generate_qrcode.py +++ b/brazilfiscalreport/dacte/generate_qrcode.py @@ -1,12 +1,14 @@ import qrcode -def draw_qr_code(self, qr_code_data, y_margin_ret, x_offset, y_offset, width, height): +def draw_qr_code( + self, qr_code_data, y_margin_ret, x_offset, y_offset, box_size=10, border=1 +): qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=1, + box_size=box_size, + border=border, ) qr.add_data(qr_code_data) qr.make(fit=True) @@ -17,4 +19,4 @@ def draw_qr_code(self, qr_code_data, y_margin_ret, x_offset, y_offset, width, he num_x = y_margin_ret + x_offset num_y = self.t_margin + y_offset - self.image(qr_img_bytes, x=num_x + 1, y=num_y + 1, w=width - 2, h=height - 2) + self.image(qr_img_bytes, x=num_x + 1, y=num_y + 1, w=box_size, h=box_size) diff --git a/brazilfiscalreport/damdfe/__init__.py b/brazilfiscalreport/damdfe/__init__.py new file mode 100644 index 0000000..3b29abc --- /dev/null +++ b/brazilfiscalreport/damdfe/__init__.py @@ -0,0 +1,10 @@ +from .config import DamdfeConfig, DecimalConfig, FontType, Margins +from .damdfe import Damdfe + +__all__ = [ + "Damdfe", + "DamdfeConfig", + "DecimalConfig", + "FontType", + "Margins", +] diff --git a/brazilfiscalreport/damdfe/config.py b/brazilfiscalreport/damdfe/config.py new file mode 100644 index 0000000..3adc08f --- /dev/null +++ b/brazilfiscalreport/damdfe/config.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass, field +from enum import Enum +from io import BytesIO +from numbers import Number +from typing import Union + + +class FontType(Enum): + COURIER = "Courier" + TIMES = "Times" + + +@dataclass +class Margins: + top: Number = 5 + right: Number = 5 + bottom: Number = 5 + left: Number = 5 + + +@dataclass +class DecimalConfig: + price_precision: int = 4 + quantity_precision: int = 4 + + +@dataclass +class DamdfeConfig: + logo: Union[str, BytesIO, bytes] = None + margins: Margins = field(default_factory=Margins) + decimal_config: DecimalConfig = field(default_factory=DecimalConfig) + font_type: FontType = FontType.TIMES diff --git a/brazilfiscalreport/damdfe/damdfe.py b/brazilfiscalreport/damdfe/damdfe.py new file mode 100644 index 0000000..b3b6340 --- /dev/null +++ b/brazilfiscalreport/damdfe/damdfe.py @@ -0,0 +1,1076 @@ +# Copyright (C) 2024 Engenere - Cristiano Mafra Junior + +import xml.etree.ElementTree as ET +from io import BytesIO +from xml.etree.ElementTree import Element + +from barcode import Code128 +from barcode.writer import SVGWriter + +from ..dacte.generate_qrcode import draw_qr_code +from ..utils import ( + format_cep, + format_cpf_cnpj, + format_number, + format_phone, + get_date_utc, + get_tag_text, +) +from ..xfpdf import xFPDF +from .config import DamdfeConfig +from .damdfe_conf import TP_AMBIENTE, TP_EMISSAO, TP_EMITENTE, URL + + +def extract_text(node: Element, tag: str) -> str: + return get_tag_text(node, URL, tag) + + +class Damdfe(xFPDF): + def __init__(self, xml, config: DamdfeConfig = None): + super().__init__(unit="mm", format="A4") + self.config = config if config is not None else DamdfeConfig() + self.set_margins( + left=self.config.margins.left, + top=self.config.margins.top, + right=self.config.margins.right, + ) + self.set_auto_page_break(auto=False, margin=self.config.margins.bottom) + self.set_title("DAMDFE") + self.logo_image = self.config.logo + self.default_font = self.config.font_type.value + self.price_precision = self.config.decimal_config.price_precision + self.quantity_precision = self.config.decimal_config.quantity_precision + + root = ET.fromstring(xml) + self.inf_adic = root.find(f"{URL}infAdic") + self.inf_seg = root.find(f"{URL}infSeg") + self.disp = root.find(f"{URL}disp") + self.inf_mdfe = root.find(f"{URL}infMDFe") + self.prot_mdfe = root.find(f"{URL}protMDFe") + self.emit = root.find(f"{URL}emit") + self.ide = root.find(f"{URL}ide") + self.inf_modal = root.find(f"{URL}infModal") + self.inf_doc = root.find(f"{URL}infDoc") + self.inf_mun_descarga = root.find(f"{URL}infMunDescarga") + self.tot = root.find(f"{URL}tot") + self.inf_mdfe_supl = root.find(f"{URL}infMDFeSupl") + self.key_mdfe = self.inf_mdfe.attrib.get("Id")[4:] + self.protocol = extract_text(self.prot_mdfe, "nProt") + self.dh_recebto, self.hr_recebto = get_date_utc( + extract_text(self.prot_mdfe, "dhRecbto") + ) + self.add_page(orientation="P") + self._draw_void_watermark() + self._draw_header() + self._draw_body_info() + self._draw_voucher_information() + self._draw_insurance_information() + + def _build_chnfe_str(self): + self.chNFe_str = [] + for chnfe in self.inf_mun_descarga: + chNFe_value = extract_text(chnfe, "chNFe") + if chNFe_value: + self.chNFe_str.append(chNFe_value) + return self.chNFe_str + + def _build_percurso_str(self): + self.percurso_str = "" + for per in self.ide: + self.per = extract_text(per, "UFPer") + if self.percurso_str: + self.percurso_str += " / " + self.percurso_str += self.per + # Remove a barra extra no final + if self.percurso_str.endswith(" / "): + self.percurso_str = self.percurso_str[:-3] + return self.percurso_str + + def _draw_void_watermark(self): + """ + Draw a watermark on the DAMDFE when the protocol is not available or + when the environment is homologation. + """ + is_production_environment = extract_text(self.ide, "tpAmb") == "1" + is_protocol_available = bool(self.prot_mdfe) + + # Exit early if no watermark is needed + if is_production_environment and is_protocol_available: + return + + self.set_font(self.default_font, "B", 60) + watermark_text = "SEM VALOR FISCAL" + width = self.get_string_width(watermark_text) + self.set_text_color(r=220, g=150, b=150) + height = 15 + page_width = self.w + page_height = self.h + x_center = (page_width - width) / 2 + y_center = (page_height + height) / 2 + with self.rotation(55, x_center + (width / 2), y_center - (height / 2)): + self.text(x_center, y_center, watermark_text) + self.set_text_color(r=0, g=0, b=0) + + def draw_vertical_lines_left(self, start_y, end_y, num_lines=None): + half_page_width = self.epw / 2 - 0.25 + col_width = half_page_width / num_lines + for i in range(1, num_lines + 1): + x_line = self.l_margin + i * col_width + self.line(x1=x_line, y1=start_y, x2=x_line, y2=end_y) + + def draw_vertical_lines_right(self, start_y, end_y, num_lines=None): + half_page_width = self.epw / 2 - 0.25 + col_width = half_page_width / num_lines + start_x = self.l_margin + half_page_width + for i in range(1, num_lines + 1): + x_line = start_x + i * col_width + self.line(x1=x_line, y1=start_y, x2=x_line, y2=end_y) + + def draw_vertical_lines(self, x_start_positions, y_start, y_end, x_margin): + """ + Vertical Lines - Method Responsible + for the vertical lines in the information section of the DAMDFE + """ + for x in x_start_positions: + self.line(x1=x_margin + x, y1=y_start, x2=x_margin + x, y2=y_end) + + def _draw_header(self): + x_margin = self.l_margin + y_margin = self.y + page_width = self.epw + + self.model = extract_text(self.ide, "mod") + self.serie = extract_text(self.ide, "serie") + self.n_mdf = extract_text(self.ide, "nMDF") + self.dt, self.hr = get_date_utc(extract_text(self.ide, "dhEmi")) + self.uf_carreg = extract_text(self.ide, "UFIni") + self.uf_descarreg = extract_text(self.ide, "UFFim") + self.tp_emi = TP_EMISSAO[extract_text(self.ide, "tpEmis")] + self.dt_inicio, self.hr_inicio = get_date_utc( + extract_text(self.ide, "dhIniViagem") + ) + self.tp_emit = TP_EMITENTE[extract_text(self.ide, "tpEmit")] + self.tp_amb = TP_AMBIENTE[extract_text(self.prot_mdfe, "tpAmb")] + + cep = format_cep(extract_text(self.emit, "CEP")) + fone = format_phone(extract_text(self.emit, "fone")) + emit_info = ( + f"{extract_text(self.emit, 'xNome')}\n" + f"{extract_text(self.emit, 'xLgr')} " + f"{extract_text(self.emit, 'nro')}\n" + f"{extract_text(self.emit, 'xBairro')} " + f"{cep}\n" + f"{extract_text(self.emit, 'xMun')} - " + f"{extract_text(self.emit, 'UF')}\n" + f"CNPJ:{extract_text(self.emit, 'CNPJ')} " + f"IE:{extract_text(self.emit, 'IE')}\n" + f"RNTRC:{extract_text(self.inf_modal, 'RNTRC')} " + f"TELEFONE:{fone}" + ) + + self.set_dash_pattern(dash=0, gap=0) + self.set_font(self.default_font, "", 7) + self.rect(x=x_margin, y=y_margin, w=page_width - 0.5, h=88, style="") + h_logo = 18 + w_logo = 18 + y_logo = y_margin + if self.logo_image: + self.image( + name=self.logo_image, + x=x_margin + 2, + y=y_logo + 2, + w=w_logo + 2, + h=h_logo + 2, + keep_aspect_ratio=True, + ) + self.set_xy(x=x_margin + 25, y=y_margin + 5) + self.multi_cell(w=60, h=3, text=emit_info, border=0, align="L") + + x_middle = x_margin + (page_width - 0.5) / 2 + self.line(x_middle, y_margin, x_middle, y_margin + 88) + + y_middle = y_margin + 25 + self.line(x_margin, y_middle, x_middle, y_middle) # Aqui + self.set_xy(x=x_margin, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="DAMDFE - Documento Auxiliar do " + "Manifesto de Documentos Fiscais Eletrônicos", + border=0, + align="C", + ) + + y_middle = y_margin + 28 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + self.set_font(self.default_font, "", 6) + + self.draw_vertical_lines( + x_start_positions=[13, 21, 32, 38, 58, 73], + y_start=y_middle, + y_end=y_middle + 7, + x_margin=x_margin, + ) + + # Informações do DAMDF + # Modelo + self.set_xy(x=x_margin + 1, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="MODELO", + border=0, + align="L", + ) + self.set_xy(x=x_margin + 4, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.model, + border=0, + align="L", + ) + # Série + self.set_xy(x=x_margin + 13, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="SÉRIE", + border=0, + align="L", + ) + self.set_xy(x=x_margin + 15, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.serie, + border=0, + align="L", + ) + # Número + self.set_xy(x=x_margin + 21, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="NÚMERO", + border=0, + align="L", + ) + self.set_xy(x=x_margin + 24, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.n_mdf, + border=0, + align="L", + ) + + # FL + self.set_xy(x=x_margin + 33, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="FL", + border=0, + align="L", + ) + # Teste + self.set_xy(x=x_margin + 33, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text="1/1", + border=0, + align="L", + ) + + # DATA E HORA DE EMISSÃO + self.set_xy(x=x_margin + 39, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="DATA E HORA", + border=0, + align="L", + ) + self.set_xy(x=x_margin + 38, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=f"{self.dt} {self.hr}", + border=0, + align="L", + ) + + # UF CARREG + self.set_xy(x=x_margin + 59, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="UF CARREG", + border=0, + align="L", + ) + self.set_xy(x=x_margin + 63, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.uf_carreg, + border=0, + align="L", + ) + + # UF DESCARREG + self.set_xy(x=x_margin + 77, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="UF DESCARREG", + border=0, + align="L", + ) + self.set_xy(x=x_margin + 84, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.uf_descarreg, + border=0, + align="L", + ) + + # QR_CODE + qr_code = extract_text(self.inf_mdfe_supl, "qrCodMDFe") + + num_x = 140 + num_y = 1 + draw_qr_code(self, qr_code, 0, num_x, num_y, box_size=25, border=3) + + svg_img_bytes = BytesIO() + w_options = { + "module_width": 0.3, + } + Code128(self.key_mdfe, writer=SVGWriter()).write( + fp=svg_img_bytes, + options=w_options, + text="", + ) + self.set_font(self.default_font, "", 6.5) + self.set_xy(x=x_margin + 100, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="CONTROLE DO FISCO", + border=0, + align="L", + ) + margins_offset = {1: 8, 2: 8, 3: 7, 4: 7, 5: 6, 6: 6, 7: 5.5, 8: 5, 9: 4, 10: 4} + x_offset = margins_offset.get(self.config.margins.right) + self.image( + svg_img_bytes, x=x_middle + x_offset, y=self.t_margin + 32, w=86.18, h=17.0 + ) + + self.set_font(self.default_font, "", 6.5) + self.set_xy(x=x_middle + 25, y=y_middle + 23) + self.multi_cell( + w=100, + h=3, + text="Consulta em https://dfe-portal.svrs.rs.gov.br/MDFE/Consulta", + border=0, + align="L", + ) + + self.set_font(self.default_font, "B", 7) + self.set_xy(x=x_middle + 25, y=y_middle + 28) + self.multi_cell( + w=100, + h=3, + text=self.key_mdfe, + border=0, + align="L", + ) + + self.set_font(self.default_font, "B", 6) + self.set_xy(x=x_middle + 28, y=y_middle + 32) + self.multi_cell( + w=100, + h=3, + text="PROTOCOLO DE AUTORIZAÇÃO DE USO", + border=0, + align="L", + ) + + self.set_font(self.default_font, "", 6) + self.set_xy(x=x_middle + 32, y=y_middle + 35) + self.multi_cell( + w=100, + h=3, + text=f"{self.protocol} {self.dh_recebto} {self.hr_recebto}", + border=0, + align="L", + ) + + y_middle = y_margin + 35 + self.line(x_margin, y_middle, x_middle, y_middle) + self.draw_vertical_lines( + x_start_positions=[24, 64], + y_start=y_middle, + y_end=y_middle + 7, + x_margin=x_margin, + ) + + # Informações de Emissão + # FORMA DE EMISSÃO + self.set_xy(x=x_margin, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="FORMA DE EMISSÃO", + border=0, + align="L", + ) + self.set_xy(x=x_margin + 6, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.tp_emi, + border=0, + align="L", + ) + + # PREVISÃO DE INICIO DA VIAGEM + self.set_xy(x=x_margin + 25, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="PREVISÃO DE INICIO DA VIAGEM", + border=0, + align="L", + ) + + self.set_xy(x=x_margin + 32.5, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=f"{self.dt_inicio} {self.hr_inicio}", + border=0, + align="L", + ) + + # INSC. SUFRAMA + self.set_xy(x=x_margin + 73, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="INSC. SUFRAMA", + border=0, + align="L", + ) + + y_middle = y_margin + 42 + self.line(x_margin, y_middle, x_middle, y_middle) + self.draw_vertical_lines( + x_start_positions=[44, 70], + y_start=y_middle, + y_end=y_middle + 8, + x_margin=x_margin, + ) + + # Informações Emitente + # TIPO DO EMITENTE + self.set_xy(x=x_margin + 11, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="TIPO DO EMITENTE", + border=0, + align="L", + ) + + self.set_xy(x=x_margin + 1.5, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.tp_emit, + border=0, + align="L", + ) + + # TIPO DO AMBIENTE + self.set_xy(x=x_margin + 46, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="TIPO DO AMBIENTE", + border=0, + align="L", + ) + + self.set_xy(x=x_margin + 50, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.tp_amb, + border=0, + align="L", + ) + + # CARGA POSTERIOR + self.set_xy(x=x_margin + 73, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="CARGA POSTERIOR", + border=0, + align="L", + ) + + y_middle = y_margin + 50 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + + def _draw_body_info(self): + x_margin = self.l_margin + y_margin = self.y + page_width = self.epw + + x_middle = x_margin + (page_width - 0.5) / 2 + y_middle = y_margin + 10 + self.line(x_margin, y_middle, x_middle, y_middle) + self.set_font(self.default_font, "B", 7) + self.set_xy(x=x_margin - 2, y=y_middle - 2) + self.multi_cell( + w=100, h=0, text="MODAL RODOVIÁRIO DE CARGA", border=0, align="C" + ) + + y_middle = y_margin + 15 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + self.set_xy(x=x_margin - 2, y=y_middle - 2) + self.multi_cell(w=100, h=0, text="INFORMAÇÕES PARA ANTT", border=0, align="C") + self.draw_vertical_lines_left( + start_y=y_margin + 15, end_y=y_margin + 15 + 7, num_lines=4 + ) + self.qtd_nfe = extract_text(self.tot, "qNFe") + self.qtd_cte = extract_text(self.tot, "qCTe") + self.qtd_carga = extract_text(self.tot, "qCarga") + self.valor_carga = format_number(extract_text(self.tot, "vCarga"), precision=2) + self.placa = extract_text(self.inf_modal, "placa") + self.modal_uf = extract_text(self.inf_modal, "UF") + self.rntrc = extract_text(self.inf_modal, "RNTRC") + self.renavam = extract_text(self.inf_modal, "RENAVAM") + self.cpf_condutor = format_cpf_cnpj(extract_text(self.inf_modal, "CPF")) + self.nome_condutor = extract_text(self.inf_modal, "xNome") + # Informações para ANTT + # QTD. CT-e + self.set_font(self.default_font, "", 6.5) + self.set_xy(x=x_margin, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="QTD. CT-e", + border=0, + align="L", + ) + + self.set_xy(x=x_margin, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.qtd_cte, + border=0, + align="L", + ) + + # QTD. NF-e + self.set_xy(x=x_margin + 25, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="QTD. NF-e", + border=0, + align="l", + ) + + self.set_xy(x=x_margin + 25, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.qtd_nfe, + border=0, + align="L", + ) + + # PESO TOTAL + self.set_xy(x=x_margin + 50, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="PESO TOTAL", + border=0, + align="L", + ) + + self.set_xy(x=x_margin + 50, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.qtd_carga, + border=0, + align="L", + ) + + # VALOR TOTAL + self.set_xy(x=x_margin + 75, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="VALOR TOTAL", + border=0, + align="L", + ) + + self.set_xy(x=x_margin + 75, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=f"R$ {self.valor_carga}", + border=0, + align="L", + ) + + y_middle = y_margin + 22 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + + y_middle = y_margin + 26 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + self.set_xy(x=x_margin - 2, y=y_middle - 2) + self.set_font(self.default_font, "B", 7) + self.multi_cell(w=100, h=0, text="VEÍCULOS", border=0, align="C") + self.draw_vertical_lines_left( + start_y=y_margin + 26, end_y=y_margin + 26 + 17, num_lines=4 + ) + + # Informações do Veiculos + # PLACA + self.set_xy(x=x_margin, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="PLACA", + border=0, + align="L", + ) + self.set_font(self.default_font, "", 7) + self.set_xy(x=x_margin, y=y_middle + 4) + self.multi_cell( + w=100, + h=3, + text=self.placa, + border=0, + align="L", + ) + + # UF + self.set_font(self.default_font, "B", 7) + self.set_xy(x=x_margin + 25, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="UF", + border=0, + align="L", + ) + + self.set_font(self.default_font, "", 7) + self.set_xy(x=x_margin + 25, y=y_middle + 4) + self.multi_cell( + w=100, + h=3, + text=self.modal_uf, + border=0, + align="L", + ) + + # RNTRC + self.set_font(self.default_font, "B", 7) + self.set_xy(x=x_margin + 50, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="RNTRC", + border=0, + align="L", + ) + + self.set_font(self.default_font, "", 7) + self.set_xy(x=x_margin + 50, y=y_middle + 4) + self.multi_cell( + w=100, + h=3, + text=self.rntrc, + border=0, + align="L", + ) + + # RENAVAM + self.set_font(self.default_font, "B", 7) + self.set_xy(x=x_margin + 75, y=y_middle) + self.multi_cell( + w=100, + h=3, + text="RENAVAM", + border=0, + align="L", + ) + + self.set_font(self.default_font, "", 7) + self.set_xy(x=x_margin + 75, y=y_middle + 4) + self.multi_cell( + w=100, + h=3, + text=self.renavam, + border=0, + align="L", + ) + + self.set_xy(x=page_width / 2 - 2, y=y_middle - 2) + self.multi_cell(w=100, h=0, text="CONDUTORES", border=0, align="C") + y_middle = y_margin + 29 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + self.draw_vertical_lines_right( + start_y=y_margin + 26, end_y=y_margin + 26 + 17, num_lines=2 + ) + + # Informações do Condutores + # CPF + self.set_xy(x=y_middle + 26, y=y_middle - 2.8) + self.multi_cell( + w=100, + h=3, + text="CPF", + border=0, + align="L", + ) + + self.set_xy(x=y_middle + 26, y=y_middle + 0.5) + self.multi_cell( + w=100, + h=3, + text=self.cpf_condutor, + border=0, + align="L", + ) + + # CONDUTORES + self.set_xy(x=y_middle + 76, y=y_middle - 2.8) + self.multi_cell( + w=100, + h=3, + text="CONDUTORES", + border=0, + align="L", + ) + + self.set_xy(x=y_middle + 76, y=y_middle + 0.5) + self.multi_cell( + w=100, + h=3, + text=self.nome_condutor, + border=0, + align="L", + ) + + y_middle = y_margin + 60 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + + def _draw_voucher_information(self): + x_margin = self.l_margin + y_margin = self.y + page_width = self.epw + + self.mun_descarregamento = extract_text(self.inf_doc, "xMunDescarga") + self.cnpj_forn = extract_text(self.disp, "CNPJForn") + self.cnpj_pag = extract_text(self.disp, "CNPJPg") + self.num_comra = extract_text(self.disp, "nCompra") + self.valor_pedagio = extract_text(self.disp, "vValePed") + self.rect(x=x_margin, y=y_margin + 10.5, w=page_width - 0.5, h=30, style="") + + y_middle = y_margin + 14.5 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + self.set_xy(x=(page_width - 40) / 2, y=y_middle - 2) + self.set_font(self.default_font, "B", 7) + self.multi_cell( + w=100, h=0, text="INFORMAÇÕES DE VALE PEDÁGIO", border=0, align="L" + ) + self.draw_vertical_lines_left( + start_y=y_margin + 14.5, end_y=y_margin + 14.5 + 4, num_lines=2 + ) + self.draw_vertical_lines_right( + start_y=y_margin + 14.5, end_y=y_margin + 14.5 + 4, num_lines=2 + ) + + # Informações de Vale Pedágio + # CPF + self.set_font(self.default_font, "B", 6) + self.set_xy(x=x_margin + 12, y=y_middle + 1) + self.multi_cell( + w=100, + h=3, + text="CNPJ DA FORNECEDORA", + border=0, + align="L", + ) + + # CPF/CNPJ DO RESPONSÁVEL + self.set_xy(x=x_margin + 59, y=y_middle + 1) + self.multi_cell( + w=100, + h=3, + text="CPF/CNPJ DO RESPONSÁVEL", + border=0, + align="L", + ) + + # NÚMERO DO COMPROVANTE + self.set_xy(x=y_middle + 15, y=y_middle + 1) + self.multi_cell( + w=100, + h=3, + text="NÚMERO DO COMPROVANTE", + border=0, + align="L", + ) + + # VALOR DO VALE-PEDÁGIO + self.set_xy(x=y_middle + 67, y=y_middle + 1) + self.multi_cell( + w=100, + h=3, + text="VALOR DO VALE-PEDÁGIO", + border=0, + align="L", + ) + + y_middle = y_margin + 18.5 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + + y_middle = y_margin + 31.5 + self.set_font(self.default_font, "B", 7) + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + self.set_xy(x=(page_width - 18) / 2, y=y_middle - 2) + self.multi_cell(w=100, h=0, text="PERCURSO", border=0, align="L") + self.set_xy(x=x_margin, y=y_middle + 1.5) + self.set_font(self.default_font, "", 6.5) + self.percurso_str = self._build_percurso_str() + self.multi_cell(w=100, h=0, text=self.percurso_str, border=0, align="L") + + y_middle = y_margin + 35.5 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + + y_middle = y_margin + 40.5 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + self.set_xy(x=(page_width - 50) / 2, y=y_middle - 2) + self.set_font(self.default_font, "B", 7) + self.multi_cell( + w=100, h=0, text="INFORMAÇÕES DA COMPOSIÇÃO DA CARGA", border=0, align="L" + ) + self.draw_vertical_lines( + x_start_positions=[30, 92, 125], + y_start=y_middle, + y_end=y_middle + 4, + x_margin=x_margin, + ) + self.set_font(self.default_font, "", 5.5) + # INFORMAÇÕES DA COMPOSIÇÃO DA CARGA + # MUNICÍPIO + self.set_xy(x=x_margin, y=y_middle + 1) + self.multi_cell( + w=100, + h=3, + text="MUNICÍPIO", + border=0, + align="L", + ) + + # Informações dos Docs. Fiscais Vinculados ao Manifesto + self.set_xy(x=x_margin + 30, y=y_middle + 1) + self.multi_cell( + w=100, + h=3, + text="INFORMAÇÕES DOS DOCS. FISCAIS VINCULADOS AO MANIFESTO", + border=0, + align="L", + ) + + # MUNICÍPIO + self.set_xy(x=x_margin + 92, y=y_middle + 1) + self.multi_cell( + w=100, + h=3, + text="MUNICÍPIO", + border=0, + align="L", + ) + + # Informações dos Docs. Fiscais Vinculados ao Manifesto + self.set_xy(x=x_margin + 125, y=y_middle + 1) + self.multi_cell( + w=100, + h=3, + text="INFORMAÇÕES DOS DOCS. FISCAIS VINCULADOS AO MANIFESTO", + border=0, + align="L", + ) + current_y = y_middle + 4 + current_x_left = x_margin + line_height = 4 + num_lines = 0 + self.chNFe_str = self._build_chnfe_str() + for i in range(0, len(self.chNFe_str), 2): + self.set_xy(x=current_x_left, y=current_y) + self.multi_cell( + w=211, + h=line_height, + text=self.mun_descarregamento, + border=0, + align="L", + ) + self.set_xy(x=current_x_left + 30, y=current_y) + self.multi_cell( + w=211, + h=line_height, + text=self.chNFe_str[i], + border=0, + align="L", + ) + if i + 1 < len(self.chNFe_str): + self.set_xy(x=x_margin + 92, y=current_y) + self.multi_cell( + w=211, + h=line_height, + text=self.mun_descarregamento, + border=0, + align="L", + ) + self.set_xy(x=x_margin + 125, y=current_y) + self.multi_cell( + w=211, + h=line_height, + text=self.chNFe_str[i + 1], + border=0, + align="L", + ) + num_lines += 1 + if i + 1 < len(self.chNFe_str): + num_lines += 1 + current_y += line_height + total_height = num_lines * line_height + self.x_margin_rect = 4 + + self.rect( + x=x_margin, + y=y_margin + 40.5, + w=page_width - 0.5, + h=total_height, + style="", + ) + y_middle = y_margin + 44.5 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + + def _draw_insurance_information(self): + x_margin = self.l_margin + y_margin = self.y + page_width = self.epw + + self.fisco = extract_text(self.inf_adic, "infAdFisco") + self.obs = extract_text(self.inf_adic, "infCl") + self.seguradora_nome = extract_text(self.inf_seg, "xSeg") + self.cnpj_segurado = extract_text(self.inf_seg, "CNPJ") + self.n_apol = extract_text(self.inf_seg, "nApol") + self.nome_averbacao = extract_text(self.inf_seg, "nAver") + + self.rect( + x=x_margin, + y=(y_margin - 4) + self.x_margin_rect, + w=page_width - 0.5, + h=44, + style="", + ) + y_middle = y_margin + 4 + self.x_margin_rect + self.line(x_margin, y_middle - 4, x_margin + page_width - 0.5, y_middle - 4) + self.set_xy(x=(page_width - 45) / 2, y=y_middle - 6) + self.set_font(self.default_font, "B", 7) + self.multi_cell( + w=100, h=0, text="INFORMAÇÕES SOBRE OS SEGUROS", border=0, align="L" + ) + self.set_font(self.default_font, "", 6) + self.set_xy(x=x_margin, y=y_middle) + self.multi_cell( + w=100, + h=0, + text=f"NOME: {self.seguradora_nome} CNPJ: {self.cnpj_segurado}", + border=0, + align="L", + ) + self.set_xy(x=x_margin, y=y_middle + 4) + self.multi_cell( + w=100, + h=0, + text=f"APÓLICE: {self.n_apol} AVERBAÇÃO: {self.nome_averbacao}", + border=0, + align="L", + ) + + self.rect( + x=x_margin, + y=y_margin + 36 + self.x_margin_rect, + w=page_width - 0.5, + h=45, + style="", + ) + y_middle = y_margin + 44 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + self.set_xy(x=(page_width - 80) / 2, y=y_middle - 2) + self.set_font(self.default_font, "B", 7) + self.multi_cell( + w=100, + h=0, + text="INFORMAÇÕES COMPLEMENTARES DE INTERESSE DO CONTRIBUINTE", + border=0, + align="L", + ) + self.set_font(self.default_font, "", 6) + self.set_xy(x=x_margin, y=y_middle + 3) + self.multi_cell( + w=100, + h=3, + text=self.obs, + border=0, + align="L", + ) + + self.rect( + x=x_margin, + y=y_margin + 81 + self.x_margin_rect, + w=page_width - 0.5, + h=45, + style="", + ) + y_middle = y_margin + 90 + self.line(x_margin, y_middle, x_margin + page_width - 0.5, y_middle) + self.set_xy(x=(page_width - 65) / 2, y=y_middle - 2) + self.set_font(self.default_font, "B", 7) + self.multi_cell( + w=100, + h=0, + text="INFORMAÇÕES ADICIONAIS DE INTERESSE DO FISCO", + border=0, + align="L", + ) + self.set_font(self.default_font, "", 6) + self.set_xy(x=x_margin, y=y_middle) + self.multi_cell( + w=100, + h=3, + text=self.fisco, + border=0, + align="L", + ) diff --git a/brazilfiscalreport/damdfe/damdfe_conf.py b/brazilfiscalreport/damdfe/damdfe_conf.py new file mode 100644 index 0000000..39ce8ad --- /dev/null +++ b/brazilfiscalreport/damdfe/damdfe_conf.py @@ -0,0 +1,14 @@ +URL = ".//{http://www.portalfiscal.inf.br/mdfe}" + +TP_EMISSAO = {"1": "NORMAL", "2": "CONTINGÊNCIA"} + +TP_EMITENTE = { + "1": "PRESTADOR DE SERVIÇO DE TRANSPORTE", + "2": "TRANSPORTADOR DE CARGA PRÓPRIA", + "3": "PRESTADOR DE SERVIÇO DE TRANSPORTE QUE EMITIRÁ CT-e GLOBALIZADO", +} + +TP_AMBIENTE = { + "1": "PRODUÇÃO", + "2": "HOMOLOGAÇÃO", +} diff --git a/tests/fixtures/mdf-e_test_1.xml b/tests/fixtures/mdf-e_test_1.xml new file mode 100644 index 0000000..a6c2e43 --- /dev/null +++ b/tests/fixtures/mdf-e_test_1.xml @@ -0,0 +1,127 @@ + + + + + + 42 + 2 + 2 + 90 + 1 + 15 + 00000000 + 8 + 1 + 2024-08-09T13:38:07-03:00 + 1 + 0 + SC + SC + + 0090001 + SÃO JOÃO BATISTA + + + SC + + + SC + + 2024-08-11T13:38:07-03:00 + + + 00112233445566 + 9922334455 + EMPRESA FANTASMA + EMPRESA FANTASMA + + RUA TESTE 0011 + 0022 + KM 00 + COBRE + 0099223 + CANELINHA + 88230000 + SC + 48999999999 + teste@gmail.com + + + + + SEGURADO TESTE + 12345678000199 + 12345678901234567890 + 1234567890123456789012345678901234567890 + + + + + + 00000000 + + + RB09832 + 09022129834 + 2100 + 16000 + 37 + + CONDUTOR TESTE + 92309230203 + + 02 + 02 + SC + + + + + + 0909233 + BRUSQUE + + 09429847849344792357634954023782021734928955 + + + 08429847849344792357634954023782021734928955 + + + 09429847849344792357634954023782021734928955 + + + 08429847849344792357634954023782021734928955 + + + + + 2 + 41873.80 + 01 + 7033.1300 + + + 90329384788454 + + + Documento emitido por: Marc Demo + + + + + https://dfe-portal.svrs.rs.gov.br/mdfe/QRCode?chMDFe=42240802582975000109580010000000151908765432&tpAmb=1 + + + + + + 1 + 42240802582975000109580010000000151908765432 + 2024-08-09T13:43:42-03:00 + 942240018699639 + KGdmh/XYMKRySFCfXZat4VFHo7g= + 100 + Autorizado o uso do MDF-e + + + diff --git a/tests/generated/dacte/dacte_default.pdf b/tests/generated/dacte/dacte_default.pdf index 0919a8e..f9a3abf 100644 Binary files a/tests/generated/dacte/dacte_default.pdf and b/tests/generated/dacte/dacte_default.pdf differ diff --git a/tests/generated/dacte/dacte_default_logo.pdf b/tests/generated/dacte/dacte_default_logo.pdf index 9069c6f..2e39dad 100644 Binary files a/tests/generated/dacte/dacte_default_logo.pdf and b/tests/generated/dacte/dacte_default_logo.pdf differ diff --git a/tests/generated/dacte/dacte_multi_pages.pdf b/tests/generated/dacte/dacte_multi_pages.pdf index d592007..61f5da4 100644 Binary files a/tests/generated/dacte/dacte_multi_pages.pdf and b/tests/generated/dacte/dacte_multi_pages.pdf differ diff --git a/tests/generated/dacte/dacte_overload.pdf b/tests/generated/dacte/dacte_overload.pdf index 7b8cddd..19df826 100644 Binary files a/tests/generated/dacte/dacte_overload.pdf and b/tests/generated/dacte/dacte_overload.pdf differ diff --git a/tests/generated/damdfe/damdfe_default.pdf b/tests/generated/damdfe/damdfe_default.pdf new file mode 100644 index 0000000..f859c0a Binary files /dev/null and b/tests/generated/damdfe/damdfe_default.pdf differ diff --git a/tests/generated/damdfe/damdfe_default_logo.pdf b/tests/generated/damdfe/damdfe_default_logo.pdf new file mode 100644 index 0000000..243264c Binary files /dev/null and b/tests/generated/damdfe/damdfe_default_logo.pdf differ diff --git a/tests/generated/damdfe/damdfe_default_logo_margins.pdf b/tests/generated/damdfe/damdfe_default_logo_margins.pdf new file mode 100644 index 0000000..3b433ab Binary files /dev/null and b/tests/generated/damdfe/damdfe_default_logo_margins.pdf differ diff --git a/tests/test_damdfe.py b/tests/test_damdfe.py new file mode 100644 index 0000000..f0367ff --- /dev/null +++ b/tests/test_damdfe.py @@ -0,0 +1,42 @@ +import pytest + +from brazilfiscalreport.damdfe import ( + Damdfe, + DamdfeConfig, + Margins, +) +from tests.conftest import assert_pdf_equal, get_pdf_output_path + + +@pytest.fixture +def load_damdfe(load_xml): + def _load_damdfe(filename, config=None): + xml_content = load_xml(filename) + return Damdfe(xml=xml_content, config=config) + + return _load_damdfe + + +def test_damdfe_default(tmp_path, load_damdfe): + damdfe = load_damdfe("mdf-e_test_1.xml") + pdf_path = get_pdf_output_path("damdfe", "damdfe_default") + assert_pdf_equal(damdfe, pdf_path, tmp_path) + + +def test_damdfe_default_logo(tmp_path, load_damdfe, logo_path): + damdfe_config = DamdfeConfig( + logo=logo_path, + ) + damdfe = load_damdfe("mdf-e_test_1.xml", config=damdfe_config) + pdf_path = get_pdf_output_path("damdfe", "damdfe_default_logo") + assert_pdf_equal(damdfe, pdf_path, tmp_path) + + +def test_damdfe_default_logo_margins(tmp_path, load_damdfe, logo_path): + damdfe_config = DamdfeConfig( + logo=logo_path, + margins=Margins(top=10, right=10, bottom=10, left=10), + ) + damdfe = load_damdfe("mdf-e_test_1.xml", config=damdfe_config) + pdf_path = get_pdf_output_path("damdfe", "damdfe_default_logo_margins") + assert_pdf_equal(damdfe, pdf_path, tmp_path)