diff --git a/pytrustnfe/nfe/danfce.py b/pytrustnfe/nfe/danfce.py new file mode 100644 index 0000000..21cd50b --- /dev/null +++ b/pytrustnfe/nfe/danfce.py @@ -0,0 +1,456 @@ +# -*- coding: utf-8 -*- +# © 2017 Johny Chen Jy, Trustcode +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from textwrap import wrap +from io import BytesIO + +from reportlab.lib import utils +from reportlab.pdfgen import canvas +from reportlab.lib.units import cm, mm +from reportlab.graphics.barcode import qr +from reportlab.graphics import renderPDF +from reportlab.graphics.shapes import Drawing +from reportlab.platypus import Table, TableStyle, Paragraph, Image +from reportlab.lib.enums import TA_CENTER +from reportlab.lib.styles import ParagraphStyle + + +def format_cnpj_cpf(value): + if len(value) < 12: # CPF + cValue = '%s.%s.%s-%s' % (value[:-8], value[-8:-5], + value[-5:-2], value[-2:]) + else: + cValue = '%s.%s.%s/%s-%s' % (value[:-12], value[-12:-9], + value[-9:-6], value[-6:-2], value[-2:]) + return cValue + + +def getdateUTC(cDateUTC): + cDt = cDateUTC[0:10].split('-') + cDt.reverse() + return '/'.join(cDt), cDateUTC[11:16] + + +def format_number(cNumber, precision=0, group_sep='.', decimal_sep=','): + if cNumber: + number = float(cNumber) + return ("{:,." + str(precision) + "f}").format(number).\ + replace(",", "X").replace(".", ",").replace("X", ".") + return "" + + +def tagtext(oNode=None, cTag=None): + try: + xpath = ".//{http://www.portalfiscal.inf.br/nfe}%s" % (cTag) + cText = oNode.find(xpath).text + except: + cText = '' + return cText + + +def get_image(path, width=1 * cm): + img = utils.ImageReader(path) + iw, ih = img.getSize() + aspect = ih / float(iw) + return Image(path, width=width, height=(width * aspect)) + + +def format_telefone(numero_telefone): + if len(numero_telefone) == 10: + telefone = '(%s) %s-%s' % (numero_telefone[0:2], + numero_telefone[2:6], + numero_telefone[6:]) + else: + telefone = '(%s) %s-%s' % (numero_telefone[0:2], + numero_telefone[2:7], + numero_telefone[7:]) + return telefone + + +class danfce(object): + + def __init__(self, list_xml, logo=None): + + self.current_font_size = 7 + self.current_font_name = 'NimbusSanL-Regu' + + self.max_height = 840 + self.min_height = 1 + self.min_width = 5 + self.max_width = 200 + self.current_height = 840 + + self.oPDF_IO = BytesIO() + self.canvas = canvas.Canvas(self.oPDF_IO, pagesize=(7.2 * cm, 30 * cm)) + self.canvas.setTitle('DANFCE') + self.canvas.setLineWidth(.5) + self.canvas.setFont(self.current_font_name, self.current_font_size) + + self.list_xml = list_xml + self.logo = logo + + self.nfce_generate() + + def ide_emit(self, oXML=None): + + elem_emit = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}emit") + + # Razão Social emitente + nomeEmpresa = tagtext(oNode=elem_emit, cTag='xFant') + self.drawTitle(nomeEmpresa, 10) + + if self.logo: + img = get_image(self.logo, width=10 * mm) + img.drawOn(self.canvas, 5, 830) + + cEnd = tagtext(oNode=elem_emit, cTag="xNome") + '
' + cEnd += "CNPJ: %s " % (format_cnpj_cpf( + tagtext(oNode=elem_emit, cTag='CNPJ'))) + cEnd += "IE: %s" % (tagtext(oNode=elem_emit, cTag="IE")) + '
' + cEnd += tagtext(oNode=elem_emit, cTag='xLgr') + ', ' + tagtext( + oNode=elem_emit, cTag='nro') + ' - ' + cEnd += tagtext(oNode=elem_emit, cTag='xBairro') + '
' + tagtext( + oNode=elem_emit, cTag='xMun') + ' - ' + cEnd += tagtext(oNode=elem_emit, cTag='UF') + ' - ' + tagtext( + oNode=elem_emit, cTag='CEP') + '
' + cEnd += 'Fone: ' + format_telefone(tagtext( + oNode=elem_emit, cTag='fone')) + + self._drawCenteredParagraph(cEnd) + self.drawLine() + + def danfce_information(self): + self.drawTitle( + "DANFE NFC-e - Documento Auxiliar da Nota Fiscal de", + 7, 'NimbusSanL-Bold') + + self.drawTitle("Consumidor Eletrônica", 7, 'NimbusSanL-Bold') + + self.drawString( + "NFC-e não permite aproveitamento de crédito de ICMS", True) + self.drawLine() + + def produtos(self, oXML=None, el_det=None, oPaginator=None, + list_desc=None, list_cod_prod=None): + + rows = [['Codigo', 'Descricao', 'Qtde', 'Un', 'Vl Unit', 'Vl Total']] + colWidths = (25, 90, 15, 15, 25, 25) + rowHeights = [7] + + for id in range(oPaginator[0], oPaginator[1]): + + item = el_det[id] + el_prod = item.find(".//{http://www.portalfiscal.inf.br/nfe}prod") + + cod = tagtext(oNode=el_prod, cTag='cProd') + descricao = tagtext(oNode=el_prod, cTag='xProd') + Un = tagtext(oNode=el_prod, cTag='uCom') + qtde = format_number(tagtext(oNode=el_prod, cTag='qCom'), + precision=2) + vl_unit = format_number(tagtext(oNode=el_prod, cTag='vUnCom'), + precision=2) + vl_total = format_number( + tagtext(oNode=el_prod, cTag='vProd'), precision=2) + + new_row = [cod, descricao, qtde, Un, vl_unit, vl_total] + + rows.append(new_row) + rowHeights.append(self.current_font_size + 2) + + self._draw_product_table(rows, colWidths, rowHeights) + + def _draw_product_table(self, rows, colWidths, rowHeights): + table = Table(rows, colWidths, tuple(rowHeights)) + table.setStyle(TableStyle([ + ('FONTSIZE', (0, 0), (-1, -1), 7), + ('FONT', (0, 1), (-1, -1), 'NimbusSanL-Regu'), + ('FONT', (0, 0), (-1, 0), 'NimbusSanL-Bold'), + ('ALIGN', (0, 0), (-1, 0), "LEFT"), + ('ALIGN', (1, 0), (-1, 0), "LEFT"), + ('ALIGN', (2, 0), (-1, 0), "CENTER"), + ('ALIGN', (3, 0), (-1, 0), "CENTER"), + ('ALIGN', (0, 1), (-1, -1), "LEFT"), + ('ALIGN', (1, 1), (-1, -1), "LEFT"), + ('ALIGN', (2, 1), (-1, -1), "CENTER"), + ('ALIGN', (3, 1), (-1, -1), "CENTER"), + ])) + + w, h = table.wrapOn(self.canvas, 200, 450) + table.drawOn(self.canvas, 0, self.current_height - (h * 1.2)) + self.current_height -= (h * 1.1) + + def totais(self, oXML=None): + # Impostos + el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") + + total_tributo = format_number(tagtext(oNode=el_total, cTag='vTotTrib'), + precision=2) + valor_total = format_number(tagtext(oNode=el_total, cTag='vProd'), + precision=2) + desconto = format_number(tagtext(oNode=el_total, cTag='vDesc'), + precision=2) + valor_a_pagar = format_number(tagtext(oNode=el_total, cTag='vNF'), + precision=2) + el_pag = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}pag") + troco = format_number(tagtext(oNode=el_pag, cTag="vTroco")) + + payment_method_list = {'01': 'Dinheiro', + '02': 'Cheque', + '03': 'Cartão de Crédito', + '04': 'Cartão de Débito', + "05": "Crédito Loja", + '10': 'Vale Alimentação', + '11': 'Vale Refeição', + '12': 'Vale Presente', + '13': 'Vale Combustível', + '14': 'Duplicata Mercantil', + '15': 'Boleto Bancario', + '90': 'Sem Pagamento', + '99': 'Outros'} + quant_produtos = len(oXML.findall( + ".//{http://www.portalfiscal.inf.br/nfe}det")) + + payment_methods = [] + + for pagId, item in enumerate(el_pag): + if 'tPag' not in item.tag: + continue + + payment = [] + method = payment_method_list[item.text] + + payment.append(method) + payment.append(format_number(item.getnext().text, precision=2)) + payment_methods.append(payment) + + values = {'quantidade_itens': quant_produtos, + 'total_tributo': total_tributo, + 'valor_total': valor_total, + 'desconto': desconto, + 'valor_a_pagar': valor_a_pagar, + 'formas_de_pagamento': payment_methods, + 'troco': troco, + } + + self.draw_totals_table(values) + + self.drawLine() + + def draw_totals_table(self, values): + rowHeights = [7, 7, 7, 7, 10] + data = [['QTD.TOTAL DE ITENS', values['quantidade_itens']], + ['VALOR TOTAL R$', values['valor_total']], + ['DESCONTO R$', values['desconto']], + ['VALOR A PAGAR R$', values['valor_a_pagar']], + ['FORMA DE PAGAMENTO', 'VALOR PAGO R$'], + ] + + for item in values['formas_de_pagamento']: + data.append([item[0], item[1]]) + rowHeights.append(7) + data.append(['TROCO', format_number(values['troco'], precision=2)]) + rowHeights.append(7) + + table2 = Table(data, colWidths=(150, 50), rowHeights=tuple(rowHeights)) + table2.setStyle(TableStyle([ + ('FONTSIZE', (0, 0), (-1, -1), 7), + ('FONT', (0, 0), (1, -1), 'NimbusSanL-Regu'), + ('FONT', (0, 4), (1, 4), 'NimbusSanL-Bold'), + ('ALIGN', (1, 0), (1, -1), "RIGHT") + ])) + w, h = table2.wrapOn(self.canvas, 200, 450) + table2.drawOn(self.canvas, 0, self.current_height - (h * 1.1)) + self.current_height -= h + + def inf_authentication(self, oXML=None): + el_infNFe = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}infNFe") + # n nfce, serie e data de solicitacao + el_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") + + el_NFeSupl = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}infNFeSupl") + + el_dest = el_infNFe.find(".//{http://www.portalfiscal.inf.br/nfe}dest") + # chave, n protocolo, data autorizacao + el_prot_nfe = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}protNFe") + + el_infAdic = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}infAdic") + + url_chave = tagtext(oNode=el_NFeSupl, cTag='urlChave') + access_key = tagtext(oNode=el_prot_nfe, cTag="chNFe") + + frase_chave_acesso = 'Consulte pela Chave de Acesso em:
\ +%s
%s' % (url_chave, access_key) + + qrcode = tagtext(oNode=el_NFeSupl, cTag='qrCode') + + cnpj = tagtext(oNode=el_dest, cTag='CNPJ') + cpf = tagtext(oNode=el_dest, cTag='CPF') + if cnpj: + cnpj_cpf = format_cnpj_cpf(cnpj) + cnpj_cpf = "CONSUMIDOR CNPJ: %s" % (cnpj) + elif cpf: + cnpj_cpf = format_cnpj_cpf(cpf) + cnpj_cpf = "CONSUMIDOR CPF: %s" % (cpf) + else: + cnpj_cpf = u"CONSUMIDOR NÃO IDENTIFICADO" + + nNFC = tagtext(oNode=el_ide, cTag="nNF") + serie = tagtext(oNode=el_ide, cTag='serie') + + dataSolicitacao = getdateUTC(tagtext(oNode=el_ide, cTag="dhEmi")) + dataSolicitacao = dataSolicitacao[0] + " " + dataSolicitacao[1] + + numProtocolo = tagtext(oNode=el_prot_nfe, cTag="nProt") + + dataAutorizacao = getdateUTC(tagtext(oNode=el_prot_nfe, + cTag='dhRecbto')) + dataAutorizacao = dataAutorizacao[0] + " " + dataAutorizacao[1] + + text = u"%s
%s
NFC-e nº%s Série %s %s
\ +Protocolo de autorização: %s
Data de autorização %s
\ +" % (frase_chave_acesso, cnpj_cpf, nNFC, serie, dataSolicitacao, + numProtocolo, dataAutorizacao) + + self._drawCenteredParagraph(text) + + self.draw_qr_code(qrcode) + + infAdFisco = tagtext(oNode=el_infAdic, cTag='infAdFisco') + self._drawCenteredParagraph(infAdFisco) + + infCpl = tagtext(oNode=el_infAdic, cTag='infCpl') + self._drawCenteredParagraph(infCpl) + + def _drawCenteredParagraph(self, text): + + style = ParagraphStyle( + name='Normal', + fontName='NimbusSanL-Regu', + fontSize=7, + alignment=TA_CENTER, + leading=7, + ) + + paragraph = Paragraph(text, style=style) + w, h = paragraph.wrapOn(self.canvas, 180, 300) + paragraph.drawOn(self.canvas, 10, self.current_height - h) + self.current_height -= (h*1.1) + + def drawString(self, string, centered=False): + if centered: + self.canvas.drawCentredString( + self.max_width / 2, self.current_height, string) + self.current_height -= self.current_font_size + else: + self.canvas.drawString(self.min_width, self.current_height, string) + self.current_height -= self.current_font_size + + def drawTitle(self, string, size, font='NimbusSanL-Regu'): + self.canvas.setFont(font, size) + self.canvas.drawCentredString( + self.max_width / 2, self.current_height, string) + self.current_height -= self.current_font_size + self.canvas.setFont(self.current_font_name, self.current_font_size) + + def drawLine(self): + self.canvas.line(self.min_width, self.current_height, + self.max_width, self.current_height) + self.current_height -= self.current_font_size + + def draw_qr_code(self, string): + qr_code = qr.QrCodeWidget(string) + drawing = Drawing(23 * mm, 23 * mm) + drawing.add(qr_code) + renderPDF.draw(drawing, self.canvas, 20 * mm, self.current_height - 85) + self.current_height -= 85 + + def newpage(self): + self.current_height = self.max_height + self.Page += 1 + self.canvas.showPage() + + def nfce_generate(self): + for oXML in self.list_xml: + oXML_cobr = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}cobr") + + self.NrPages = 1 + self.Page = 1 + + # Calculando total linhas usadas para descrições dos itens + # Com bloco fatura, apenas 29 linhas para itens na primeira folha + nNr_Lin_Pg_1 = 34 if oXML_cobr is None else 30 + # [ rec_ini , rec_fim , lines , limit_lines ] + oPaginator = [[0, 0, 0, nNr_Lin_Pg_1]] + el_det = oXML.findall(".//{http://www.portalfiscal.inf.br/nfe}det") + if el_det is not None: + list_desc = [] + list_cod_prod = [] + nPg = 0 + for nId, item in enumerate(el_det): + el_prod = item.find( + ".//{http://www.portalfiscal.inf.br/nfe}prod") + infAdProd = item.find( + ".//{http://www.portalfiscal.inf.br/nfe}infAdProd") + + list_ = wrap(tagtext(oNode=el_prod, cTag='xProd'), 56) + if infAdProd is not None: + list_.extend(wrap(infAdProd.text, 56)) + list_desc.append(list_) + + list_cProd = wrap(tagtext(oNode=el_prod, cTag='cProd'), 14) + list_cod_prod.append(list_cProd) + + # Nr linhas necessárias p/ descrição item + nLin_Itens = len(list_) + + if (oPaginator[nPg][2] + nLin_Itens) >= oPaginator[nPg][3]: + oPaginator.append([0, 0, 0, 77]) + nPg += 1 + oPaginator[nPg][0] = nId + oPaginator[nPg][1] = nId + 1 + oPaginator[nPg][2] = nLin_Itens + else: + # adiciona-se 1 pelo funcionamento de xrange + oPaginator[nPg][1] = nId + 1 + oPaginator[nPg][2] += nLin_Itens + + self.NrPages = len(oPaginator) # Calculando nr. páginas + + self.ide_emit(oXML=oXML) + # self.destinatario(oXML=oXML) + self.danfce_information() + + self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPaginator[0], + list_desc=list_desc, list_cod_prod=list_cod_prod) + + self.drawLine() + + self.totais(oXML=oXML) + + self.inf_authentication(oXML=oXML) + + # Gera o restante das páginas do XML + for oPag in oPaginator[1:]: + if oPag: + self.newpage() + self.ide_emit(oXML=oXML) + # self.destinatario(oXML=oXML) + self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPag, + list_desc=list_desc, + list_cod_prod=list_cod_prod) + self.totais(oXML=oXML) + self.inf_authentication(oXML=oXML) + + self.newpage() + + self.canvas.save() + + def writeto_pdf(self, fileObj): + pdf_out = self.oPDF_IO.getvalue() + self.oPDF_IO.close() + fileObj.write(pdf_out)