Merge branch 'develop'

Generar y facturar
This commit is contained in:
Mauricio Baeza 2017-10-26 18:53:27 -05:00
commit 795aa2e50c
19 changed files with 2554 additions and 96 deletions

View File

@ -2,6 +2,10 @@
## Mini ERP para la legislación mexicana
Este proyecto esta en continuo desarrollo, contratar un esquema de soporte,
**En cada relación comercial, hay una relación humana**
Este proyecto está en continuo desarrollo, contratar un esquema de soporte,
nos ayuda a continuar su desarrollo. Ponte en contacto con nosotros para
contratar.

View File

@ -9,3 +9,6 @@ bcrypt
python-dateutil
zeep
chardet
pyqrcode
pypng
reportlab

View File

@ -1,12 +1,726 @@
#!/usr/bin/env python3
import falcon
from models.main import get_cp
#~ 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, Image
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
class AppPostalCode(object):
#~ 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.5 * 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)
text = 'Factura elaborada con software libre: www.empresalibre.net'
self.drawString(1.5 * cm, 1.5 * cm, 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):
text = styles.pop('valor', '')
if not value:
value = text
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):
logo_path = data.pop('logo', '')
logo_style = styles.pop('logo', {})
for k, v in styles.items():
self._set_text(styles[k], data.get(k, ''))
if logo_path and logo_style:
rect = logo_style['rectangulo']
keys = ('x', 'y', 'width', 'height')
for k in keys:
rect[k] = rect[k] * cm
self.canv.drawImage(logo_path, **rect)
return
def _receptor(self, styles, data):
title = styles.pop('titulo', {})
for k, v in styles.items():
self._set_text(styles[k], data.get(k, ''))
if title:
rect = title['rectangulo']
self.canv.saveState()
self.canv.rotate(90)
value = title.pop('valor', '')
title['rectangulo']['x'], title['rectangulo']['y'] = \
title['rectangulo']['y'], title['rectangulo']['x'] * -1
self._set_text(title, value)
self.canv.restoreState()
return
def _comprobante1(self, styles, data):
title = styles.pop('titulo', {})
for k, v in styles.items():
self._set_text(styles[k], data.get(k, ''))
if title:
rect = title['rectangulo']
self.canv.saveState()
self.canv.rotate(90)
value = title.pop('valor', '')
title['rectangulo']['x'], title['rectangulo']['y'] = \
title['rectangulo']['y'], title['rectangulo']['x'] * -1
self._set_text(title, value)
self.canv.restoreState()
return
def afterPage(self):
encabezado = self._custom_styles['encabezado']
self.canv.saveState()
self._emisor(encabezado['emisor'], self._data['emisor'])
self._receptor(encabezado['receptor'], self._data['receptor'])
self._comprobante1(encabezado['comprobante'], self._data['comprobante'])
self.canv.restoreState()
return
def _currency(self, value, simbol='$'):
return '{} {:,.2f}'.format(simbol, float(value))
def _format(self, value, field):
fields = ('valorunitario', 'importe')
if field in fields:
return self._currency(value)
return value
def _conceptos(self, conceptos):
headers = (('Clave', 'Descripción', 'Unidad', 'Cantidad',
'Valor Unitario', 'Importe'),)
fields = ('noidentificacion', 'descripcion', 'unidad', 'cantidad',
'valorunitario', 'importe')
rows = []
for concepto in conceptos:
row = tuple([self._format(concepto[f], f) for f in fields])
rows.append(row)
return headers + tuple(rows)
def _totales(self, values):
#~ print (values)
rows = [('', 'Subtotal', self._currency(values['subtotal']))]
for tax in values['traslados']:
row = ('', tax[0], self._currency(tax[1]))
rows.append(row)
for tax in values['retenciones']:
row = ('', tax[0], self._currency(tax[1]))
rows.append(row)
for tax in values['taxlocales']:
row = ('', tax[0], self._currency(tax[1]))
rows.append(row)
row = ('', 'Total', self._currency(values['total']))
rows.append(row)
widths = [12.5 * cm, 4 * cm, 3 * cm]
table_styles = [
('GRID', (0, 0), (-1, -1), 0.05 * cm, colors.white),
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('FONTSIZE', (0, 0), (-1, -1), 8),
('BACKGROUND', (1, 0), (-1, -1), colors.linen),
('TEXTCOLOR', (1, 0), (-1, -1), colors.darkred),
('FACE', (1, 0), (-1, -1), 'Helvetica-Bold'),
]
table = Table(rows, colWidths=widths, spaceBefore=0.25*cm)
table.setStyle(TableStyle(table_styles))
return table
def _comprobante2(self, styles, data):
leyenda = styles.pop('leyenda', {})
ls = []
for k, v in styles.items():
if k in data:
if 'spaceBefore' in v['estilo']:
v['estilo']['spaceBefore'] = v['estilo']['spaceBefore'] * cm
ps = ParagraphStyle(**v['estilo'])
p = Paragraph(data[k], ps)
ls.append(p)
cbb = Image(data['path_cbb'])
cbb.drawHeight = 4 * cm
cbb.drawWidth = 4 * cm
style_bt = getSampleStyleSheet()['BodyText']
style_bt.leading = 8
html_t = '<b><font size=6>{}</font></b>'
html = '<font color="darkred" size=5>{}</font>'
msg = 'Cadena original del complemento de certificación digital del SAT'
rows = [
(cbb, Paragraph(html_t.format('Sello Digital del CFDI'), style_bt)),
('', Paragraph(html.format(data['sellocfd']), style_bt)),
('', Paragraph(html_t.format('Sello Digital del SAT'), style_bt)),
('', Paragraph(html.format(data['sellosat']), style_bt)),
('', Paragraph(html_t.format(msg), style_bt)),
('', Paragraph(html.format(data['cadenaoriginal']), style_bt)),
]
widths = [4 * cm, 15.5 * cm]
table_styles = [
('FONTSIZE', (0, 0), (-1, -1), 6),
('SPAN', (0, 0), (0, -1)),
('FACE', (1, 0), (1, 0), 'Helvetica-Bold'),
('BACKGROUND', (1, 1), (1, 1), colors.linen),
('TEXTCOLOR', (1, 1), (1, 1), colors.darkred),
('FACE', (1, 2), (1, 2), 'Helvetica-Bold'),
('BACKGROUND', (1, 3), (1, 3), colors.linen),
('TEXTCOLOR', (1, 3), (1, 3), colors.darkred),
('FACE', (1, 4), (1, 4), 'Helvetica-Bold'),
('BACKGROUND', (1, 5), (1, 5), colors.linen),
('TEXTCOLOR', (1, 5), (1, 5), colors.darkred),
('ALIGN', (0, 0), (0, 0), 'CENTER'),
('VALIGN', (0, 0), (0, 0), 'MIDDLE'),
]
table = Table(rows, colWidths=widths)
table.setStyle(TableStyle(table_styles))
ls.append(table)
if leyenda:
if 'spaceBefore' in leyenda['estilo']:
leyenda['estilo']['spaceBefore'] = \
leyenda['estilo']['spaceBefore'] * cm
msg = 'Este documento es una representación impresa de un CFDI'
ps = ParagraphStyle(**leyenda['estilo'])
p = Paragraph(msg, ps)
ls.append(p)
return ls
@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):
#~ print (values)
self._data = values
rows = self._conceptos(self._data['conceptos'])
widths = [2 * cm, 9 * cm, 1.5 * cm, 2 * cm, 2 * cm, 3 * cm]
table_styles = [
('GRID', (0, 0), (-1, -1), 0.05 * cm, colors.white),
('FONTSIZE', (0, 0), (-1, 0), 7),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FACE', (0, 0), (-1, 0), 'Helvetica-Bold'),
('BACKGROUND', (0, 0), (-1, 0), colors.darkred),
('FONTSIZE', (0, 1), (-1, -1), 7),
('VALIGN', (0, 1), (-1, -1), 'TOP'),
('ALIGN', (0, 1), (0, -1), 'CENTER'),
('ALIGN', (2, 1), (2, -1), 'CENTER'),
('ALIGN', (3, 1), (5, -1), 'RIGHT'),
('LINEBELOW', (0, 1), (-1, -1), 0.05 * cm, colors.darkred),
('LINEBEFORE', (0, 1), (-1, -1), 0.05 * cm, colors.white),
]
table_conceptos = Table(rows, colWidths=widths, repeatRows=1)
table_conceptos.setStyle(TableStyle(table_styles))
totales = self._totales(self.data['totales'])
comprobante = self._comprobante2(
self._custom_styles['comprobante'], self.data['comprobante'])
self._rows = [Spacer(0, 6*cm), table_conceptos, totales] + comprobante
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.black),
('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
def on_get(self, req, resp):
values = req.params
req.context['result'] = get_cp(values['cp'])
resp.status = falcon.HTTP_200

View File

@ -76,12 +76,33 @@ class AppValues(object):
if file_object is None:
session = req.env['beaker.session']
values = req.params
req.context['result'] = self._db.validate_cert(values, session)
if table == 'correo':
req.context['result'] = self._db.validate_email(values)
elif table == 'sendmail':
req.context['result'] = self._db.send_email(values, session)
else:
req.context['result'] = self._db.validate_cert(values, session)
else:
req.context['result'] = self._db.add_cert(file_object)
resp.status = falcon.HTTP_200
class AppConfig(object):
def __init__(self, db):
self._db = db
def on_get(self, req, resp):
values = req.params
req.context['result'] = self._db.get_config(values)
resp.status = falcon.HTTP_200
def on_post(self, req, resp):
values = req.params
req.context['result'] = self._db.add_config(values)
resp.status = falcon.HTTP_200
class AppPartners(object):
@ -206,8 +227,9 @@ class AppDocumentos(object):
#~ self._not_json = True
def on_get(self, req, resp, type_doc, id_doc):
session = req.env['beaker.session']
req.context['result'], file_name, content_type = \
self._db.get_doc(type_doc, id_doc)
self._db.get_doc(type_doc, id_doc, session['rfc'])
resp.append_header('Content-Disposition',
'attachment; filename={}'.format(file_name))
resp.content_type = content_type

View File

@ -20,7 +20,10 @@ from zeep.cache import SqliteCache
from zeep.transports import Transport
from zeep.exceptions import Fault, TransportError
from .configpac import DEBUG, TIMEOUT, AUTH, URL
if __name__ == '__main__':
from configpac import DEBUG, TIMEOUT, AUTH, URL
else:
from .configpac import DEBUG, TIMEOUT, AUTH, URL
log = Logger('PAC')
@ -147,7 +150,7 @@ class Ecodex(object):
class Finkok(object):
def __init__(self):
def __init__(self, auth={}):
self.codes = URL['codes']
self.error = ''
self.message = ''
@ -159,6 +162,9 @@ class Finkok(object):
if DEBUG:
self._history = HistoryPlugin()
self._plugins = [self._history]
self._auth = AUTH
else:
self._auth = auth
def _debug(self):
if not DEBUG:
@ -225,6 +231,11 @@ class Finkok(object):
def timbra_xml(self, file_xml):
self.error = ''
if not DEBUG and not self._auth:
self.error = 'Sin datos para timbrar'
return
method = 'timbra'
ok, xml = self._validate_xml(file_xml)
if not ok:
@ -233,8 +244,8 @@ class Finkok(object):
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': AUTH['USER'],
'password': AUTH['PASS'],
'username': self._auth['USER'],
'password': self._auth['PASS'],
'xml': xml,
}
if URL['quick_stamp']:
@ -261,8 +272,8 @@ class Finkok(object):
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': AUTH['USER'],
'password': AUTH['PASS'],
'username': self._auth['USER'],
'password': self._auth['PASS'],
'uuid': uuid,
'taxpayer_id': self.rfc,
'invoice_type': 'I',
@ -296,7 +307,8 @@ class Finkok(object):
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
try:
result = client.service.stamped(xml, AUTH['USER'], AUTH['PASS'])
result = client.service.stamped(
xml, self._auth['user'], self._auth['pass'])
except Fault as e:
self.error = str(e)
return ''
@ -310,7 +322,8 @@ class Finkok(object):
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
try:
result = client.service.query_pending(AUTH['USER'], AUTH['PASS'], uuid)
result = client.service.query_pending(
self._auth['USER'], self._auth['PASS'], uuid)
#~ print (result.date)
#~ tree = parseString(unescape(result.xml))
#~ response = tree.toprettyxml(encoding='utf-8').decode('utf-8')
@ -334,8 +347,8 @@ class Finkok(object):
args = {
'UUIDS': uuid_type(uuids=sa(string=uuids)),
'username': AUTH['USER'],
'password': AUTH['PASS'],
'username': self._auth['USER'],
'password': self._auth['PASS'],
'taxpayer_id': rfc,
'cer': cer,
'key': key,
@ -366,8 +379,8 @@ class Finkok(object):
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': AUTH['USER'],
'password': AUTH['PASS'],
'username': self._auth['USER'],
'password': self._auth['PASS'],
'xml': xml,
'store_pending': True,
}
@ -385,8 +398,8 @@ class Finkok(object):
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': AUTH['USER'],
'password': AUTH['PASS'],
'username': self._auth['USER'],
'password': self._auth['PASS'],
'taxpayer_id': rfc,
'uuid': '',
'type': type_acuse,
@ -413,8 +426,8 @@ class Finkok(object):
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': AUTH['USER'],
'password': AUTH['PASS'],
'username': self._auth['USER'],
'password': self._auth['PASS'],
'uuid': '',
}
try:

View File

@ -8,15 +8,32 @@ import mimetypes
import os
import re
import sqlite3
import socket
import subprocess
import tempfile
import time
import unicodedata
import uuid
import zipfile
from io import BytesIO
from smtplib import SMTPException, SMTPAuthenticationError
from xml.etree import ElementTree as ET
try:
import uno
from com.sun.star.beans import PropertyValue
from com.sun.star.awt import Size
APP_LIBO = True
except:
APP_LIBO = False
import pyqrcode
from dateutil import parser
from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice
from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \
PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL
PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PATH_MEDIA, PRE
#~ def _get_hash(password):
@ -78,7 +95,23 @@ def get_value(arg):
return value
def _valid_db_companies():
con = sqlite3.connect(COMPANIES)
sql = """
CREATE TABLE IF NOT EXISTS names(
rfc TEXT NOT NULL COLLATE NOCASE UNIQUE,
con TEXT NOT NULL
);
"""
cursor = con.cursor()
cursor.executescript(sql)
cursor.close()
con.close()
return
def _get_args(rfc):
_valid_db_companies()
con = sqlite3.connect(COMPANIES)
cursor = con.cursor()
sql = "SELECT con FROM names WHERE rfc=?"
@ -94,6 +127,18 @@ def _get_args(rfc):
return values[0]
def get_rfcs():
_valid_db_companies()
con = sqlite3.connect(COMPANIES)
cursor = con.cursor()
sql = "SELECT * FROM names"
cursor.execute(sql)
values = cursor.fetchall()
cursor.close()
con.close()
return values
def get_con(rfc=''):
if not rfc:
rfc = get_value('RFC').upper()
@ -106,17 +151,6 @@ def get_con(rfc=''):
return loads(args)
def get_rfcs():
con = sqlite3.connect(COMPANIES)
cursor = con.cursor()
sql = "SELECT * FROM names"
cursor.execute(sql)
values = cursor.fetchall()
cursor.close()
con.close()
return values
def get_sat_key(table, key):
con = sqlite3.connect(DB_SAT)
cursor = con.cursor()
@ -155,6 +189,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)
@ -165,6 +203,32 @@ def get_template(name, data={}):
return template.render(**data)
def get_custom_styles(name, default='plantilla_factura.json'):
path = _join(PATH_MEDIA, 'templates', name.lower())
if is_file(path):
with open(path) as fh:
return loads(fh.read())
path = _join(PATH_TEMPLATES, default)
if is_file(path):
with open(path) as fh:
return loads(fh.read())
return {}
def get_template_ods(name, default='plantilla_factura.ods'):
path = _join(PATH_MEDIA, 'templates', name.lower())
if is_file(path):
return path
path = _join(PATH_TEMPLATES, default)
if is_file(path):
return path
return ''
def dumps(data):
return json.dumps(data, default=str)
@ -207,7 +271,7 @@ def to_slug(string):
value = (unicodedata.normalize('NFKD', string)
.encode('ascii', 'ignore')
.decode('ascii').lower())
return value
return value.replace(' ', '_')
class Certificado(object):
@ -384,11 +448,19 @@ def make_xml(data, certificado):
return cfdi.add_sello(sello)
def timbra_xml(xml):
def timbra_xml(xml, auth):
from .pac import Finkok as PAC
if DEBUG:
auth = {}
else:
if not auth:
msg = 'Sin datos para timbrar'
result = {'ok': True, 'error': msg}
return result
result = {'ok': True, 'error': ''}
pac = PAC()
pac = PAC(auth)
xml = pac.timbra_xml(xml)
if not xml:
result['ok'] = False
@ -399,3 +471,663 @@ def timbra_xml(xml):
result['uuid'] = pac.uuid
result['fecha'] = pac.fecha
return result
class LIBO(object):
HOST = 'localhost'
PORT = '8100'
ARG = 'socket,host={},port={};urp;StarOffice.ComponentContext'.format(
HOST, PORT)
def __init__(self):
self._app = None
self._start_office()
self._init_values()
def _init_values(self):
self._ctx = None
self._sm = None
self._desktop = None
if self.is_running:
ctx = uno.getComponentContext()
service = 'com.sun.star.bridge.UnoUrlResolver'
resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx)
self._ctx = resolver.resolve('uno:{}'.format(self.ARG))
self._sm = self._ctx.ServiceManager
self._desktop = self._create_instance('com.sun.star.frame.Desktop')
return
def _create_instance(self, name, with_context=True):
if with_context:
instance = self._sm.createInstanceWithContext(name, self._ctx)
else:
instance = self._sm.createInstance(name)
return instance
@property
def is_running(self):
try:
s = socket.create_connection((self.HOST, self.PORT), 5.0)
s.close()
return True
except ConnectionRefusedError:
return False
def _start_office(self):
if self.is_running:
return
c = 1
while c < 4:
c += 1
self.app = subprocess.Popen([
'soffice', '--headless', '--accept={}'.format(self.ARG)],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
time.sleep(5)
if self.is_running:
return
return
def _set_properties(self, properties):
pl = []
for k, v in properties.items():
pv = PropertyValue()
pv.Name = k
pv.Value = v
pl.append(pv)
return tuple(pl)
def _doc_open(self, path, options):
options = self._set_properties(options)
path = self._path_url(path)
try:
doc = self._desktop.loadComponentFromURL(path, '_blank', 0, options)
return doc
except:
return None
def _path_url(self, path):
if path.startswith('file://'):
return path
return uno.systemPathToFileUrl(path)
def close(self):
if self.is_running:
if not self._desktop is None:
self._desktop.terminate()
if not self._app is None:
self._app.terminate()
return
def _read(self, path):
try:
return open(path, 'rb').read()
except:
return b''
def _clean(self):
self._sd.SearchRegularExpression = True
self._sd.setSearchString("\{(\w.+)\}")
self._search.replaceAll(self._sd)
return
def _cancelado(self, cancel):
if not cancel:
pd = self._sheet.getDrawPage()
if pd.getCount():
pd.remove(pd.getByIndex(0))
return
def _set_search(self):
self._sheet = self._template.getSheets().getByIndex(0)
self._search = self._sheet.getPrintAreas()[0]
self._search = self._sheet.getCellRangeByPosition(
self._search.StartColumn,
self._search.StartRow,
self._search.EndColumn,
self._search.EndRow
)
self._sd = self._sheet.createSearchDescriptor()
self._sd.SearchCaseSensitive = False
return
def _next_cell(self, cell):
col = cell.getCellAddress().Column
row = cell.getCellAddress().Row + 1
return self._sheet.getCellByPosition(col, row)
def _copy_cell(self, cell):
destino = self._next_cell(cell)
self._sheet.copyRange(destino.getCellAddress(), cell.getRangeAddress())
return destino
def _set_cell(self, k='', v=None, cell=None, value=False):
if k:
self._sd.setSearchString(k)
ranges = self._search.findAll(self._sd)
if ranges:
ranges = ranges.getRangeAddressesAsString().split(';')
for r in ranges:
for c in r.split(','):
cell = self._sheet.getCellRangeByName(c)
if v is None:
return cell
if cell.getImplementationName() == 'ScCellObj':
pattern = re.compile(k, re.IGNORECASE)
nv = pattern.sub(v, cell.getString())
if value:
cell.setValue(nv)
else:
cell.setString(nv)
return cell
if cell:
if cell.getImplementationName() == 'ScCellObj':
ca = cell.getCellAddress()
new_cell = self._sheet.getCellByPosition(ca.Column, ca.Row + 1)
if value:
new_cell.setValue(v)
else:
new_cell.setString(v)
return new_cell
def _comprobante(self, data):
for k, v in data.items():
if k in ('total', 'descuento', 'subtotal'):
self._set_cell('{cfdi.%s}' % k, v, value=True)
else:
self._set_cell('{cfdi.%s}' % k, v)
return
def _emisor(self, data):
for k, v in data.items():
self._set_cell('{emisor.%s}' % k, v)
return
def _receptor(self, data):
for k, v in data.items():
self._set_cell('{receptor.%s}' % k, v)
return
def _conceptos(self, data):
first = True
for concepto in data:
key = concepto.get('noidentificacion', '')
description = concepto['descripcion']
unidad = concepto['unidad']
cantidad = concepto['cantidad']
valor_unitario = concepto['valorunitario']
importe = concepto['importe']
if first:
first = False
cell_1 = self._set_cell('{noidentificacion}', key)
cell_2 = self._set_cell('{descripcion}', description)
cell_3 = self._set_cell('{unidad}', unidad)
cell_4 = self._set_cell('{cantidad}', cantidad, value=True)
cell_5 = self._set_cell('{valorunitario}', valor_unitario, value=True)
cell_6 = self._set_cell('{importe}', importe, value=True)
return
def _totales(self, data):
currency = data['moneda']
cell_title = self._set_cell('{subtotal.titulo}', 'SubTotal')
value = data['subtotal']
cell_value = self._set_cell('{subtotal}', value, value=True)
cell_value.CellStyle = currency
#~ Si encuentra el campo {total}, se asume que los totales e impuestos
#~ están declarados de forma independiente cada uno
#~ if self._add_totales(xml):
#~ return
#~ Si no se encuentra, copia las celdas hacia abajo de
#~ {subtotal.titulo} y {subtotal}
if 'descuento' in data:
self._copy_cell(cell_title)
self._copy_cell(cell_value)
cell_title = self._set_cell(v='Descuento', cell=cell_title)
value = data['descuento']
cell_value = self._set_cell(v=value, cell=cell_value, value=True)
cell_value.CellStyle = currency
for tax in data['traslados']:
self._copy_cell(cell_title)
self._copy_cell(cell_value)
cell_title = self._set_cell(v=tax[0], cell=cell_title)
cell_value = self._set_cell(v=tax[1], cell=cell_value, value=True)
cell_value.CellStyle = currency
for tax in data['retenciones']:
self._copy_cell(cell_title)
self._copy_cell(cell_value)
cell_title = self._set_cell(v=tax[0], cell=cell_title)
cell_value = self._set_cell(v=tax[1], cell=cell_value, value=True)
cell_value.CellStyle = currency
for tax in data['taxlocales']:
self._copy_cell(cell_title)
self._copy_cell(cell_value)
cell_title = self._set_cell(v=tax[0], cell=cell_title)
cell_value = self._set_cell(v=tax[1], cell=cell_value, value=True)
cell_value.CellStyle = currency
self._copy_cell(cell_title)
self._copy_cell(cell_value)
cell_title = self._set_cell(v='Total', cell=cell_title)
value = data['total']
cell_value = self._set_cell(v=value, cell=cell_value, value=True)
cell_value.CellStyle = currency
return
def _timbre(self, data):
for k, v in data.items():
self._set_cell('{timbre.%s}' % k, v)
pd = self._sheet.getDrawPage()
image = self._template.createInstance('com.sun.star.drawing.GraphicObjectShape')
image.GraphicURL = data['path_cbb']
pd.add(image)
s = Size()
s.Width = 4250
s.Height = 4500
image.setSize(s)
image.Anchor = self._set_cell('{timbre.cbb}')
return
def _render(self, data):
self._set_search()
self._comprobante(data['comprobante'])
self._emisor(data['emisor'])
self._receptor(data['receptor'])
self._conceptos(data['conceptos'])
self._totales(data['totales'])
self._timbre(data['timbre'])
self._cancelado(data['cancelada'])
self._clean()
return
def pdf(self, path, data):
options = {'AsTemplate': True, 'Hidden': True}
self._template = self._doc_open(path, options)
if self._template is None:
return b''
self._render(data)
path = '{}.ods'.format(tempfile.mkstemp()[1])
self._template.storeToURL(self._path_url(path), ())
doc = self._doc_open(path, {'Hidden': True})
options = {'FilterName': 'calc_pdf_Export'}
path = tempfile.mkstemp()[1]
doc.storeToURL(self._path_url(path), self._set_properties(options))
doc.close(True)
self._template.close(True)
return self._read(path)
def to_pdf(data):
rfc = data['emisor']['rfc']
version = data['comprobante']['version']
if APP_LIBO:
app = LIBO()
if app.is_running:
name = '{}_{}.ods'.format(rfc, version)
path = get_template_ods(name)
if path:
return app.pdf(path, data)
name = '{}_{}.json'.format(rfc, version)
custom_styles = get_custom_styles(name)
path = get_path_temp()
pdf = TemplateInvoice(path)
pdf.custom_styles = custom_styles
pdf.data = data
pdf.render()
return read_file(path)
def parse_xml(xml):
return ET.fromstring(xml)
def get_dict(data):
return CaseInsensitiveDict(data)
def to_letters(value, moneda):
monedas = {
'MXN': 'peso',
'USD': 'dólar',
'EUR': 'euro',
}
return NumLet(value, monedas[moneda]).letras
def get_qr(data):
path = tempfile.mkstemp()[1]
qr = pyqrcode.create(data, mode='binary')
qr.png(path, scale=7)
return path
def _comprobante(values, options):
data = CaseInsensitiveDict(values)
del data['certificado']
data['totalenletras'] = to_letters(float(data['total']), data['moneda'])
if data['version'] == '3.3':
tipos = {
'I': 'ingreso',
'E': 'egreso',
'T': 'traslado',
}
data['tipodecomprobante'] = tipos.get(data['tipodecomprobante'])
data['lugarexpedicion'] = 'C.P. de Expedición: {}'.format(data['lugarexpedicion'])
data['metododepago'] = options['metododepago']
data['formadepago'] = options['formadepago']
data['moneda'] = options['moneda']
data['tipocambio'] = 'Tipo de Cambio: $ {:0.2f}'.format(
float(data['tipocambio']))
if 'serie' in data:
data['folio'] = '{}-{}'.format(data['serie'], data['folio'])
return data
def _emisor(doc, version, values):
node = doc.find('{}Emisor'.format(PRE[version]))
data = CaseInsensitiveDict(node.attrib.copy())
node = node.find('{}DomicilioFiscal'.format(PRE[version]))
if not node is None:
data.update(CaseInsensitiveDict(node.attrib.copy()))
data['regimenfiscal'] = values['regimenfiscal']
path = _join(PATH_MEDIA, 'logos', '{}.png'.format(data['rfc'].lower()))
if is_file(path):
data['logo'] = path
return data
def _receptor(doc, version, values):
node = doc.find('{}Receptor'.format(PRE[version]))
data = CaseInsensitiveDict(node.attrib.copy())
node = node.find('{}Domicilio'.format(PRE[version]))
if not node is None:
data.update(node.attrib.copy())
data['usocfdi'] = values['usocfdi']
return data
def _conceptos(doc, version):
data = []
conceptos = doc.find('{}Conceptos'.format(PRE[version]))
for c in conceptos.getchildren():
values = CaseInsensitiveDict(c.attrib.copy())
if version == '3.3':
values['noidentificacion'] = '{}\n(SAT {})'.format(
values['noidentificacion'], values['ClaveProdServ'])
values['unidad'] = '({})\n{}'.format(
values['ClaveUnidad'], values['unidad'])
data.append(values)
return data
def _totales(doc, cfdi, version):
data = {}
data['moneda'] = doc.attrib['Moneda']
data['subtotal'] = cfdi['subtotal']
if 'descuento' in cfdi:
data['descuento'] = cfdi['descuento']
data['total'] = cfdi['total']
tn = {
'001': 'ISR',
'002': 'IVA',
'003': 'IEPS',
}
traslados = []
retenciones = []
taxlocales = []
imp = doc.find('{}Impuestos'.format(PRE[version]))
if imp is not None:
tmp = CaseInsensitiveDict(imp.attrib.copy())
for k, v in tmp.items():
data[k] = v
node = imp.find('{}Traslados'.format(PRE[version]))
if node is not None:
for n in node.getchildren():
tmp = CaseInsensitiveDict(n.attrib.copy())
if version == '3.3':
title = 'Traslado {} {}'.format(
tn.get(tmp['impuesto']), tmp['tasaocuota'])
else:
title = 'Traslado {} {}'.format(tmp['impuesto'], tmp['tasa'])
traslados.append((title, float(tmp['importe'])))
node = imp.find('{}Retenciones'.format(PRE[version]))
if node is not None:
for n in node.getchildren():
tmp = CaseInsensitiveDict(n.attrib.copy())
if version == '3.3':
title = 'Retención {} {}'.format(
tn.get(tmp['impuesto']), '')
else:
title = 'Retención {} {}'.format(tmp['impuesto'], '')
retenciones.append((title, float(tmp['importe'])))
#~ com = xml.find('%sComplemento' % PRE)
#~ if com is not None:
#~ otros = com.find('%sImpuestosLocales' % IMP_LOCAL)
#~ if otros is not None:
#~ for otro in list(otros):
#~ if otro.tag == '%sRetencionesLocales' % IMP_LOCAL:
#~ name = 'ImpLocRetenido'
#~ tasa = 'TasadeRetencion'
#~ else:
#~ name = 'ImpLocTrasladado'
#~ tasa = 'TasadeTraslado'
#~ title = '%s %s %%' % (otro.attrib[name], otro.attrib[tasa])
#~ value = otro.attrib['Importe']
#~ self._copy_cell(cell_title)
#~ self._copy_cell(cell_value)
#~ cell_title = self._set_cell(v=title, cell=cell_title)
#~ cell_value = self._set_cell(v=value, cell=cell_value, value=True)
#~ cell_value.CellStyle = currency
data['traslados'] = traslados
data['retenciones'] = retenciones
data['taxlocales'] = taxlocales
return data
def _timbre(doc, version, values):
CADENA = '||{version}|{UUID}|{FechaTimbrado}|{selloCFD}|{noCertificadoSAT}||'
if version == '3.3':
CADENA = '||{Version}|{UUID}|{FechaTimbrado}|{SelloCFD}|{NoCertificadoSAT}||'
node = doc.find('{}Complemento/{}TimbreFiscalDigital'.format(
PRE[version], PRE['TIMBRE']))
data = CaseInsensitiveDict(node.attrib.copy())
total_s = '%017.06f' % float(values['total'])
qr_data = '?re=%s&rr=%s&tt=%s&id=%s' % (
values['rfc_emisor'],
values['rfc_receptor'],
total_s,
node.attrib['UUID'])
data['path_cbb'] = get_qr(qr_data)
data['cadenaoriginal'] = CADENA.format(**node.attrib)
return data
def get_data_from_xml(invoice, values):
data = {'cancelada': invoice.cancelada}
doc = parse_xml(invoice.xml)
data['comprobante'] = _comprobante(doc.attrib.copy(), values)
version = data['comprobante']['version']
data['emisor'] = _emisor(doc, version, values)
data['receptor'] = _receptor(doc, version, values)
data['conceptos'] = _conceptos(doc, version)
data['totales'] = _totales(doc, data['comprobante'], version)
options = {
'rfc_emisor': data['emisor']['rfc'],
'rfc_receptor': data['receptor']['rfc'],
'total': data['comprobante']['total'],
}
data['timbre'] = _timbre(doc, version, options)
del data['timbre']['version']
data['comprobante'].update(data['timbre'])
return data
def to_zip(*files):
zip_buffer = BytesIO()
with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED, False) as zip_file:
for data, file_name in files:
zip_file.writestr(file_name, data)
return zip_buffer.getvalue()
def make_fields(xml):
doc = ET.fromstring(xml)
data = CaseInsensitiveDict(doc.attrib.copy())
data.pop('certificado')
data.pop('sello')
version = data['version']
receptor = doc.find('{}Receptor'.format(PRE[version]))
receptor = CaseInsensitiveDict(receptor.attrib.copy())
data['receptor_nombre'] = receptor['nombre']
data['receptor_rfc'] = receptor['rfc']
data = {k.lower(): v for k, v in data.items()}
return data
def make_info_mail(data, fields):
return data.format(**fields).replace('\n', '<br/>')
def send_mail(data):
msg = ''
server = SendMail(data['server'])
is_connect = server.is_connect
if is_connect:
msg = server.send(data['options'])
else:
msg = server.error
server.close()
return {'ok': is_connect, 'msg': msg}
def get_path_info(path):
path, filename = os.path.split(path)
name, extension = os.path.splitext(filename)
return (path, filename, name, extension)
def get_path_temp():
return tempfile.mkstemp()[1]
class ImportFacturaLibre(object):
def __init__(self, path):
self._con = None
self._cursor = None
self._is_connect = self._connect(path)
@property
def is_connect(self):
return self._is_connect
def _connect(self, path):
try:
self._con = sqlite3.connect(path)
self._con.row_factory = sqlite3.Row
self._cursor = self._con.cursor()
return True
except Exception as e:
log.error(e)
return False
def close(self):
try:
self._cursor.close()
self._con.close()
except:
pass
return
def import_data(self):
data = {}
tables = (
('receptores', 'Socios'),
)
for source, target in tables:
data[target] = self._get_table(source)
return data
def _get_table(self, table):
return getattr(self, '_{}'.format(table))()
def _receptores(self):
sql = "SELECT * FROM receptores"
self._cursor.execute(sql)
rows = self._cursor.fetchall()
#~ names = [d[0] for d in self._cursor.description]
fields = (
('id', 'id'),
('rfc', 'rfc'),
('nombre', 'nombre'),
('calle', 'calle'),
('noExterior', 'no_exterior'),
('noInterior', 'no_interior'),
('colonia', 'colonia'),
('municipio', 'municipio'),
('estado', 'estado'),
('pais', 'pais'),
('codigoPostal', 'codigo_postal'),
('extranjero', 'es_extranjero'),
('activo', 'es_activo'),
('fechaalta', 'fecha_alta'),
('notas', 'notas'),
('cuentaCliente', 'cuenta_cliente'),
('cuentaProveedor', 'cuenta_proveedor'),
('saldoCliente', 'saldo_cliente'),
('saldoProveedor', 'saldo_proveedor'),
('esCliente', 'es_cliente'),
('esProveedor', 'es_proveedor'),
)
data = []
sql1 = "SELECT correo FROM correos WHERE id_cliente=?"
sql2 = "SELECT telefono FROM telefonos WHERE id_cliente=?"
for row in rows:
new = {t: row[s] for s, t in fields}
new['slug'] = to_slug(new['nombre'])
if new['es_extranjero']:
new['tipo_persona'] = 4
elif new['rfc'] == 'XAXX010101000':
new['tipo_persona'] = 3
elif len(new['rfc']) == 12:
new['tipo_persona'] = 2
self._cursor.execute(sql1, (new['id'],))
tmp = self._cursor.fetchall()
if tmp:
new['correo_facturas'] = ', '.join([r[0] for r in tmp])
self._cursor.execute(sql2, (new['id'],))
tmp = self._cursor.fetchall()
if tmp:
new['telefonos'] = ', '.join([r[0] for r in tmp])
data.append(new)
return data

View File

@ -13,7 +13,7 @@ from middleware import (
)
from models.db import StorageEngine
from controllers.main import (
AppLogin, AppLogout, AppAdmin, AppEmisor,
AppLogin, AppLogout, AppAdmin, AppEmisor, AppConfig,
AppMain, AppValues, AppPartners, AppProducts, AppInvoices, AppFolios,
AppDocumentos
)
@ -38,6 +38,7 @@ api.add_route('/emisor', AppEmisor(db))
api.add_route('/folios', AppFolios(db))
api.add_route('/main', AppMain(db))
api.add_route('/values/{table}', AppValues(db))
api.add_route('/config', AppConfig(db))
api.add_route('/doc/{type_doc}/{id_doc}', AppDocumentos(db))
api.add_route('/partners', AppPartners(db))
api.add_route('/products', AppProducts(db))

View File

@ -14,12 +14,24 @@ class StorageEngine(object):
def get_values(self, table, values=None):
return getattr(self, '_get_{}'.format(table))(values)
def get_config(self, values):
return main.Configuracion.get_(values)
def add_config(self, values):
return main.Configuracion.add(values)
def add_cert(self, file_object):
return main.Certificado.add(file_object)
def validate_cert(self, values, session):
return main.Certificado.validate(values, session)
def validate_email(self, values):
return main.test_correo(values)
def send_email(self, values, session):
return main.Facturas.send(values['id'], session['rfc'])
def _get_cert(self, values):
return main.Certificado.get_data()
@ -115,9 +127,15 @@ class StorageEngine(object):
def add_folios(self, values):
return main.Folios.add(values)
def get_doc(self, type_doc, id):
def get_doc(self, type_doc, id, rfc):
if type_doc == 'xml':
data, file_name = main.Facturas.get_xml(id)
content_type = 'application.xml'
content_type = 'application/xml'
if type_doc == 'pdf':
data, file_name = main.Facturas.get_pdf(id, rfc)
content_type = 'application/pdf'
if type_doc == 'zip':
data, file_name = main.Facturas.get_zip(id, rfc)
content_type = 'application/octet-stream'
return data, file_name, content_type

View File

@ -14,7 +14,9 @@ if __name__ == '__main__':
from controllers import util
from settings import log, VERSION, PATH_CP, COMPANIES
from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \
INIT_VALUES
FORMAT = '{0:.2f}'
@ -59,9 +61,34 @@ def desconectar():
class Configuracion(BaseModel):
clave = TextField()
clave = TextField(unique=True)
valor = TextField(default='')
@classmethod
def get_(cls, keys):
if keys['fields'] == 'correo':
fields = ('correo_servidor', 'correo_puerto', 'correo_ssl',
'correo_usuario', 'correo_contra', 'correo_copia',
'correo_asunto', 'correo_mensaje', 'correo_directo')
data = (Configuracion
.select()
.where(Configuracion.clave.in_(fields))
)
values = {r.clave: r.valor for r in data}
return values
@classmethod
def add(cls, values):
try:
for k, v in values.items():
obj, created = Configuracion.get_or_create(clave=k)
obj.valor = v
obj.save()
return {'ok': True}
except Exception as e:
log.error(str(e))
return {'ok': False, 'msg': str(e)}
class Meta:
order_by = ('clave',)
indexes = (
@ -110,6 +137,9 @@ class SATRegimenes(BaseModel):
(('key', 'name'), True),
)
def __str__(self):
return '{} ({})'.format(self.name, self.key)
@classmethod
def get_(cls, ids):
if isinstance(ids, int):
@ -155,6 +185,10 @@ class Emisor(BaseModel):
correo = TextField(default='')
web = TextField(default='')
curp = TextField(default='')
correo_timbrado = TextField(default='')
token_timbrado = TextField(default='')
token_soporte = TextField(default='')
logo = TextField(default='')
regimenes = ManyToManyField(SATRegimenes, related_name='emisores')
def __str__(self):
@ -184,6 +218,7 @@ class Emisor(BaseModel):
'emisor_municipio': obj.municipio,
'emisor_estado': obj.estado,
'emisor_pais': obj.pais,
'emisor_logo': obj.logo,
'emisor_nombre_comercial': obj.nombre_comercial,
'emisor_telefono': obj.telefono,
'emisor_correo': obj.correo,
@ -193,6 +228,9 @@ class Emisor(BaseModel):
'ong_autorizacion': obj.autorizacion,
'ong_fecha': obj.fecha_autorizacion,
'ong_fecha_dof': obj.fecha_dof,
'correo_timbrado': obj.correo_timbrado,
'token_timbrado': obj.token_timbrado,
'token_soporte': obj.token_soporte,
'regimenes': [row.id for row in obj.regimenes]
}
else:
@ -200,6 +238,14 @@ class Emisor(BaseModel):
return {'ok': True, 'row': row}
@classmethod
def get_auth(cls):
try:
obj = Emisor.select()[0]
return {'USER': obj.correo_timbrado, 'PASS': obj.token_timbrado}
except:
return {}
@classmethod
def get_regimenes(cls):
obj = Emisor.select()[0]
@ -216,6 +262,7 @@ class Emisor(BaseModel):
fields['municipio'] = fields.pop('emisor_municipio', '')
fields['estado'] = fields.pop('emisor_estado', '')
fields['pais'] = fields.pop('emisor_pais', 'México')
fields['logo'] = fields.pop('emisor_logo', '')
fields['nombre_comercial'] = fields.pop('emisor_nombre_comercial', '')
fields['telefono'] = fields.pop('emisor_telefono', '')
fields['correo'] = fields.pop('emisor_correo', '')
@ -458,6 +505,9 @@ class SATFormaPago(BaseModel):
(('key', 'name'), True),
)
def __str__(self):
return 'Forma de pago: ({}) {}'.format(self.key, self.name)
@classmethod
def get_activos(cls, values):
field = SATFormaPago.id
@ -496,6 +546,9 @@ class SATMonedas(BaseModel):
(('key', 'name'), True),
)
def __str__(self):
return 'Moneda: ({}) {}'.format(self.key, self.name)
@classmethod
def get_activos(cls):
rows = (SATMonedas
@ -529,6 +582,22 @@ class SATImpuestos(BaseModel):
return tuple(rows)
class SATTipoRelacion(BaseModel):
key = TextField(index=True, unique=True)
name = TextField(default='', index=True)
activo = BooleanField(default=False)
default = BooleanField(default=False)
class Meta:
order_by = ('-default', 'name',)
indexes = (
(('key', 'name'), True),
)
def __str__(self):
return 'Tipo de relación: ({}) {}'.format(self.key, self.name)
class SATUsoCfdi(BaseModel):
key = TextField(index=True, unique=True)
name = TextField(default='', index=True)
@ -543,6 +612,9 @@ class SATUsoCfdi(BaseModel):
(('key', 'name'), True),
)
def __str__(self):
return 'Uso del CFDI: {} ({})'.format(self.name, self.key)
@classmethod
def get_activos(cls, values):
field = SATUsoCfdi.id
@ -594,6 +666,8 @@ class Socios(BaseModel):
es_proveedor = BooleanField(default=False)
cuenta_cliente = TextField(default='')
cuenta_proveedor = TextField(default='')
saldo_cliente = DecimalField(default=0.0, decimal_places=6, auto_round=True)
saldo_proveedor = DecimalField(default=0.0, decimal_places=6, auto_round=True)
web = TextField(default='')
correo_facturas = TextField(default='')
forma_pago = ForeignKeyField(SATFormaPago, null=True)
@ -644,12 +718,12 @@ class Socios(BaseModel):
Socios.id, Socios.nombre, Socios.rfc,
SATFormaPago.key.alias('forma_pago'),
SATUsoCfdi.key.alias('uso_cfdi'))
.join(SATFormaPago).switch(Socios)
.join(SATUsoCfdi).switch(Socios)
.where(
(Socios.id==id) & (Socios.es_cliente==True))
.join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios)
.join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios)
.where((Socios.id==id) & (Socios.es_cliente==True))
.dicts()
)
print (id, row)
if len(row):
return {'ok': True, 'row': row[0]}
return {'ok': False}
@ -660,8 +734,8 @@ class Socios(BaseModel):
.select(Socios.id, Socios.nombre, Socios.rfc,
SATFormaPago.key.alias('forma_pago'),
SATUsoCfdi.key.alias('uso_cfdi'))
.join(SATFormaPago).switch(Socios)
.join(SATUsoCfdi).switch(Socios)
.join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios)
.join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios)
.where((Socios.es_cliente==True) &
(Socios.rfc.contains(name) |
Socios.nombre.contains(name)))
@ -724,7 +798,7 @@ class Socios(BaseModel):
class Productos(BaseModel):
categoria = ForeignKeyField(Categorias, null=True)
clave = TextField(unique=True, index=True)
clave_sat = TextField()
clave_sat = TextField(default='')
descripcion = TextField(index=True)
unidad = ForeignKeyField(SATUnidades)
valor_unitario = DecimalField(default=0.0, decimal_places=6, auto_round=True)
@ -901,6 +975,7 @@ class Productos(BaseModel):
class Facturas(BaseModel):
cliente = ForeignKeyField(Socios)
version = TextField(default=CURRENT_CFDI)
serie = TextField(default='')
folio = IntegerField(default=0)
fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S'])
@ -928,6 +1003,9 @@ class Facturas(BaseModel):
regimen_fiscal = TextField(default='')
notas = TextField(default='')
pagada = BooleanField(default=False)
cancelada = BooleanField(default=False)
donativo = BooleanField(default=False)
tipo_relacion = TextField(default='')
error = TextField(default='')
class Meta:
@ -939,6 +1017,158 @@ class Facturas(BaseModel):
name = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc)
return obj.xml, name
#~ Revisar
def _get_data_cfdi_to_pdf(self, xml, cancel, version):
pre_nomina = PRE['NOMINA'][version]
data['nomina'] = {}
node = doc.find('{}Complemento/{}Nomina'.format(pre, pre_nomina))
if not node is None:
data['nomina']['nomina'] = node.attrib.copy()
subnode = node.find('{}Emisor'.format(pre_nomina))
if not subnode is None:
data['emisor'].update(subnode.attrib.copy())
subnode = node.find('{}Receptor'.format(pre_nomina))
data['receptor'].update(subnode.attrib.copy())
subnode = node.find('{}Percepciones'.format(pre_nomina))
data['nomina']['percepciones'] = subnode.attrib.copy()
detalle = []
for n in subnode.getchildren():
if 'SeparacionIndemnizacion' in n.tag:
continue
detalle.append(n.attrib.copy())
data['nomina']['percepciones']['detalle'] = detalle
data['nomina']['deducciones'] = None
subnode = node.find('{}Deducciones'.format(pre_nomina))
if not subnode is None:
data['nomina']['deducciones'] = subnode.attrib.copy()
detalle = []
for n in subnode.getchildren():
detalle.append(n.attrib.copy())
data['nomina']['deducciones']['detalle'] = detalle
data['nomina']['incapacidades'] = None
subnode = node.find('{}Incapacidades'.format(pre_nomina))
if not subnode is None:
detalle = []
for n in subnode.getchildren():
detalle.append(n.attrib.copy())
data['nomina']['incapacidades'] = detalle
data['nomina']['otrospagos'] = None
subnode = node.find('{}OtrosPagos'.format(pre_nomina))
if not subnode is None:
data['nomina']['otrospagos'] = subnode.attrib.copy()
detalle = []
for n in subnode.getchildren():
detalle.append(n.attrib.copy())
ns = n.find('{}SubsidioAlEmpleo'.format(pre_nomina))
if not ns is None:
data['nomina']['otrospagos']['SubsidioCausado'] = ns.attrib['SubsidioCausado']
data['nomina']['otrospagos']['detalle'] = detalle
return data
def _get_not_in_xml(self, invoice):
values = {}
obj = SATRegimenes.get(SATRegimenes.key==invoice.regimen_fiscal)
values['regimenfiscal'] = str(obj)
obj = SATUsoCfdi.get(SATUsoCfdi.key==invoice.uso_cfdi)
values['usocfdi'] = str(obj)
mp = {
'PUE': 'Pago en una sola exhibición',
'PPD': 'Pago en parcialidades o diferido',
}
values['metododepago'] = 'Método de Pago: ({}) {}'.format(
invoice.metodo_pago, mp[invoice.metodo_pago])
obj = SATFormaPago.get(SATFormaPago.key==invoice.forma_pago)
values['formadepago'] = str(obj)
obj = SATMonedas.get(SATMonedas.key==invoice.moneda)
values['moneda'] = str(obj)
return values
@classmethod
def get_pdf(cls, id, rfc):
obj = Facturas.get(Facturas.id==id)
name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc)
if obj.uuid is None:
return b'', name
values = cls._get_not_in_xml(cls, obj)
data = util.get_data_from_xml(obj, values)
doc = util.to_pdf(data)
return doc, name
@classmethod
def get_zip(cls, id, rfc):
obj = Facturas.get(Facturas.id==id)
name_zip = '{}{}_{}.zip'.format(obj.serie, obj.folio, obj.cliente.rfc)
if obj.uuid is None:
return b'', name_zip
file_xml = cls.get_xml(id)
if not file_xml[0]:
return b'', name_zip
file_pdf = cls.get_pdf(id, rfc)
if not file_pdf[0]:
return b'', name_zip
file_zip = util.to_zip(file_xml, file_pdf)
return file_zip, name_zip
@classmethod
def send(cls, id, rfc):
values = Configuracion.get_({'fields': 'correo'})
if not values:
msg = 'No esta configurado el servidor de correo de salida'
return {'ok': False, 'msg': msg}
obj = Facturas.get(Facturas.id==id)
if obj.uuid is None:
msg = 'La factura no esta timbrada'
return {'ok': False, 'msg': msg}
if not obj.cliente.correo_facturas:
msg = 'El cliente no tiene configurado el correo para facturas'
return {'ok': False, 'msg': msg}
files = (cls.get_zip(id, rfc),)
fields = util.make_fields(obj.xml)
server = {
'servidor': values['correo_servidor'],
'puerto': values['correo_puerto'],
'ssl': bool(int(values['correo_ssl'])),
'usuario': values['correo_usuario'],
'contra': values['correo_contra'],
}
options = {
'para': obj.cliente.correo_facturas,
'copia': values['correo_copia'],
'asunto': util.make_info_mail(values['correo_asunto'], fields),
'mensaje': util.make_info_mail(values['correo_mensaje'], fields),
'files': files,
}
data= {
'server': server,
'options': options,
}
result = util.send_mail(data)
if not result['ok'] or result['msg']:
return {'ok': False, 'msg': result['msg']}
msg = 'Factura enviada correctamente'
return {'ok': True, 'msg': msg}
@classmethod
def get_(cls, values):
rows = tuple(Facturas
@ -1214,14 +1444,17 @@ class Facturas(BaseModel):
obj.estatus = 'Generada'
obj.save()
auth = Emisor.get_auth()
error = False
msg = 'Factura timbrada correctamente'
result = util.timbra_xml(obj.xml)
result = util.timbra_xml(obj.xml, auth)
if result['ok']:
obj.xml = result['xml']
obj.uuid = result['uuid']
obj.fecha_timbrado = result['fecha']
obj.estatus = 'Timbrada'
obj.error = ''
obj.save()
row = {'uuid': obj.uuid, 'estatus': 'Timbrada'}
else:
@ -1234,6 +1467,17 @@ class Facturas(BaseModel):
return {'ok': result['ok'], 'msg': msg, 'row': row}
class FacturasRelacionadas(BaseModel):
factura = ForeignKeyField(Facturas, related_name='original')
factura_origen = ForeignKeyField(Facturas, related_name='relacion')
class Meta:
order_by = ('factura',)
indexes = (
(('factura', 'factura_origen'), True),
)
class FacturasDetalle(BaseModel):
factura = ForeignKeyField(Facturas)
producto = ForeignKeyField(Productos, null=True)
@ -1330,26 +1574,51 @@ def get_sat_key(key):
return util.get_sat_key('products', key)
def test_correo(values):
server = {
'servidor': values['correo_servidor'],
'puerto': values['correo_puerto'],
'ssl': bool(values['correo_ssl'].replace('0', '')),
'usuario': values['correo_usuario'],
'contra': values['correo_contra'],
}
options = {
'para': values['correo_usuario'],
'copia': values['correo_copia'],
'asunto': values['correo_asunto'],
'mensaje': values['correo_mensaje'].replace('\n', '<br/>'),
'files': [],
}
data= {
'server': server,
'options': options,
}
return util.send_mail(data)
def _init_values():
data = (
{'key': 'version', 'value': VERSION},
{'key': 'rfc_publico', 'value': 'XAXX010101000'},
{'key': 'rfc_extranjero', 'value': 'XEXX010101000'},
{'key': 'decimales', 'value': '2'},
)
for row in data:
try:
Configuration.create(**row)
Configuracion.create(**row)
except IntegrityError:
pass
log.info('Valores iniciales insertados...')
return
def _crear_tablas():
def _crear_tablas(rfc):
tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion,
Emisor, Facturas, FacturasDetalle, FacturasImpuestos, Folios, Productos,
Emisor, Facturas, FacturasDetalle, FacturasImpuestos, Folios,
FacturasRelacionadas, Productos,
SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes,
SATUnidades, SATUsoCfdi, Socios, Tags, Usuarios,
SATTipoRelacion, SATUnidades, SATUsoCfdi,
Socios, Tags, Usuarios,
Emisor.regimenes.get_through_model(),
Socios.tags.get_through_model(),
Productos.impuestos.get_through_model(),
@ -1357,6 +1626,21 @@ def _crear_tablas():
]
database_proxy.create_tables(tablas, True)
log.info('Tablas creadas correctamente...')
try:
usuario = 'admin'
contraseña = 'blades3.3'
obj = Usuarios.create(
usuario=usuario, contraseña=contraseña, es_superusuario=True)
log.info('SuperUsuario creado correctamente...')
except IntegrityError:
msg = 'El usuario ya existe'
log.error(msg)
pass
_init_values()
_importar_valores('', rfc)
return True
@ -1425,6 +1709,7 @@ def _cambiar_contraseña():
def _add_emisor(rfc, args):
util._valid_db_companies()
con = sqlite3.connect(COMPANIES)
cursor = con.cursor()
sql = """
@ -1442,6 +1727,25 @@ def _add_emisor(rfc, args):
return True
def _delete_emisor(rfc):
util._valid_db_companies()
con = sqlite3.connect(COMPANIES)
cursor = con.cursor()
sql = """
DELETE FROM names
WHERE rfc = ?"""
try:
cursor.execute(sql, (rfc,))
except Exception as e:
log.error(e)
return False
con.commit()
cursor.close()
con.close()
return True
def _iniciar_bd():
rfc = input('Introduce el RFC: ').strip().upper()
if not rfc:
@ -1454,7 +1758,7 @@ def _iniciar_bd():
return
conectar(args)
if _crear_tablas():
if _crear_tablas(rfc):
return
log.error('No se pudieron crear las tablas')
@ -1481,7 +1785,7 @@ def _agregar_rfc():
args = opt.copy()
if conectar(args):
if _add_emisor(rfc, util.dumps(opt)) and _crear_tablas():
if _add_emisor(rfc, util.dumps(opt)) and _crear_tablas(rfc):
log.info('RFC agregado correctamente...')
return
@ -1489,6 +1793,23 @@ def _agregar_rfc():
return
def _borrar_rfc():
rfc = input('Introduce el RFC a borrar: ').strip().upper()
if not rfc:
msg = 'El RFC es requerido'
log.error(msg)
return
confirm = input('¿Estás seguro de borrar el RFC?')
if _delete_emisor(rfc):
log.info('RFC borrado correctamente...')
return
log.error('No se pudo borrar el RFC')
return
def _listar_rfc():
data = util.get_rfcs()
for row in data:
@ -1497,7 +1818,40 @@ def _listar_rfc():
return
def _importar_valores(archivo):
def _importar_valores(archivo='', rfc=''):
if not rfc:
rfc = input('Introduce el RFC: ').strip().upper()
if not rfc:
msg = 'El RFC es requerido'
log.error(msg)
return
args = util.get_con(rfc)
if not args:
return
conectar(args)
if not archivo:
archivo = INIT_VALUES
log.info('Importando datos...')
regimen = ''
rows = util.loads(open(archivo, 'r').read())
for row in rows:
log.info('\tImportando tabla: {}'.format(row['tabla']))
table = globals()[row['tabla']]
for r in row['datos']:
try:
table.create(**r)
except IntegrityError:
pass
log.info('Importación terminada...')
return
def _importar_factura_libre(archivo):
rfc = input('Introduce el RFC: ').strip().upper()
if not rfc:
msg = 'El RFC es requerido'
@ -1511,24 +1865,21 @@ def _importar_valores(archivo):
conectar(args)
log.info('Importando datos...')
regimen = ''
rows = util.loads(open(archivo, 'r').read())
for row in rows:
log.info('\tImportando tabla: {}'.format(row['tabla']))
if row['tabla'] == 'Emisor' and 'regimen' in row:
regimen = row['regimen']
table = globals()[row['tabla']]
for r in row['datos']:
try:
table.create(**r)
except IntegrityError:
pass
app = util.ImportFacturaLibre(archivo)
if not app.is_connect:
log.error('\tNo se pudo conectar a la base de datos')
return
if regimen:
emisor = Emisor.select()[0]
regimen = SATRegimenes.get(SATRegimenes.key == regimen)
emisor.regimenes.clear()
emisor.regimenes.add(regimen)
data = app.import_data()
for table, rows in data.items():
log.info('\tImportando: {}'.format(table))
model = globals()[table]
for row in rows:
try:
model.create(**row)
except IntegrityError:
msg = '\t{}'.format(str(row))
log.error(msg)
log.info('Importación terminada...')
return
@ -1555,10 +1906,11 @@ help_lr = 'Listar RFCs'
@click.option('-rfc', '--rfc', help=help_rfc, is_flag=True, default=False)
@click.option('-br', '--borrar-rfc', help=help_br, is_flag=True, default=False)
@click.option('-lr', '--listar-rfc', help=help_lr, is_flag=True, default=False)
@click.option('-i', '--importar_valores', is_flag=True, default=False)
@click.option('-i', '--importar-valores', is_flag=True, default=False)
@click.option('-a', '--archivo')
@click.option('-fl', '--factura-libre', is_flag=True, default=False)
def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, rfc,
borrar_rfc, listar_rfc, importar_valores, archivo):
borrar_rfc, listar_rfc, importar_valores, archivo, factura_libre):
opt = locals()
if opt['iniciar_bd']:
@ -1600,6 +1952,21 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, rfc,
_importar_valores(opt['archivo'])
sys.exit(0)
if opt['factura_libre']:
if not opt['archivo']:
msg = 'Falta la ruta de la base de datos'
raise click.ClickException(msg)
if not util.is_file(opt['archivo']):
msg = 'No es un archivo'
raise click.ClickException(msg)
_, _, _, ext = util.get_path_info(opt['archivo'])
if ext != '.sqlite':
msg = 'No es una base de datos'
raise click.ClickException(msg)
_importar_factura_libre(opt['archivo'])
sys.exit(0)
return

View File

@ -7,19 +7,25 @@ from mako.lookup import TemplateLookup
from logbook import Logger, StreamHandler, RotatingFileHandler
logbook.set_datetime_format('local')
from conf import DEBUG
DEBUG = True
DEBUG = DEBUG
VERSION = '0.1.0'
EMAIL_SUPPORT = ('soporte@empresalibre.net',)
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
PATH_STATIC = os.path.abspath(os.path.join(BASE_DIR, '..'))
PATH_TEMPLATES = os.path.abspath(os.path.join(BASE_DIR, '..', 'templates'))
PATH_MEDIA = os.path.abspath(os.path.join(BASE_DIR, '..', 'docs'))
PATH_CP = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'cp.db'))
COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db'))
DB_SAT = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'sat.db'))
IV = 'valores_iniciales.json'
INIT_VALUES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', IV))
PATH_XSLT = os.path.abspath(os.path.join(BASE_DIR, '..', 'xslt'))
PATH_BIN = os.path.abspath(os.path.join(BASE_DIR, '..', 'bin'))
@ -36,6 +42,7 @@ format_string = '[{record.time:%d-%b-%Y %H:%M:%S}] ' \
'{record.channel}: ' \
'{record.message}'
if DEBUG:
LOG_LEVEL = 'DEBUG'
StreamHandler(
@ -51,6 +58,12 @@ else:
level=LOG_LEVEL,
format_string=format_string).push_application()
StreamHandler(
sys.stdout,
level=LOG_LEVEL,
format_string=format_string).push_application()
log = Logger(LOG_NAME)
@ -59,3 +72,19 @@ PATH_OPENSSL = 'openssl'
if 'win' in sys.platform:
PATH_XSLTPROC = os.path.join(PATH_BIN, 'xsltproc.exe')
PATH_OPENSSL = os.path.join(PATH_BIN, 'openssl.exe')
PRE = {
'2.0': '{http://www.sat.gob.mx/cfd/2}',
'2.2': '{http://www.sat.gob.mx/cfd/2}',
'3.0': '{http://www.sat.gob.mx/cfd/3}',
'3.2': '{http://www.sat.gob.mx/cfd/3}',
'3.3': '{http://www.sat.gob.mx/cfd/3}',
'TIMBRE': '{http://www.sat.gob.mx/TimbreFiscalDigital}',
'NOMINA': {
'1.1': '{http://www.sat.gob.mx/nomina}',
'1.2': '{http://www.sat.gob.mx/nomina12}',
}
}
CURRENT_CFDI = '3.3'

Binary file not shown.

View File

@ -23,7 +23,20 @@
{"key": "HUR", "name": "Hora", "activo": true},
{"key": "H87", "name": "Pieza", "activo": true},
{"key": "E48", "name": "Servicio", "activo": true},
{"key": "E51", "name": "Trabajo", "activo": false}
{"key": "E51", "name": "Trabajo", "activo": false},
{"key": "ACT", "name": "Actividad", "activo": false}
]
},
{
"tabla": "SATTipoRelacion",
"datos": [
{"key": "01", "name": "Nota de crédito de los documentos relacionados", "activo": true},
{"key": "02", "name": "Nota de débito de los documentos relacionados", "activo": true},
{"key": "03", "name": "Devolución de mercancía sobre facturas o traslados previos", "activo": true},
{"key": "04", "name": "Sustitución de los CFDI previos", "activo": true, "default": true},
{"key": "05", "name": "Traslados de mercancias facturados previamente", "activo": true},
{"key": "06", "name": "Factura generada por los traslados previos", "activo": true},
{"key": "07", "name": "Actividad", "CFDI por aplicación de anticipo": true}
]
},
{

View File

@ -16,6 +16,8 @@ var controllers = {
$$('up_cert').attachEvent('onUploadComplete', up_cert_upload_complete)
$$('cmd_agregar_serie').attachEvent('onItemClick', cmd_agregar_serie_click)
$$('grid_folios').attachEvent('onItemClick', grid_folios_click)
$$('cmd_probar_correo').attachEvent('onItemClick', cmd_probar_correo_click)
$$('cmd_guardar_correo').attachEvent('onItemClick', cmd_guardar_correo_click)
}
}
@ -133,7 +135,9 @@ function get_emisor(){
var emisor = values.row.emisor
$$('lst_emisor_regimen').parse(values.row.regimenes)
form.setValues(emisor, true)
$$('lst_emisor_regimen').select(emisor.regimenes)
if(emisor.regimenes){
$$('lst_emisor_regimen').select(emisor.regimenes)
}
}else{
msg_error(values.msg)
}
@ -175,6 +179,24 @@ function get_table_folios(){
}
function get_config_correo(){
var form = $$('form_correo')
var fields = form.getValues()
webix.ajax().get('/config', {'fields': 'correo'}, {
error: function(text, data, xhr) {
msg = 'Error al consultar'
msg_error(msg)
},
success: function(text, data, xhr) {
var values = data.json()
form.setValues(values)
}
})
}
function multi_admin_change(prevID, nextID){
//~ webix.message(nextID)
if(nextID == 'app_emisor'){
@ -188,6 +210,11 @@ function multi_admin_change(prevID, nextID){
get_table_folios()
return
}
if(nextID == 'app_correo'){
get_config_correo()
return
}
}
@ -442,10 +469,120 @@ function grid_folios_click(id, e, node){
msg_error(msg)
}
})
}
}
})
}
function validar_correo(values){
if(!values.correo_servidor.trim()){
msg = 'El servidor de salida no puede estar vacío'
msg_error(msg)
return false
}
if(!values.correo_puerto){
msg = 'El puerto no puede ser cero'
msg_error(msg)
return false
}
if(!values.correo_usuario.trim()){
msg = 'El nombre de usuario no puede estar vacío'
msg_error(msg)
return false
}
if(!values.correo_contra.trim()){
msg = 'La contraseña no puede estar vacía'
msg_error(msg)
return false
}
if(!values.correo_asunto.trim()){
msg = 'El asunto del correo no puede estar vacío'
msg_error(msg)
return false
}
if(!values.correo_mensaje.trim()){
msg = 'El mensaje del correo no puede estar vacío'
msg_error(msg)
return false
}
return true
}
function cmd_probar_correo_click(){
var form = $$('form_correo')
var values = form.getValues()
if(!validar_correo(values)){
return
}
webix.ajax().sync().post('/values/correo', values, {
error: function(text, data, xhr) {
msg = 'Error al probar el correo'
msg_error(msg)
},
success: function(text, data, xhr) {
var values = data.json();
if (values.ok){
msg = 'Correo de prueba enviado correctamente. Ya puedes \
guardar esta configuración'
msg_sucess(msg)
}else{
msg_error(values.msg)
}
}
})
}
function save_config_mail(values){
webix.ajax().sync().post('/config', values, {
error: function(text, data, xhr) {
msg = 'Error al guardar la configuración'
msg_error(msg)
},
success: function(text, data, xhr) {
var values = data.json();
if (values.ok){
msg = 'Configuración guardada correctamente'
msg_sucess(msg)
}else{
msg_error(values.msg)
}
}
})
}
function cmd_guardar_correo_click(){
var form = $$('form_correo')
var values = form.getValues()
if(!validar_correo(values)){
return
}
msg = 'Asegurate de haber probado la configuración<BR><BR>\
¿Estás seguro de guardar estos datos?'
webix.confirm({
title: 'Configuración de correo',
ok: 'Si',
cancel: 'No',
type: 'confirm-error',
text: msg,
callback:function(result){
if(result){
save_config_mail(values)
}
}
})
}

View File

@ -11,8 +11,8 @@ function get_series(){
pre = values[0]
$$('lst_serie').getList().parse(values)
$$('lst_serie').setValue(pre.id)
if(pre.usar_con){
$$('lst_tipo_comprobante').setValue(pre.usar_con)
if(pre.usarcon){
$$('lst_tipo_comprobante').setValue(pre.usarcon)
$$('lst_tipo_comprobante').config.readonly = true
$$('lst_tipo_comprobante').refresh()
}
@ -42,7 +42,7 @@ function get_monedas(){
$$('lst_moneda').setValue(pre.id)
if(values.length == 1){
$$('fs_moneda').hide()
$$('fs_moneda').refresh()
//~ $$('fs_moneda').refresh()
}
})
}
@ -63,7 +63,7 @@ function get_regimen_fiscal(){
$$('lst_regimen_fiscal').setValue(pre.id)
if(values.length == 1){
$$('fs_regimen_fiscal').hide()
$$('fs_regimen_fiscal').refresh()
//~ $$('fs_regimen_fiscal').refresh()
}
})
}
@ -121,6 +121,9 @@ function delete_invoice(id){
function cmd_delete_invoice_click(id, e, node){
if(gi.count() == 0){
return
}
var row = gi.getSelectedItem()
if (row == undefined){
@ -642,10 +645,90 @@ function cmd_invoice_timbrar_click(){
}
function enviar_correo(row){
if(!row.uuid){
msg_error('La factura no esta timbrada')
return
}
msg = '¿Estás seguro de enviar por correo esta factura?'
webix.confirm({
title: 'Enviar Factura',
ok: 'Si',
cancel: 'No',
type: 'confirm-error',
text: msg,
callback:function(result){
if(result){
webix.ajax().post('/values/sendmail', {'id': row.id}, {
error:function(text, data, XmlHttpRequest){
msg = 'Ocurrio un error, consulta a soporte técnico'
msg_error(msg)
},
success:function(text, data, XmlHttpRequest){
values = data.json();
if(values.ok){
msg_sucess(values.msg)
}else{
msg_error(values.msg)
}
}
})
}
}
})
}
function grid_invoices_click(id, e, node){
var row = this.getItem(id)
if(id.column == 'xml'){
location = '/doc/xml/' + row.id
}else if(id.column == 'pdf'){
location = '/doc/pdf/' + row.id
}else if(id.column == 'zip'){
location = '/doc/zip/' + row.id
}else if(id.column == 'email'){
enviar_correo(row)
}
}
function send_cancel(id){
show(id)
}
function cmd_invoice_cancelar_click(){
if(gi.count() == 0){
return
}
var row = gi.getSelectedItem()
if (row == undefined){
msg_error('Selecciona una factura')
return
}
if(!row.uuid){
msg_error('La factura no esta timbrada, solo es posible cancelar \
facturas timbradas')
return
}
msg = '¿Estás seguro de enviar a cancelar esta factura?<BR><BR> \
ESTA ACCIÓN NO SE PUEDE DESHACER'
webix.confirm({
title: 'Cancelar Factura',
ok: 'Si',
cancel: 'No',
type: 'confirm-error',
text: msg,
callback:function(result){
if(result){
send_cancel(row.id)
}
}
})
}

View File

@ -44,6 +44,7 @@ var controllers = {
$$('grid_details').attachEvent('onBeforeEditStart', grid_details_before_edit_start)
$$('grid_details').attachEvent('onBeforeEditStop', grid_details_before_edit_stop)
$$('cmd_invoice_timbrar').attachEvent('onItemClick', cmd_invoice_timbrar_click)
$$('cmd_invoice_cancelar').attachEvent('onItemClick', cmd_invoice_cancelar_click)
$$('grid_invoices').attachEvent('onItemClick', grid_invoices_click)
}
}

View File

@ -3,6 +3,7 @@ var menu_data = [
{id: 'app_home', icon: 'dashboard', value: 'Inicio'},
{id: 'app_emisor', icon: 'user-circle', value: 'Emisor'},
{id: 'app_folios', icon: 'sort-numeric-asc', value: 'Folios'},
{id: 'app_correo', icon: 'envelope-o', value: 'Correo'},
]
@ -61,6 +62,8 @@ var emisor_datos_fiscales = [
var emisor_otros_datos= [
{template: 'Generales', type: 'section'},
{view: 'search', id: 'emisor_logo', icon: 'file-image-o',
name: 'emisor_logo', label: 'Logotipo: '},
{view: 'text', id: 'emisor_nombre_comercial',
name: 'emisor_nombre_comercial', label: 'Nombre comercial: '},
{view: 'text', id: 'emisor_telefono', name: 'emisor_telefono',
@ -86,6 +89,13 @@ var emisor_otros_datos= [
{cols: [{view: 'datepicker', id: 'ong_fecha_dof', name: 'ong_fecha_dof',
label: 'Fecha de DOF: ', disabled: true, format: '%d-%M-%Y',
placeholder: 'Fecha de publicación en el DOF'}, {}]},
{template: 'Timbrado y Soporte', type: 'section'},
{view: 'text', id: 'correo_timbrado',
name: 'correo_timbrado', label: 'Usuario para Timbrado: '},
{view: 'text', id: 'token_timbrado',
name: 'token_timbrado', label: 'Token de Timbrado: '},
{view: 'text', id: 'token_soporte',
name: 'token_soporte', label: 'Token de Soporte: '},
]
@ -205,6 +215,55 @@ var emisor_folios = [
]
var emisor_correo = [
{template: 'Servidor de Salida', type: 'section'},
{cols: [
{view: 'text', id: 'correo_servidor', name: 'correo_servidor',
label: 'Servidor SMTP: '},
{}]},
{cols: [
{view: 'counter', id: 'correo_puerto', name: 'correo_puerto',
label: 'Puerto: ', value: 26, step: 1},
{}]},
{cols: [
{view: 'checkbox', id: 'correo_ssl', name: 'correo_ssl',
label: 'Usar TLS/SSL: '},
{}]},
{cols: [
{view: 'text', id: 'correo_usuario', name: 'correo_usuario',
label: 'Usuario: '},
{}]},
{cols: [
{view: 'text', id: 'correo_contra', name: 'correo_contra',
label: 'Contraseña: ', type: 'password'},
{}]},
{cols: [
{view: 'text', id: 'correo_copia', name: 'correo_copia',
label: 'Con copia a: '}
]},
{cols: [
{view: 'text', id: 'correo_asunto', name: 'correo_asunto',
label: 'Asunto del correo: '}
]},
{cols: [
{view: 'textarea', id: 'correo_mensaje', name: 'correo_mensaje',
label: 'Mensaje del correo: ', height: 200}
]},
{cols: [
{view: 'checkbox', id: 'correo_directo', name: 'correo_directo',
label: 'Enviar directamente: '},
{}]},
{minHeight: 25},
{cols: [{},
{view: 'button', id: 'cmd_probar_correo', label: 'Probar Configuración',
autowidth: true, type: 'form'},
{maxWidth: 100},
{view: 'button', id: 'cmd_guardar_correo', label: 'Guardar Configuración',
autowidth: true, type: 'form'},
{}]}
]
var controls_folios = [
{
view: 'tabview',
@ -219,6 +278,20 @@ var controls_folios = [
]
var controls_correo = [
{
view: 'tabview',
id: 'tab_correo',
tabbar: {options: ['Correo Electrónico']},
animate: true,
cells: [
{id: 'Correo Electrónico', rows: emisor_correo},
{},
]
}
]
var form_folios = {
type: 'space',
cols: [{
@ -239,6 +312,22 @@ var form_folios = {
}
var form_correo = {
type: 'space',
cols: [{
view: 'form',
id: 'form_correo',
complexData: true,
elements: controls_correo,
elementsConfig: {
labelWidth: 150,
labelAlign: 'right'
},
autoheight: true
}]
}
var app_emisor = {
id: 'app_emisor',
rows:[
@ -267,6 +356,17 @@ var app_folios = {
}
var app_correo = {
id: 'app_correo',
rows:[
{view: 'template', id: 'th_correo', type: 'header',
template: 'Configuración de correo'},
form_correo,
{},
]
}
var multi_admin = {
id: 'multi_admin',
animate: true,
@ -278,6 +378,7 @@ var multi_admin = {
},
app_emisor,
app_folios,
app_correo,
]
}

View File

@ -14,6 +14,9 @@ var toolbar_invoices = [
var toolbar_invoices_util = [
{view: 'button', id: 'cmd_invoice_timbrar', label: 'Timbrar',
type: 'iconButton', autowidth: true, icon: 'ticket'},
{},
{view: 'button', id: 'cmd_invoice_cancelar', label: 'Cancelar',
type: 'iconButton', autowidth: true, icon: 'ban'},
]
@ -142,10 +145,10 @@ var suggest_products = {
header: true,
columns: [
{id: 'id', hidden: true},
{id: 'clave', adjust: 'data'},
{id: 'descripcion', adjust: 'data'},
{id: 'unidad', adjust: 'data'},
{id: 'valor_unitario', adjust: 'data',
{id: 'clave', header: 'Clave', adjust: 'data'},
{id: 'descripcion', header: 'Descripción', adjust: 'data'},
{id: 'unidad', header: 'Unidad', adjust: 'data'},
{id: 'valor_unitario', header: 'Valor Unitario', adjust: 'data',
format: webix.i18n.priceFormat}
],
dataFeed:function(text){

View File

@ -131,7 +131,7 @@ var toolbar_contacts = [
var grid_contacts_cols = [
{id: 'index', header:'#', adjust:'data', css:'right',
{id: 'index', header: '#', adjust:'data', css:'right',
footer: {content: 'rowCount'}},
{id: 'id', header: '', hidden: true},
{id: 'title', header: 'Título', adjust:'data', sort: 'string',
@ -160,7 +160,7 @@ var grid_contacts = {
on:{
'data->onStoreUpdated':function(){
this.data.each(function(obj, i){
obj.index = i+1;
obj.index = i + 1
})
}
},

View File

@ -0,0 +1,217 @@
{
"encabezado": {
"emisor": {
"direccion": {
"rectangulo": {"x": 1.0, "y": 26.2, "width": 19.6, "height": 0.4,
"radius": 0.1, "stroke": false, "fill": true, "color": "darkred"}
},
"nombre": {
"rectangulo": {"x": 10.6, "y": 25.6, "width": 10.0, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 14, "alignment": 2, "textColor": "darkred",
"backColor": "white"}
},
"rfc": {
"rectangulo": {"x": 10.6, "y": 25.0, "width": 10.0, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "rfc", "fontName": "Helvetica-Bold",
"fontSize": 12, "alignment": 2, "textColor": "darkred",
"backColor": "white"}
},
"regimenfiscal": {
"rectangulo": {"x": 10.6, "y": 24.4, "width": 10.0, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "regimenfiscal", "fontName": "Helvetica-Bold",
"fontSize": 7, "alignment": 2, "textColor": "darkred",
"backColor": "white"}
},
"logo": {
"rectangulo": {"x": 1.0, "y": 24.2, "width": 2.5, "height": 2.5}
}
},
"receptor": {
"titulo": {
"valor": "Receptor",
"rectangulo": {"x": 1.5, "y": 20.8, "width": 2.8, "height": 0.9,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 9, "alignment": 1, "textColor": "darkred",
"backColor": "linen"}
},
"nombre": {
"rectangulo": {"x": 2.0, "y": 23.2, "width": 15.0, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 10, "alignment": 0, "textColor": "black",
"backColor": "white"}
},
"rfc": {
"rectangulo": {"x": 2.0, "y": 22.5, "width": 10.0, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "rfc", "fontName": "Helvetica-Bold",
"fontSize": 9, "alignment": 0, "textColor": "black",
"backColor": "white"}
},
"usocfdi": {
"rectangulo": {"x": 2.0, "y": 20.7, "width": 15.0, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "usocfdi", "fontName": "Helvetica",
"fontSize": 8, "alignment": 0, "textColor": "black",
"backColor": "white"}
}
},
"comprobante": {
"titulo": {
"valor": "Datos CFDI",
"rectangulo": {"x": 14.0, "y": 20.8, "width": 2.8, "height": 0.9,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 9, "alignment": 1, "textColor": "darkred",
"backColor": "linen"}
},
"t_folio": {
"valor": "Folio:",
"rectangulo": {"x": 14.2, "y": 23.2, "width": 1.0, "height": 0.8,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 8, "alignment": 0, "textColor": "black",
"backColor": "white"}
},
"folio": {
"rectangulo": {"x": 15.1, "y": 23.2, "width": 3.0, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 8, "alignment": 0, "textColor": "red",
"backColor": "white"}
},
"t_tipo": {
"valor": "Tipo:",
"rectangulo": {"x": 18.2, "y": 23.2, "width": 1.0, "height": 0.8,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 8, "alignment": 0, "textColor": "black",
"backColor": "white"}
},
"tipodecomprobante": {
"rectangulo": {"x": 19.0, "y": 23.2, "width": 1.5, "height": 0.8,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 8, "alignment": 0, "textColor": "red",
"backColor": "white"}
},
"t_uuid": {
"valor": "Folio Fiscal:",
"rectangulo": {"x": 14.2, "y": 22.7, "width": 3.0, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 0, "textColor": "black",
"backColor": "white"}
},
"uuid": {
"rectangulo": {"x": 15.6, "y": 22.7, "width": 6.5, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 0, "textColor": "red",
"backColor": "white"}
},
"t_fecha": {
"valor": "Fecha Expedición:",
"rectangulo": {"x": 14.2, "y": 22.1, "width": 2.2, "height": 0.3,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 2, "textColor": "black",
"backColor": "white"}
},
"fecha": {
"rectangulo": {"x": 16.5, "y": 22.1, "width": 3.0, "height": 0.3,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 0, "textColor": "black",
"backColor": "white"}
},
"t_fecha_timbrado": {
"valor": "Fecha Timbrado:",
"rectangulo": {"x": 14.2, "y": 21.8, "width": 2.2, "height": 0.3,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 2, "textColor": "black",
"backColor": "white"}
},
"fechatimbrado": {
"rectangulo": {"x": 16.5, "y": 21.8, "width": 3.0, "height": 0.3,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 0, "textColor": "black",
"backColor": "white"}
},
"t_serie_emisor": {
"valor": "Serie CSD Emisor:",
"rectangulo": {"x": 14.2, "y": 21.4, "width": 2.2, "height": 0.3,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 2, "textColor": "black",
"backColor": "white"}
},
"nocertificado": {
"rectangulo": {"x": 16.5, "y": 21.4, "width": 3.0, "height": 0.3,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 0, "textColor": "black",
"backColor": "white"}
},
"t_serie_sat": {
"valor": "Serie CSD SAT:",
"rectangulo": {"x": 14.2, "y": 21.1, "width": 2.2, "height": 0.3,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 2, "textColor": "black",
"backColor": "white"}
},
"nocertificadosat": {
"rectangulo": {"x": 16.5, "y": 21.1, "width": 3.0, "height": 0.3,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 0, "textColor": "black",
"backColor": "white"}
},
"lugarexpedicion": {
"rectangulo": {"x": 14.2, "y": 20.7, "width": 5.5, "height": 0.4,
"radius": 0.0, "stroke": false, "fill": false},
"estilo": {"name": "nombre", "fontName": "Helvetica-Bold",
"fontSize": 7, "alignment": 1, "textColor": "black",
"backColor": "white"}
}
}
},
"conceptos": {
},
"comprobante": {
"totalenletras": {
"estilo": {"name": "enletras", "fontName": "Helvetica-Bold",
"fontSize": 7, "alignment": 1, "textColor": "black",
"spaceBefore": 0.1}
},
"formadepago": {
"estilo": {"name": "formadepago", "fontName": "Helvetica",
"fontSize": 7, "alignment": 0, "textColor": "black"}
},
"metododepago": {
"estilo": {"name": "metododepago", "fontName": "Helvetica",
"fontSize": 7, "alignment": 0, "textColor": "black"}
},
"moneda": {
"estilo": {"name": "moneda", "fontName": "Helvetica",
"fontSize": 7, "alignment": 0, "textColor": "black"}
},
"tipocambio": {
"estilo": {"name": "tipocambio", "fontName": "Helvetica",
"fontSize": 7, "alignment": 0, "textColor": "black"}
},
"leyenda": {
"estilo": {"name": "leyenda", "fontName": "Helvetica-Bold",
"fontSize": 6, "alignment": 1, "textColor": "black",
"spaceBefore": 0.2}
}
}
}