Generar PDF con reportlab
This commit is contained in:
parent
d3671de3a4
commit
5fabcceb70
|
@ -10,3 +10,4 @@ python-dateutil
|
|||
zeep
|
||||
chardet
|
||||
pyqrcode
|
||||
reportlab
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue