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 zeep
chardet chardet
pyqrcode pyqrcode
reportlab

View File

@ -1,18 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
#~ import falcon #~ 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 re
import smtplib import smtplib
import collections import collections
@ -24,7 +12,17 @@ from email.mime.text import MIMEText
from email import encoders from email import encoders
from email.utils import formatdate 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): class CaseInsensitiveDict(collections.MutableMapping):
"""A case-insensitive ``dict``-like object. """A case-insensitive ``dict``-like object.
Implements all methods and operations of Implements all methods and operations of
@ -307,3 +305,248 @@ class SendMail(object):
except: except:
pass pass
return 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 smtplib import SMTPException, SMTPAuthenticationError
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
import uno #~ import uno
from com.sun.star.beans import PropertyValue #~ from com.sun.star.beans import PropertyValue
from com.sun.star.awt import Size #~ from com.sun.star.awt import Size
import pyqrcode import pyqrcode
from dateutil import parser 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, \ from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \
PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PRE PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PRE
@ -168,6 +168,10 @@ def get_file(path):
return open(path, 'rb') return open(path, 'rb')
def read_file(path, mode='rb'):
return open(path, mode).read()
def get_size(path): def get_size(path):
return os.path.getsize(path) return os.path.getsize(path)
@ -178,16 +182,18 @@ def get_template(name, data={}):
return template.render(**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) path = _join(PATH_TEMPLATES, name)
if is_file(path): if is_file(path):
return path with open(path) as fh:
return loads(fh.read())
path = _join(PATH_TEMPLATES, default) path = _join(PATH_TEMPLATES, default)
if is_file(path): if is_file(path):
return path with open(path) as fh:
return loads(fh.read())
return '' return {}
def dumps(data): def dumps(data):
@ -719,13 +725,17 @@ class LIBO(object):
return self._read(path) return self._read(path)
def to_pdf(path, data): def to_pdf(styles, data):
app = LIBO() #~ app = LIBO()
#~ if not app.is_running:
if not app.is_running: #~ return b''
return b'' #~ return app.pdf(path, data)
path = get_path_temp()
return app.pdf(path, data) pdf = TemplateInvoice(path)
pdf.custom_styles = styles
pdf.data = data
pdf.render()
return read_file(path)
def parse_xml(xml): def parse_xml(xml):
@ -780,7 +790,7 @@ def _emisor(doc, version, values):
data = CaseInsensitiveDict(node.attrib.copy()) data = CaseInsensitiveDict(node.attrib.copy())
node = node.find('{}DomicilioFiscal'.format(PRE[version])) node = node.find('{}DomicilioFiscal'.format(PRE[version]))
if not node is None: if not node is None:
data.update(node.attrib.copy()) data.update(CaseInsensitiveDict(node.attrib.copy()))
data['regimenfiscal'] = values['regimenfiscal'] data['regimenfiscal'] = values['regimenfiscal']
return data return data
@ -897,9 +907,9 @@ def _timbre(doc, version, values):
return data return data
def get_data(invoice, rfc, values): def get_data_from_xml(invoice, rfc, values):
name = '{}_factura.ods'.format(rfc.lower()) name = '{}_factura.json'.format(rfc.lower())
path = get_path_template(name) custom_styles = get_custom_styles(name)
data = {'cancelada': invoice.cancelada} data = {'cancelada': invoice.cancelada}
doc = parse_xml(invoice.xml) doc = parse_xml(invoice.xml)
@ -917,7 +927,7 @@ def get_data(invoice, rfc, values):
} }
data['timbre'] = _timbre(doc, version, options) data['timbre'] = _timbre(doc, version, options)
return path, data return custom_styles, data
def to_zip(*files): def to_zip(*files):
@ -966,6 +976,10 @@ def get_path_info(path):
return (path, filename, name, extension) return (path, filename, name, extension)
def get_path_temp():
return tempfile.mkstemp()[1]
class ImportFacturaLibre(object): class ImportFacturaLibre(object):
def __init__(self, path): def __init__(self, path):

View File

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