Generar PDF con reportlab

This commit is contained in:
Mauricio Baeza 2017-10-23 00:45:41 -05:00
parent d3671de3a4
commit 5fabcceb70
5 changed files with 319 additions and 34 deletions

View File

@ -10,3 +10,4 @@ python-dateutil
zeep
chardet
pyqrcode
reportlab

View File

@ -1,18 +1,6 @@
#!/usr/bin/env python3
#~ import falcon
#~ from models.main import get_cp
#~ class AppPostalCode(object):
#~ def on_get(self, req, resp):
#~ values = req.params
#~ req.context['result'] = get_cp(values['cp'])
#~ resp.status = falcon.HTTP_200
#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
import re
import smtplib
import collections
@ -24,7 +12,17 @@ from email.mime.text import MIMEText
from email import encoders
from email.utils import formatdate
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib.pagesizes import letter
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from reportlab.platypus import Paragraph, Table, TableStyle, Spacer
from reportlab.pdfgen import canvas
#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
class CaseInsensitiveDict(collections.MutableMapping):
"""A case-insensitive ``dict``-like object.
Implements all methods and operations of
@ -307,3 +305,248 @@ class SendMail(object):
except:
pass
return
class NumberedCanvas(canvas.Canvas):
X = 20.59 * cm
Y = 1 * cm
def __init__(self, *args, **kwargs):
canvas.Canvas.__init__(self, *args, **kwargs)
self._saved_page_states = []
def showPage(self):
self._saved_page_states.append(dict(self.__dict__))
self._startPage()
return
def save(self):
"""add page info to each page (page x of y)"""
page_count = len(self._saved_page_states)
for state in self._saved_page_states:
self.__dict__.update(state)
self.draw_page_number(page_count)
canvas.Canvas.showPage(self)
canvas.Canvas.save(self)
return
def draw_page_number(self, page_count):
self.setFont('Helvetica', 8)
self.setFillColor(colors.darkred)
text = 'Página {} de {}'.format(self._pageNumber, page_count)
self.drawRightString(self.X, self.Y, text)
return
class TemplateInvoice(BaseDocTemplate):
def __init__(self, *args, **kwargs):
# letter 21.6 x 27.9
kwargs['pagesize'] = letter
kwargs['rightMargin'] = 1 * cm
kwargs['leftMargin'] = 1 * cm
kwargs['topMargin'] = 1.5 * cm
kwargs['bottomMargin'] = 1.5 * cm
BaseDocTemplate.__init__(self, *args, **kwargs)
self._data = {}
self._rows = []
def _set_rect(self, style):
color = style.pop('color', 'black')
if isinstance(color, str):
self.canv.setFillColor(getattr(colors, color))
else:
self.canv.setFillColorRGB(*color)
keys = ('x', 'y', 'width', 'height', 'radius')
for k in keys:
style[k] = style[k] * cm
self.canv.roundRect(**style)
return
def _set_text(self, styles, value):
rect = styles['rectangulo']
if value:
self.canv.setFillColor(colors.white)
self.canv.setStrokeColor(colors.white)
ps = ParagraphStyle(**styles['estilo'])
p = Paragraph(value, ps)
p.wrap(rect['width'] * cm, rect['height'] * cm)
p.drawOn(self.canv, rect['x'] * cm, rect['y'] * cm)
self._set_rect(rect)
return
def _emisor(self, styles, data):
for k, v in styles.items():
self._set_text(styles[k], data.get(k, ''))
return
def afterPage(self):
encabezado = self._custom_styles['encabezado']
self.canv.saveState()
self._emisor(encabezado['emisor'], self._data['emisor'])
self.canv.restoreState()
return
@property
def custom_styles(self):
return self._custom_styles
@custom_styles.setter
def custom_styles(self, values):
self._custom_styles = values
@property
def data(self):
return self._data
@data.setter
def data(self, values):
self._data = values
text = 'Total este reporte = $ {}'.format('1,000.00')
ps = ParagraphStyle(
name='Total',
fontSize=12,
fontName='Helvetica-BoldOblique',
textColor=colors.darkred,
spaceBefore=0.5 * cm,
spaceAfter=0.5 * cm)
p1 = Paragraph(text, ps)
text = 'Nota: esta suma no incluye documentos cancelados'
ps = ParagraphStyle(
name='Note',
fontSize=7,
fontName='Helvetica-BoldOblique')
p2 = Paragraph('', ps)
self._rows = [p2]
def render(self):
frame = Frame(self.leftMargin, self.bottomMargin,
self.width, self.height, id='normal')
template = PageTemplate(id='invoice', frames=frame)
self.addPageTemplates([template])
self.build(self._rows, canvasmaker=NumberedCanvas)
return
class ReportTemplate(BaseDocTemplate):
"""Override the BaseDocTemplate class to do custom handle_XXX actions"""
def __init__(self, *args, **kwargs):
# letter 21.6 x 27.9
kwargs['pagesize'] = letter
kwargs['rightMargin'] = 1 * cm
kwargs['leftMargin'] = 1 * cm
kwargs['topMargin'] = 1.5 * cm
kwargs['bottomMargin'] = 1.5 * cm
BaseDocTemplate.__init__(self, *args, **kwargs)
self.styles = getSampleStyleSheet()
self.header = {}
self.data = []
def afterPage(self):
"""Called after each page has been processed"""
self.canv.saveState()
date = datetime.datetime.today().strftime('%A, %d de %B del %Y')
self.canv.setStrokeColorRGB(0.5, 0, 0)
self.canv.setFont("Helvetica", 8)
self.canv.drawRightString(20.59 * cm, 26.9 * cm, date)
self.canv.line(1 * cm, 26.4 * cm, 20.6 * cm, 26.4 * cm)
path_cur = os.path.dirname(os.path.realpath(__file__))
path_img = os.path.join(path_cur, 'logo.png')
try:
self.canv.drawImage(path_img, 1 * cm, 24.2 * cm, 4 * cm, 2 * cm)
except:
pass
self.canv.roundRect(
5 * cm, 25.4 * cm, 15.5 * cm, 0.6 * cm, 0.15 * cm,
stroke=True, fill=False)
self.canv.setFont('Helvetica-BoldOblique', 10)
self.canv.drawCentredString(12.75 * cm, 25.6 * cm, self.header['emisor'])
self.canv.roundRect(
5 * cm, 24.4 * cm, 15.5 * cm, 0.6 * cm, 0.15 * cm,
stroke=True, fill=False)
self.canv.setFont('Helvetica-BoldOblique', 9)
self.canv.drawCentredString(12.75 * cm, 24.6 * cm, self.header['title'])
self.canv.line(1 * cm, 1.5 * cm, 20.6 * cm, 1.5 * cm)
self.canv.restoreState()
return
def set_data(self, data):
self.header['emisor'] = data['emisor']
self.header['title'] = data['title']
cols = len(data['rows'][0])
widths = []
for w in data['widths']:
widths.append(float(w) * cm)
t_styles = [
('GRID', (0, 0), (-1, -1), 0.25, colors.darkred),
('FONTSIZE', (0, 0), (-1, 0), 9),
('BOX', (0, 0), (-1, 0), 1, colors.darkred),
('TEXTCOLOR', (0, 0), (-1, 0), colors.darkred),
('FONTSIZE', (0, 1), (-1, -1), 8),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('ALIGN', (0, 0), (0, -1), 'RIGHT'),
]
if cols == 6:
t_styles += [
('ALIGN', (1, 1), (1, -1), 'CENTER'),
('ALIGN', (3, 1), (3, -1), 'CENTER'),
('ALIGN', (4, 1), (4, -1), 'RIGHT'),
]
elif cols == 3:
t_styles += [
('ALIGN', (-1, 0), (-1, -1), 'RIGHT'),
('ALIGN', (-2, 0), (-2, -1), 'RIGHT'),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
]
elif cols == 2:
t_styles += [
('ALIGN', (-1, 0), (-1, -1), 'RIGHT'),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
]
rows = []
for i, r in enumerate(data['rows']):
n = i + 1
rows.append(('{}.-'.format(n),) + r)
if cols == 6:
if r[4] == 'Cancelado':
t_styles += [
('GRID', (0, n), (-1, n), 0.25, colors.red),
('TEXTCOLOR', (0, n), (-1, n), colors.red),
]
rows.insert(0, data['titles'])
t = Table(rows, colWidths=widths, repeatRows=1)
t.setStyle(TableStyle(t_styles))
text = 'Total este reporte = $ {}'.format(data['total'])
ps = ParagraphStyle(
name='Total',
fontSize=12,
fontName='Helvetica-BoldOblique',
textColor=colors.darkred,
spaceBefore=0.5 * cm,
spaceAfter=0.5 * cm)
p1 = Paragraph(text, ps)
text = 'Nota: esta suma no incluye documentos cancelados'
ps = ParagraphStyle(
name='Note',
fontSize=7,
fontName='Helvetica-BoldOblique')
p2 = Paragraph(text, ps)
self.data = [t, p1, p2]
return
def render(self):
frame = Frame(self.leftMargin, self.bottomMargin,
self.width, self.height, id='normal')
template = PageTemplate(id='report', frames=frame)
self.addPageTemplates([template])
self.build(self.data, canvasmaker=NumberedCanvas)
return

View File

@ -20,14 +20,14 @@ from io import BytesIO
from smtplib import SMTPException, SMTPAuthenticationError
from xml.etree import ElementTree as ET
import uno
from com.sun.star.beans import PropertyValue
from com.sun.star.awt import Size
#~ import uno
#~ from com.sun.star.beans import PropertyValue
#~ from com.sun.star.awt import Size
import pyqrcode
from dateutil import parser
from .helper import CaseInsensitiveDict, NumLet, SendMail
from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice
from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \
PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PRE
@ -168,6 +168,10 @@ def get_file(path):
return open(path, 'rb')
def read_file(path, mode='rb'):
return open(path, mode).read()
def get_size(path):
return os.path.getsize(path)
@ -178,16 +182,18 @@ def get_template(name, data={}):
return template.render(**data)
def get_path_template(name, default='plantilla_factura.ods'):
def get_custom_styles(name, default='plantilla_factura.json'):
path = _join(PATH_TEMPLATES, name)
if is_file(path):
return path
with open(path) as fh:
return loads(fh.read())
path = _join(PATH_TEMPLATES, default)
if is_file(path):
return path
with open(path) as fh:
return loads(fh.read())
return ''
return {}
def dumps(data):
@ -719,13 +725,17 @@ class LIBO(object):
return self._read(path)
def to_pdf(path, data):
app = LIBO()
if not app.is_running:
return b''
return app.pdf(path, data)
def to_pdf(styles, data):
#~ app = LIBO()
#~ if not app.is_running:
#~ return b''
#~ return app.pdf(path, data)
path = get_path_temp()
pdf = TemplateInvoice(path)
pdf.custom_styles = styles
pdf.data = data
pdf.render()
return read_file(path)
def parse_xml(xml):
@ -780,7 +790,7 @@ def _emisor(doc, version, values):
data = CaseInsensitiveDict(node.attrib.copy())
node = node.find('{}DomicilioFiscal'.format(PRE[version]))
if not node is None:
data.update(node.attrib.copy())
data.update(CaseInsensitiveDict(node.attrib.copy()))
data['regimenfiscal'] = values['regimenfiscal']
return data
@ -897,9 +907,9 @@ def _timbre(doc, version, values):
return data
def get_data(invoice, rfc, values):
name = '{}_factura.ods'.format(rfc.lower())
path = get_path_template(name)
def get_data_from_xml(invoice, rfc, values):
name = '{}_factura.json'.format(rfc.lower())
custom_styles = get_custom_styles(name)
data = {'cancelada': invoice.cancelada}
doc = parse_xml(invoice.xml)
@ -917,7 +927,7 @@ def get_data(invoice, rfc, values):
}
data['timbre'] = _timbre(doc, version, options)
return path, data
return custom_styles, data
def to_zip(*files):
@ -966,6 +976,10 @@ def get_path_info(path):
return (path, filename, name, extension)
def get_path_temp():
return tempfile.mkstemp()[1]
class ImportFacturaLibre(object):
def __init__(self, path):

View File

@ -16,6 +16,7 @@ if __name__ == '__main__':
from controllers import util
from settings import log, VERSION, PATH_CP, COMPANIES, PRE
FORMAT = '{0:.2f}'
@ -1083,8 +1084,8 @@ class Facturas(BaseModel):
return b'', name
values = cls._get_not_in_xml(cls, obj)
path, data = util.get_data(obj, rfc, values)
doc = util.to_pdf(path, data)
custom_styles, data = util.get_data_from_xml(obj, rfc, values)
doc = util.to_pdf(custom_styles, data)
return doc, name
@classmethod

View File

@ -0,0 +1,26 @@
{
"encabezado": {
"emisor": {
"direccion": {
"rectangulo": {"x": 1.0, "y": 26.9, "width": 19.6, "height": 0.4,
"radius": 0.1, "stroke": false, "fill": true, "color": "darkred"}
},
"nombre": {
"rectangulo": {"x": 10.6, "y": 25.9, "width": 10.0, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold", "fontSize": 12,
"alignment": 2, "textColor": "darkred", "backColor": "white"}
},
"rfc": {
"rectangulo": {"x": 10.6, "y": 25.1, "width": 10.0, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "rfc", "fontName": "Helvetica-Bold", "fontSize": 11,
"alignment": 2, "textColor": "darkred", "backColor": "white"}
}
}
},
"conceptos": {
},
"comprobante": {
}
}