commit
795aa2e50c
|
@ -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.
|
||||
|
|
|
@ -9,3 +9,6 @@ bcrypt
|
|||
python-dateutil
|
||||
zeep
|
||||
chardet
|
||||
pyqrcode
|
||||
pypng
|
||||
reportlab
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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.
|
@ -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}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue