empresa-libre/source/app/controllers/helper.py

553 lines
18 KiB
Python

#!/usr/bin/env python3
#~ import falcon
import re
import smtplib
import collections
from collections import OrderedDict
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
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
``collections.MutableMapping`` as well as dict's ``copy``. Also
provides ``lower_items``.
All keys are expected to be strings. The structure remembers the
case of the last key to be set, and ``iter(instance)``,
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
will contain case-sensitive keys. However, querying and contains
testing is case insensitive::
cid = CaseInsensitiveDict()
cid['Accept'] = 'application/json'
cid['aCCEPT'] == 'application/json' # True
list(cid) == ['Accept'] # True
For example, ``headers['content-encoding']`` will return the
value of a ``'Content-Encoding'`` response header, regardless
of how the header name was originally stored.
If the constructor, ``.update``, or equality comparison
operations are given keys that have equal ``.lower()``s, the
behavior is undefined.
"""
def __init__(self, data=None, **kwargs):
self._store = OrderedDict()
if data is None:
data = {}
self.update(data, **kwargs)
def __setitem__(self, key, value):
# Use the lowercased key for lookups, but store the actual
# key alongside the value.
self._store[key.lower()] = (key, value)
def __getitem__(self, key):
return self._store[key.lower()][1]
def __delitem__(self, key):
del self._store[key.lower()]
def __iter__(self):
return (casedkey for casedkey, mappedvalue in self._store.values())
def __len__(self):
return len(self._store)
def lower_items(self):
"""Like iteritems(), but with all lowercase keys."""
return (
(lowerkey, keyval[1])
for (lowerkey, keyval)
in self._store.items()
)
def __eq__(self, other):
if isinstance(other, collections.Mapping):
other = CaseInsensitiveDict(other)
else:
return NotImplemented
# Compare insensitively
return dict(self.lower_items()) == dict(other.lower_items())
# Copy is required
def copy(self):
return CaseInsensitiveDict(self._store.values())
def __repr__(self):
return str(dict(self.items()))
class NumLet(object):
def __init__(self, value, moneda, **args):
self._letras = self._letters(value, moneda)
@property
def letras(self):
return self._letras.upper()
#~ def _letters(self, numero, moneda='peso', texto_inicial='-(',
#~ texto_final='/100 m.n.)-', fraccion_letras=False, fraccion=''):
def _letters(self, numero, moneda='peso'):
texto_inicial = ''
texto_final = '/100 m.n.)-'
fraccion_letras = False
fraccion = ''
enletras = texto_inicial
numero = abs(numero)
numtmp = '%015d' % numero
if numero < 1:
enletras += 'cero ' + self._plural(moneda) + ' '
else:
enletras += self._numlet(numero)
if numero == 1 or numero < 2:
enletras += moneda + ' '
elif int(''.join(numtmp[3:])) == 0 or int(''.join(numtmp[9:])) == 0:
enletras += 'de ' + self._plural(moneda) + ' '
else:
enletras += self._plural(moneda) + ' '
decimal = '%0.2f' % numero
decimal = decimal.split('.')[1]
#~ decimal = int((numero-int(numero))*100)
if fraccion_letras:
if decimal == 0:
enletras += 'con cero ' + self._plural(fraccion)
elif decimal == 1:
enletras += 'con un ' + fraccion
else:
enletras += 'con ' + self._numlet(int(decimal)) + self.plural(fraccion)
else:
enletras += decimal
enletras += texto_final
return enletras
def _numlet(self, numero):
numtmp = '%015d' % numero
co1=0
letras = ''
leyenda = ''
for co1 in range(0,5):
inicio = co1*3
cen = int(numtmp[inicio:inicio+1][0])
dec = int(numtmp[inicio+1:inicio+2][0])
uni = int(numtmp[inicio+2:inicio+3][0])
letra3 = self.centena(uni, dec, cen)
letra2 = self.decena(uni, dec)
letra1 = self.unidad(uni, dec)
if co1 == 0:
if (cen+dec+uni) == 1:
leyenda = 'billon '
elif (cen+dec+uni) > 1:
leyenda = 'billones '
elif co1 == 1:
if (cen+dec+uni) >= 1 and int(''.join(numtmp[6:9])) == 0:
leyenda = "mil millones "
elif (cen+dec+uni) >= 1:
leyenda = "mil "
elif co1 == 2:
if (cen+dec) == 0 and uni == 1:
leyenda = 'millon '
elif cen > 0 or dec > 0 or uni > 1:
leyenda = 'millones '
elif co1 == 3:
if (cen+dec+uni) >= 1:
leyenda = 'mil '
elif co1 == 4:
if (cen+dec+uni) >= 1:
leyenda = ''
letras += letra3 + letra2 + letra1 + leyenda
letra1 = ''
letra2 = ''
letra3 = ''
leyenda = ''
return letras
def centena(self, uni, dec, cen):
letras = ''
numeros = ["","","doscientos ","trescientos ","cuatrocientos ","quinientos ","seiscientos ","setecientos ","ochocientos ","novecientos "]
if cen == 1:
if (dec+uni) == 0:
letras = 'cien '
else:
letras = 'ciento '
elif cen >= 2 and cen <= 9:
letras = numeros[cen]
return letras
def decena(self, uni, dec):
letras = ''
numeros = ["diez ","once ","doce ","trece ","catorce ","quince ","dieci","dieci","dieci","dieci"]
decenas = ["","","","treinta ","cuarenta ","cincuenta ","sesenta ","setenta ","ochenta ","noventa "]
if dec == 1:
letras = numeros[uni]
elif dec == 2:
if uni == 0:
letras = 'veinte '
elif uni > 0:
letras = 'veinti'
elif dec >= 3 and dec <= 9:
letras = decenas[dec]
if uni > 0 and dec > 2:
letras = letras+'y '
return letras
def unidad(self, uni, dec):
letras = ''
numeros = ["","un ","dos ","tres ","cuatro ","cinco ","seis ","siete ","ocho ","nueve "]
if dec != 1:
if uni > 0 and uni <= 5:
letras = numeros[uni]
if uni >= 6 and uni <= 9:
letras = numeros[uni]
return letras
def _plural(self, palabra):
if re.search('[aeiou]$', palabra):
return re.sub('$', 's', palabra)
else:
return palabra + 'es'
class SendMail(object):
def __init__(self, config):
self._config = config
self._server = None
self._error = ''
self._is_connect = self._login()
@property
def is_connect(self):
return self._is_connect
@property
def error(self):
return self._error
def _login(self):
try:
if self._config['ssl']:
self._server = smtplib.SMTP_SSL(
self._config['servidor'],
self._config['puerto'], timeout=10)
else:
self._server = smtplib.SMTP(
self._config['servidor'],
self._config['puerto'], timeout=10)
self._server.login(self._config['usuario'], self._config['contra'])
return True
except smtplib.SMTPAuthenticationError as e:
if '535' in str(e):
self._error = 'Nombre de usuario o contraseƱa invƔlidos'
return False
if '534' in str(e) and 'gmail' in self._config['servidor']:
self._error = 'Necesitas activar el acceso a otras ' \
'aplicaciones en tu cuenta de GMail'
return False
except smtplib.SMTPException as e:
self._error = str(e)
return False
except Exception as e:
self._error = str(e)
return False
return
def send(self, options):
try:
message = MIMEMultipart()
message['From'] = self._config['usuario']
message['To'] = options['para']
message['CC'] = options['copia']
message['Subject'] = options['asunto']
message['Date'] = formatdate(localtime=True)
message.attach(MIMEText(options['mensaje'], 'html'))
for f in options['files']:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f[0])
encoders.encode_base64(part)
part.add_header(
'Content-Disposition',
"attachment; filename={}".format(f[1]))
message.attach(part)
receivers = options['para'].split(',') + options['copia'].split(',')
self._server.sendmail(
self._config['usuario'], receivers, message.as_string())
return ''
except Exception as e:
return str(e)
def close(self):
try:
self._server.quit()
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