diff --git a/README.md b/README.md
index c7b3810..802558d 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/requirements.txt b/requirements.txt
index 759159c..9b10d62 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,3 +9,6 @@ bcrypt
python-dateutil
zeep
chardet
+pyqrcode
+pypng
+reportlab
diff --git a/source/app/controllers/helper.py b/source/app/controllers/helper.py
index 159a6b6..0ccfc90 100644
--- a/source/app/controllers/helper.py
+++ b/source/app/controllers/helper.py
@@ -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 = '{}'
+ html = '{}'
+ 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
diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py
index ac55355..1939e44 100644
--- a/source/app/controllers/main.py
+++ b/source/app/controllers/main.py
@@ -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
diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py
index 49e3cc5..25b7fa4 100644
--- a/source/app/controllers/pac.py
+++ b/source/app/controllers/pac.py
@@ -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:
diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py
index 156de65..2b4fea4 100644
--- a/source/app/controllers/util.py
+++ b/source/app/controllers/util.py
@@ -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', '
')
+
+
+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
diff --git a/source/app/main.py b/source/app/main.py
index b423f09..a35833f 100644
--- a/source/app/main.py
+++ b/source/app/main.py
@@ -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))
diff --git a/source/app/models/db.py b/source/app/models/db.py
index fe07363..f5a460d 100644
--- a/source/app/models/db.py
+++ b/source/app/models/db.py
@@ -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
diff --git a/source/app/models/main.py b/source/app/models/main.py
index 586b8c4..60ecc57 100644
--- a/source/app/models/main.py
+++ b/source/app/models/main.py
@@ -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', '
'),
+ '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
diff --git a/source/app/settings.py b/source/app/settings.py
index 88b87ff..1776135 100644
--- a/source/app/settings.py
+++ b/source/app/settings.py
@@ -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'
diff --git a/source/db/rfc.db.ini b/source/db/rfc.db.ini
deleted file mode 100644
index cd38d7c..0000000
Binary files a/source/db/rfc.db.ini and /dev/null differ
diff --git a/source/db/valores_iniciales.json b/source/db/valores_iniciales.json
index 60fae31..53adaed 100644
--- a/source/db/valores_iniciales.json
+++ b/source/db/valores_iniciales.json
@@ -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}
]
},
{
diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js
index 47c90b8..0790a61 100644
--- a/source/static/js/controller/admin.js
+++ b/source/static/js/controller/admin.js
@@ -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
\
+ ¿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)
+ }
+ }
+ })
+}
+
+
diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js
index eaf553d..a79cdc2 100644
--- a/source/static/js/controller/invoices.js
+++ b/source/static/js/controller/invoices.js
@@ -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?
\
+ 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)
+ }
+ }
+ })
+}
diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js
index 8830ebd..4dd4c24 100644
--- a/source/static/js/controller/main.js
+++ b/source/static/js/controller/main.js
@@ -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)
}
}
diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js
index 9ca919d..53d6fa8 100644
--- a/source/static/js/ui/admin.js
+++ b/source/static/js/ui/admin.js
@@ -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,
]
}
diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js
index 950baf1..9e110c6 100644
--- a/source/static/js/ui/invoices.js
+++ b/source/static/js/ui/invoices.js
@@ -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){
diff --git a/source/static/js/ui/partners.js b/source/static/js/ui/partners.js
index 9c031af..c927068 100644
--- a/source/static/js/ui/partners.js
+++ b/source/static/js/ui/partners.js
@@ -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
})
}
},
diff --git a/source/templates/plantilla_factura.json b/source/templates/plantilla_factura.json
new file mode 100644
index 0000000..6aabf3f
--- /dev/null
+++ b/source/templates/plantilla_factura.json
@@ -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}
+ }
+}
+}