diff --git a/.gitignore b/.gitignore
index ba31efa..dd92c9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@ source/media
docs/bk/
source/docs/
site/
+vedev/
cache/
credenciales.conf
*.sqlite
@@ -24,4 +25,5 @@ credenciales.conf
*.service
*.orig
rfc.db
+Dockerfile
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a71e4f9..aec701c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+v 1.26.0 [15-ene-2019]
+----------------------
+ - Mejora: Generar PDF desde plantilla JSON
+
+
v 1.25.0 [07-nov-2018]
----------------------
- Fix: Al importar días de pago en nómina
diff --git a/VERSION b/VERSION
index ad21919..5ff8c4f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.25.0
+1.26.0
diff --git a/source/app/controllers/helper.py b/source/app/controllers/helper.py
index efc7274..5b1aa8f 100644
--- a/source/app/controllers/helper.py
+++ b/source/app/controllers/helper.py
@@ -29,6 +29,9 @@ from reportlab.platypus import Paragraph, Table, TableStyle, Spacer
from reportlab.pdfgen import canvas
+CANCEL = False
+
+
#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
class CaseInsensitiveDict(collections.MutableMapping):
"""A case-insensitive ``dict``-like object.
@@ -340,6 +343,7 @@ class SendMail(object):
class NumberedCanvas(canvas.Canvas):
X = 20.59 * cm
+ XC = 21.6 * cm / 2 + 1.5 * cm
Y = 1.5 * cm
def __init__(self, *args, **kwargs):
@@ -357,19 +361,33 @@ class NumberedCanvas(canvas.Canvas):
for state in self._saved_page_states:
self.__dict__.update(state)
self.draw_page_number(page_count)
+ self.draw_cancel()
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.setFillColor(colors.darkred)
+ text = f'Página {self._pageNumber} de {page_count}'
self.drawRightString(self.X, self.Y, text)
- text = 'Factura elaborada con software libre: www.empresalibre.net'
+
+ text = 'Este documento es una representación impresa de un CFDI'
+ self.drawCentredString(self.XC, self.Y, text)
+
+ text = 'Factura elaborada con software libre'
self.drawString(1.5 * cm, 1.5 * cm, text)
return
+ def draw_cancel(self):
+ global CANCEL
+ if CANCEL:
+ self.rotate(45)
+ self.setFont('Helvetica', 100)
+ self.setFillColor(colors.red)
+ text = 'Cancelada'
+ self.drawCentredString(20 * cm, 3 * cm, text)
+ return
class TemplateInvoice(BaseDocTemplate):
@@ -418,6 +436,9 @@ class TemplateInvoice(BaseDocTemplate):
def _emisor(self, styles, data):
logo_path = data.pop('logo', '')
logo_style = styles.pop('logo', {})
+ logo_path2 = data.pop('logo2', '')
+ logo_style2 = styles.pop('logo2', {})
+ sucursales = styles.pop('sucursales', {})
for k, v in styles.items():
self._set_text(styles[k], data.get(k, ''))
@@ -428,6 +449,28 @@ class TemplateInvoice(BaseDocTemplate):
for k in keys:
rect[k] = rect[k] * cm
self.canv.drawImage(logo_path, **rect)
+
+ if logo_path2 and logo_style2:
+ rect = logo_style2['rectangulo']
+ keys = ('x', 'y', 'width', 'height')
+ for k in keys:
+ rect[k] = rect[k] * cm
+ self.canv.drawImage(logo_path2, **rect)
+
+ if sucursales:
+ for k, sucursal in sucursales.items():
+ values = sucursal.pop('textos')
+ x = sucursal['rectangulo']['x']
+ y = sucursal['rectangulo']['y']
+ w = sucursal['rectangulo']['width']
+ h = sucursal['rectangulo']['height']
+ for v in values:
+ self._set_text(sucursal, v)
+ y -= h
+ sucursal['rectangulo']['x'] = x
+ sucursal['rectangulo']['y'] = y
+ sucursal['rectangulo']['width'] = w
+ sucursal['rectangulo']['height'] = h
return
def _receptor(self, styles, data):
@@ -449,6 +492,7 @@ class TemplateInvoice(BaseDocTemplate):
def _comprobante1(self, styles, data):
title = styles.pop('titulo', {})
+ self.canv.setTitle(f"Factura {data.get('seriefolio', '')}")
for k, v in styles.items():
self._set_text(styles[k], data.get(k, ''))
@@ -480,6 +524,12 @@ class TemplateInvoice(BaseDocTemplate):
fields = ('valorunitario', 'importe')
if field in fields:
return self._currency(value)
+
+ if field == 'descripcion':
+ html = '{}'
+ style_bt = getSampleStyleSheet()['BodyText']
+ return Paragraph(html.format(value), style_bt)
+
return value
def _conceptos(self, conceptos):
@@ -515,8 +565,8 @@ class TemplateInvoice(BaseDocTemplate):
('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),
+ ('BACKGROUND', (1, 0), (-1, -1), colors.lightgrey),
+ ('TEXTCOLOR', (1, 0), (-1, -1), colors.black),
('FACE', (1, 0), (-1, -1), 'Helvetica-Bold'),
]
table = Table(rows, colWidths=widths, spaceBefore=0.25*cm)
@@ -524,7 +574,7 @@ class TemplateInvoice(BaseDocTemplate):
return table
def _comprobante2(self, styles, data):
- leyenda = styles.pop('leyenda', {})
+ leyendas = styles.pop('leyendas', {})
ls = []
for k, v in styles.items():
@@ -542,7 +592,7 @@ class TemplateInvoice(BaseDocTemplate):
style_bt = getSampleStyleSheet()['BodyText']
style_bt.leading = 8
html_t = '{}'
- html = '{}'
+ html = '{}'
msg = 'Cadena original del complemento de certificación digital del SAT'
rows = [
(cbb, Paragraph(html_t.format('Sello Digital del CFDI'), style_bt)),
@@ -558,13 +608,13 @@ class TemplateInvoice(BaseDocTemplate):
('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),
+ ('BACKGROUND', (1, 1), (1, 1), colors.lightgrey),
('TEXTCOLOR', (1, 1), (1, 1), colors.darkred),
('FACE', (1, 2), (1, 2), 'Helvetica-Bold'),
- ('BACKGROUND', (1, 3), (1, 3), colors.linen),
+ ('BACKGROUND', (1, 3), (1, 3), colors.lightgrey),
('TEXTCOLOR', (1, 3), (1, 3), colors.darkred),
('FACE', (1, 4), (1, 4), 'Helvetica-Bold'),
- ('BACKGROUND', (1, 5), (1, 5), colors.linen),
+ ('BACKGROUND', (1, 5), (1, 5), colors.lightgrey),
('TEXTCOLOR', (1, 5), (1, 5), colors.darkred),
('ALIGN', (0, 0), (0, 0), 'CENTER'),
('VALIGN', (0, 0), (0, 0), 'MIDDLE'),
@@ -573,14 +623,14 @@ class TemplateInvoice(BaseDocTemplate):
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)
+ if leyendas:
+ if 'spaceBefore' in leyendas['estilo']:
+ leyendas['estilo']['spaceBefore'] = \
+ leyendas['estilo']['spaceBefore'] * cm
+ for t in leyendas['textos']:
+ ps = ParagraphStyle(**leyendas['estilo'])
+ p = Paragraph(t, ps)
+ ls.append(p)
return ls
@@ -596,8 +646,10 @@ class TemplateInvoice(BaseDocTemplate):
return self._data
@data.setter
def data(self, values):
- #~ print (values)
+ global CANCEL
+ # ~ print (values)
self._data = values
+ CANCEL = self._data['cancelada']
rows = self._conceptos(self._data['conceptos'])
widths = [2 * cm, 9 * cm, 1.5 * cm, 2 * cm, 2 * cm, 3 * cm]
@@ -607,13 +659,13 @@ class TemplateInvoice(BaseDocTemplate):
('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),
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
('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),
+ ('LINEBELOW', (0, 1), (-1, -1), 0.05 * cm, colors.grey),
('LINEBEFORE', (0, 1), (-1, -1), 0.05 * cm, colors.white),
]
table_conceptos = Table(rows, colWidths=widths, repeatRows=1)
@@ -623,7 +675,7 @@ class TemplateInvoice(BaseDocTemplate):
comprobante = self._comprobante2(
self._custom_styles['comprobante'], self.data['comprobante'])
- self._rows = [Spacer(0, 6*cm), table_conceptos, totales] + comprobante
+ self._rows = [Spacer(0, 5.7 * cm), table_conceptos, totales] + comprobante
def render(self):
frame = Frame(self.leftMargin, self.bottomMargin,
diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py
index 353f7ad..5952af9 100644
--- a/source/app/controllers/main.py
+++ b/source/app/controllers/main.py
@@ -510,7 +510,7 @@ class AppDocumentos(object):
session = req.env['beaker.session']
req.context['result'], file_name, content_type = \
self._db.get_doc(type_doc, id_doc, session['rfc'])
- if not type_doc in ('pdf', 'pre', 'tpdf', 'pdfpago'):
+ if not type_doc in ('pdf', 'pre', 'tpdf', 'pdfpago', 'html'):
resp.append_header('Content-Disposition',
'attachment; filename={}'.format(file_name))
resp.content_type = content_type
diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py
index 5338e94..673b7ea 100644
--- a/source/app/controllers/util.py
+++ b/source/app/controllers/util.py
@@ -16,9 +16,11 @@
# ~ You should have received a copy of the GNU General Public License
# ~ along with this program. If not, see
' + 'X'*100 + '
' + 'X'*100
+ # ~ data['timbre_sellosat'] = 'X'*100 + '
' + 'X'*100 + '
' + 'X'*100
+
+ return template.render(**data)
+
+
+def html_to_pdf(data):
+ path_pdf = '/home/mau/test.pdf'
+ css = '/home/mau/projects/empresa-libre/source/static/css/invoice.css'
+
+ # ~ font_config = FontConfiguration()
+ # ~ html = HTML(string=data)
+ # ~ css = CSS(filename=path_css)
+ # ~ html.write_pdf(path_pdf, stylesheets=[css], font_config=font_config)
+ options = {
+ 'page-size': 'Letter',
+ 'margin-top': '0.50in',
+ 'margin-right': '0.50in',
+ 'margin-bottom': '0.50in',
+ 'margin-left': '0.50in',
+ 'encoding': "UTF-8",
+ }
+
+ pdfkit.from_string(data.decode(), path_pdf, options=options, css=css)
+
+ return
+
+
def import_employees(rfc):
name = '{}_employees.ods'.format(rfc.lower())
path = _join(PATH_MEDIA, 'tmp', name)
@@ -1621,15 +1704,20 @@ def get_dict(data):
return CaseInsensitiveDict(data)
-def to_letters(value, moneda):
- return NumLet(value, moneda).letras
+def to_letters(value, currency):
+ return NumLet(value, currency).letras
-def get_qr(data):
- path = get_path_temp('.qr')
+def get_qr(data, p=True):
qr = pyqrcode.create(data, mode='binary')
- qr.png(path, scale=7)
- return path
+ if p:
+ path = get_path_temp('.qr')
+ qr.png(path, scale=7)
+ return path
+
+ buffer = io.BytesIO()
+ qr.png(buffer, scale=8)
+ return base64.b64encode(buffer.getvalue()).decode()
def _get_relacionados(doc, version):
@@ -1881,6 +1969,7 @@ def _timbre(doc, version, values):
'sello': '&fe={}'.format(data['sellocfd'][-8:]),
}
qr_data = '{url}{uuid}{emisor}{receptor}{total}{sello}'.format(**qr_data)
+
data['path_cbb'] = get_qr(qr_data)
data['cadenaoriginal'] = CADENA.format(**data)
return data
@@ -2083,39 +2172,58 @@ def get_date(value, next_day=False):
return d
+class UpFile(object):
+
+
+ def __init__(self):
+ self._init_values()
+
+ def _init_values(self):
+
+ return
+
+
def upload_file(rfc, opt, file_obj):
- if opt == 'emisorlogo':
- tmp = file_obj.filename.split('.')
- name = '{}.{}'.format(rfc.lower(), tmp[-1].lower())
- path = _join(PATH_MEDIA, 'logos', name)
- elif opt == 'txt_plantilla_factura_32':
- tmp = file_obj.filename.split('.')
- ext = tmp[-1].lower()
- if ext != 'ods':
- msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
+ rfc = rfc.lower()
+ tmp = file_obj.filename.split('.')
+ ext = tmp[-1].lower()
+
+ EXTENSIONS = {
+ 'txt_plantilla_factura_32': EXT['ODS'],
+ 'txt_plantilla_factura_33': EXT['ODS'],
+ 'txt_plantilla_factura_html': EXT['HTML'],
+ 'txt_plantilla_factura_css': EXT['CSS'],
+ 'txt_plantilla_factura_json': EXT['JSON'],
+ }
+
+ if opt in EXTENSIONS:
+ if ext != EXTENSIONS[opt]:
+ msg = (
+ f"Extensión de archivo incorrecta, "
+ f"selecciona un archivo {EXTENSIONS[opt].upper()}"
+ )
return {'status': 'server', 'name': msg, 'ok': False}
- name = '{}_3.2.ods'.format(rfc.lower())
- path = _join(PATH_MEDIA, 'templates', name)
- elif opt == 'txt_plantilla_factura_33':
- tmp = file_obj.filename.split('.')
- ext = tmp[-1].lower()
- if ext != 'ods':
- msg = 'Extensión de archivo incorrecta, selecciona un archivo ODS'
- return {'status': 'server', 'name': msg, 'ok': False}
+ NAMES = {
+ 'txt_plantilla_factura_32': f"{rfc}_3.2.ods",
+ 'txt_plantilla_factura_33': f"{rfc}_3.3.ods",
+ 'txt_plantilla_factura_html': f"{rfc}_3.3.html",
+ 'txt_plantilla_factura_css': f"{rfc}.css",
+ 'txt_plantilla_factura_json': f"{rfc}_3.3.json",
+ }
+ name = NAMES[opt]
+ paths = {
+ 'txt_plantilla_factura_32': _join(PATHS['USER'], name),
+ 'txt_plantilla_factura_33': _join(PATHS['USER'], name),
+ 'txt_plantilla_factura_html': _join(PATHS['USER'], name),
+ 'txt_plantilla_factura_css': _join(PATHS['CSS'], name),
+ 'txt_plantilla_factura_json': _join(PATHS['USER'], name),
+ }
+ if save_file(paths[opt], file_obj.file.read()):
+ return {'status': 'server', 'name': file_obj.filename, 'ok': True}
+ return {'status': 'error', 'ok': False}
- name = '{}_3.3.ods'.format(rfc.lower())
- path = _join(PATH_MEDIA, 'templates', name)
- elif opt == 'txt_plantilla_factura_33j':
- tmp = file_obj.filename.split('.')
- ext = tmp[-1].lower()
- if ext != 'json':
- msg = 'Extensión de archivo incorrecta, selecciona un archivo JSON'
- return {'status': 'server', 'name': msg, 'ok': False}
-
- name = '{}_3.3.json'.format(rfc.lower())
- path = _join(PATH_MEDIA, 'templates', name)
- elif opt == 'txt_plantilla_ticket':
+ if opt == 'txt_plantilla_ticket':
tmp = file_obj.filename.split('.')
ext = tmp[-1].lower()
if ext != 'ods':
@@ -3648,3 +3756,8 @@ def validate_rfc(value):
return ''
except:
return msg
+
+
+def parse_xml2(xml_str):
+ return etree.fromstring(xml_str.encode('utf-8'))
+
diff --git a/source/app/main.py b/source/app/main.py
index ec12f0c..e7afd80 100644
--- a/source/app/main.py
+++ b/source/app/main.py
@@ -8,7 +8,6 @@ from middleware import (
AuthMiddleware,
JSONTranslator,
ConnectionMiddleware,
- static,
handle_404
)
from models.db import StorageEngine
@@ -64,9 +63,6 @@ api.add_route('/satformapago', AppSATFormaPago(db))
api.add_route('/socioscb', AppSociosCuentasBanco(db))
-# ~ Activa si usas waitress y NO estas usando servidor web
-# ~ api.add_sink(static, '/static')
-
session_options = {
'session.type': 'file',
'session.cookie_expires': True,
diff --git a/source/app/main_debug.ini b/source/app/main_debug.ini
index e6090c5..a9e0a87 100644
--- a/source/app/main_debug.ini
+++ b/source/app/main_debug.ini
@@ -8,4 +8,4 @@ threads = 4
py-autoreload = 1
thunder-lock = true
static-map = /static=../static
-http-timeout = 300
\ No newline at end of file
+http-timeout = 300
diff --git a/source/app/middleware.py b/source/app/middleware.py
index bbe031b..9c873c1 100644
--- a/source/app/middleware.py
+++ b/source/app/middleware.py
@@ -3,7 +3,7 @@
import falcon
from controllers import util
from models import main
-from settings import MV, PATH_STATIC
+from settings import MV
def handle_404(req, resp):
@@ -20,14 +20,14 @@ def get_template(req, resp, resource):
resp.body = util.get_template(resource.template, data)
-def static(req, res):
- path = PATH_STATIC + req.path
- if util.is_file(path):
- res.content_type = util.get_mimetype(path)
- res.stream, res.stream_len = util.get_stream(path)
- res.status = falcon.HTTP_200
- else:
- res.status = falcon.HTTP_404
+# ~ def static(req, res):
+ # ~ path = PATH_STATIC + req.path
+ # ~ if util.is_file(path):
+ # ~ res.content_type = util.get_mimetype(path)
+ # ~ res.stream, res.stream_len = util.get_stream(path)
+ # ~ res.status = falcon.HTTP_200
+ # ~ else:
+ # ~ res.status = falcon.HTTP_404
class AuthMiddleware(object):
diff --git a/source/app/models/main.py b/source/app/models/main.py
index 71eb26f..db985fe 100644
--- a/source/app/models/main.py
+++ b/source/app/models/main.py
@@ -37,6 +37,12 @@ from settings import log, DEBUG, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI,
DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \
DEFAULT_CFDIPAY, CURRENCY_MN
+from settings import (
+ EXT,
+ MXN,
+ PATHS,
+ VALUES_PDF,
+)
FORMAT = '{0:.2f}'
FORMAT3 = '{0:.3f}'
@@ -97,9 +103,11 @@ def upload_file(rfc, opt, file_obj):
else:
return Facturas.import_cfdi(xml, sxml)
+ if opt == 'emisorlogo':
+ return Emisor.save_logo(file_obj)
+
result = util.upload_file(rfc, opt, file_obj)
if result['ok']:
-
names = ('bdfl', 'employees', 'nomina', 'products', 'invoiceods')
if not opt in names:
Configuracion.add({opt: file_obj.filename})
@@ -204,12 +212,15 @@ def get_doc(type_doc, id, rfc):
'ods': 'application/octet-stream',
'zip': 'application/octet-stream',
'nomlog': 'application/txt',
+ 'html': 'text/html',
}
content_type = types.get(type_doc, 'application/pdf')
if type_doc == 'xml':
data, file_name = Facturas.get_xml(id)
elif type_doc == 'pdf':
data, file_name = Facturas.get_pdf(id, rfc)
+ elif type_doc == 'html':
+ data, file_name = Facturas.get_html(id)
elif type_doc == 'ods':
data, file_name = Facturas.get_ods(id, rfc)
elif type_doc == 'zip':
@@ -303,9 +314,38 @@ class Configuracion(BaseModel):
clave = TextField(unique=True)
valor = TextField(default='')
+ class Meta:
+ order_by = ('clave',)
+ indexes = (
+ (('clave', 'valor'), True),
+ )
+
def __str__(self):
return '{} = {}'.format(self.clave, self.valor)
+ def _clean_email(self, values={}):
+ fields = (
+ 'correo_asunto',
+ 'correo_confirmacion',
+ 'correo_contra',
+ 'correo_directo',
+ 'correo_mensaje',
+ 'correo_puerto',
+ 'correo_servidor',
+ 'correo_ssl',
+ 'correo_usuario',
+ )
+
+ q = (Configuracion
+ .update(**{'valor': ''})
+ .where(Configuracion.clave.in_(fields))
+ )
+ result = q.execute()
+ msg = 'Configuración guardada correctamente'
+ if not result:
+ msg = 'No se pudo guardar la configuración'
+ return {'ok': result, 'msg': msg}
+
@classmethod
def get_bool(cls, key):
data = (Configuracion
@@ -460,11 +500,14 @@ class Configuracion(BaseModel):
fields = (
'txt_plantilla_factura_32',
'txt_plantilla_factura_33',
- 'txt_plantilla_factura_33j',
+ 'txt_plantilla_factura_html',
+ 'txt_plantilla_factura_css',
+ 'txt_plantilla_factura_json',
'txt_plantilla_nomina1233',
'txt_plantilla_pagos10',
'txt_plantilla_ticket',
'txt_plantilla_donataria',
+ 'make_pdf_from',
)
data = (Configuracion
.select()
@@ -488,6 +531,10 @@ class Configuracion(BaseModel):
@classmethod
def add(cls, values):
+ opt = values.pop('opt', '')
+ if opt:
+ return getattr(cls, f'_{opt}')(cls, values)
+
# ~ print (values)
try:
for k, v in values.items():
@@ -505,12 +552,6 @@ class Configuracion(BaseModel):
q = Configuracion.delete().where(Configuracion.clave==key)
return bool(q.execute())
- class Meta:
- order_by = ('clave',)
- indexes = (
- (('clave', 'valor'), True),
- )
-
class Tags(BaseModel):
tag = TextField(index=True, unique=True)
@@ -762,6 +803,10 @@ class SATRegimenes(BaseModel):
def __str__(self):
return '{} ({})'.format(self.name, self.key)
+ @classmethod
+ def get_by_key(cls, key):
+ return SATRegimenes.get(SATRegimenes.key==key)
+
@classmethod
def get_(cls, ids):
if isinstance(ids, int):
@@ -814,12 +859,30 @@ class Emisor(BaseModel):
registro_patronal = TextField(default='')
regimenes = ManyToManyField(SATRegimenes, related_name='emisores')
+ class Meta:
+ order_by = ('nombre',)
+
def __str__(self):
t = '{} ({})'
return t.format(self.nombre, self.rfc)
- class Meta:
- order_by = ('nombre',)
+ @classmethod
+ def save_logo(cls, file_obj):
+ result = {'status': 'error', 'ok': False}
+ name = file_obj.filename.split('.')
+ if name[-1].lower() != EXT['PNG']:
+ return result
+
+ emisor = Emisor.select()[0]
+ rfc = emisor.rfc.lower()
+ name = f'{rfc}.png'
+ path = util._join(PATHS['IMG'], name)
+ if util.save_file(path, file_obj.file.read()):
+ emisor.logo = file_obj.filename
+ emisor.save()
+ result = {'status': 'server', 'name': file_obj.filename, 'ok': True}
+
+ return result
@classmethod
def get_(cls, rfc):
@@ -847,6 +910,7 @@ class Emisor(BaseModel):
'emisor_telefono': obj.telefono,
'emisor_correo': obj.correo,
'emisor_web': obj.web,
+ 'emisor_logo': obj.logo,
'es_escuela': obj.es_escuela,
'es_ong': obj.es_ong,
'ong_autorizacion': obj.autorizacion,
@@ -1366,6 +1430,10 @@ class SATMonedas(BaseModel):
def __str__(self):
return 'Moneda: ({}) {}'.format(self.key, self.name)
+ @classmethod
+ def get_by_key(cls, key):
+ return SATMonedas.get(SATMonedas.key==key)
+
@classmethod
def get_multi_currency(cls):
count_currencies = len(cls.get_activos())
@@ -1946,6 +2014,10 @@ class SATUsoCfdi(BaseModel):
def __str__(self):
return 'Uso del CFDI: {} ({})'.format(self.name, self.key)
+ @classmethod
+ def get_by_key(cls, key):
+ return SATUsoCfdi.get(SATUsoCfdi.key==key)
+
@classmethod
def actualizar(self, values):
id = int(values['id'])
@@ -3647,6 +3719,7 @@ class Facturas(BaseModel):
values['receptor'] = {}
for k, v in receptor.items():
values['receptor'][k] = v
+
return values
@classmethod
@@ -3661,9 +3734,10 @@ class Facturas(BaseModel):
if obj.uuid is None:
return b'', name
+ pdf_from = Configuracion.get_('make_pdf_from') or '1'
values = cls._get_not_in_xml(cls, obj, emisor)
data = util.get_data_from_xml(obj, values)
- doc = util.to_pdf(data, emisor.rfc)
+ doc = util.to_pdf(data, emisor.rfc, pdf_from=pdf_from)
if sync:
target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
@@ -3671,6 +3745,137 @@ class Facturas(BaseModel):
return doc, name
+ def _get_concepto(self, node, currency):
+ d = util.get_dict(node.attrib)
+ concepto = {
+ 'clave': f"{d['noidentificacion']}
({d['claveprodserv']})",
+ 'descripcion': self._get_description(self, node, d),
+ 'unidad': f"{d['unidad']}
({d['claveunidad']})",
+ 'cantidad': d['cantidad'],
+ 'valorunitario': util.format_currency(d['valorunitario'], currency),
+ 'importe': util.format_currency(d['importe'], currency),
+ }
+ return concepto
+
+ def _get_description(self, node, data):
+ return data['descripcion']
+
+ def _get_tax(self, node, data, currency):
+ type_tax = 'Traslado'
+ if 'Retenciones' in node.tag:
+ type_tax = 'Retención'
+
+ for n in node:
+ d = util.get_dict(n.attrib)
+ name = VALUES_PDF['TAX'].get(d['impuesto'])
+ tasa = f"{float(d['tasaocuota']):.2f}"
+ title = f"{type_tax} {name} {tasa}"
+ importe = util.format_currency(d['importe'], currency)
+ data['totales'].append((title, importe))
+ return
+
+ def _get_others_values(self, invoice, emisor):
+ data = {
+ 'rfc': emisor.rfc.lower(),
+ 'version': invoice.version,
+ 'cancelada': VALUES_PDF['CANCEL'].get(invoice.cancelada),
+ 'cfdi_notas': invoice.notas,
+ }
+
+ xml = util.parse_xml2(invoice.xml)
+ d = util.get_dict(xml.attrib)
+ d.pop('Certificado', '')
+ currency = d.get('Moneda', 'MXN')
+
+ data.update({f'cfdi_{k.lower()}': v for k, v in d.items()})
+
+ data['cfdi_tipodecomprobante'] = \
+ VALUES_PDF['TYPE'][data['cfdi_tipodecomprobante']]
+
+ if data.get('cfdi_formapago', ''):
+ data['cfdi_formapago'] = str(
+ SATFormaPago.get_by_key(data['cfdi_formapago']))
+ if data.get('cfdi_metodopago', ''):
+ data['cfdi_metodopago'] = 'Método de Pago: ({}) {}'.format(
+ data['cfdi_metodopago'],
+ VALUES_PDF['METHOD'][data['cfdi_metodopago']])
+
+ data['cfdi_moneda'] = str(SATMonedas.get_by_key(currency))
+ data['cfdi_tipocambio'] = util.format_currency(
+ data['cfdi_tipocambio'], currency, 4)
+ data['cfdi_totalenletras'] = util.to_letters(
+ float(data['cfdi_total']), currency)
+
+ data['totales'] = [(
+ 'Subtotal', util.format_currency(data['cfdi_subtotal'], currency))]
+
+ for node in xml:
+ if 'Emisor' in node.tag:
+ d = {f'emisor_{k.lower()}': v for k, v in node.attrib.items()}
+ data.update(d)
+ elif 'Receptor' in node.tag:
+ d = {f'receptor_{k.lower()}': v for k, v in node.attrib.items()}
+ data.update(d)
+ elif 'Conceptos' in node.tag:
+ data['conceptos'] = []
+ for subnode in node:
+ concepto = self._get_concepto(self, subnode, currency)
+ data['conceptos'].append(concepto)
+ elif 'Impuestos' in node.tag:
+ for subnode in node:
+ self._get_tax(self, subnode, data, currency)
+ elif 'Complemento' in node.tag:
+ for sn in node:
+ if 'TimbreFiscalDigital' in sn.tag:
+ d = {f'timbre_{k.lower()}': v for k, v in sn.attrib.items()}
+ data.update(d)
+
+ data['emisor_logo'] = data['emisor_rfc'].lower()
+ data['emisor_regimenfiscal'] = str(
+ SATRegimenes.get_by_key(data['emisor_regimenfiscal']))
+ data['receptor_usocfdi'] = str(
+ SATUsoCfdi.get_by_key(data['receptor_usocfdi']))
+
+ data['totales'].append((
+ 'Total', util.format_currency(data['cfdi_total'], currency)))
+
+ data['timbre_cadenaoriginal'] = (
+ f"||{data['timbre_version']}|"
+ f"{data['timbre_uuid']}|"
+ f"{data['timbre_fechatimbrado']}|"
+ f"{data['timbre_sellocfd']}|"
+ f"{data['timbre_nocertificadosat']}||"
+ )
+
+ qr_data = (
+ f"https://verificacfdi.facturaelectronica.sat.gob.mx/"
+ f"default.aspx?&id={data['timbre_uuid']}&re={data['emisor_rfc']}"
+ f"&rr={data['receptor_rfc']}&tt={data['cfdi_total']}"
+ f"&fe={data['cfdi_sello'][-8:]}"
+ )
+ cbb = util.get_qr(qr_data, False)
+ data['cbb'] = f'data:image/png;base64,{cbb}'
+
+ return data
+
+ @classmethod
+ def get_html(cls, id):
+ try:
+ emisor = Emisor.select()[0]
+ except IndexError:
+ return '', 'sin_datos_de_emisor.html'
+
+ obj = Facturas.get(Facturas.id==id)
+ name = '{}{}_{}.html'.format(obj.serie, obj.folio, obj.cliente.rfc)
+ if obj.uuid is None:
+ return '', name
+
+ data = cls._get_others_values(cls, obj, emisor)
+ doc = util.to_html(data)
+ # ~ util.html_to_pdf(doc)
+
+ return doc, name
+
@classmethod
def get_ods(cls, id, rfc):
try:
diff --git a/source/app/settings.py b/source/app/settings.py
index df4130f..5db1316 100644
--- a/source/app/settings.py
+++ b/source/app/settings.py
@@ -47,13 +47,21 @@ except ImportError:
DEBUG = DEBUG
-VERSION = '1.25.0'
+VERSION = '1.26.0'
EMAIL_SUPPORT = ('soporte@empresalibre.net',)
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
-PATH_STATIC = os.path.abspath(os.path.join(BASE_DIR, '..'))
+path_static = os.path.abspath(os.path.join(BASE_DIR, '..', 'static'))
+path_docs = os.path.abspath(os.path.join(BASE_DIR, '..', 'docs'))
+
+path_css = os.path.join(path_static, 'css')
+path_img = os.path.join(path_static, 'img')
+path_user_template = os.path.join(path_docs, 'templates')
+
+# ~ 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'))
@@ -74,7 +82,10 @@ TEMPLATE_CANCEL = os.path.abspath(os.path.join(PATH_TEMPLATES, CT))
PATH_XSLT = os.path.abspath(os.path.join(BASE_DIR, '..', 'xslt'))
PATH_BIN = os.path.abspath(os.path.join(BASE_DIR, '..', 'bin'))
-template_lookup = TemplateLookup(directories=[PATH_TEMPLATES],
+PATH_TEMPLATES_USER = os.path.abspath(os.path.join(
+ BASE_DIR, '..', 'docs', 'templates'))
+directories=[PATH_TEMPLATES, PATH_TEMPLATES_USER]
+template_lookup = TemplateLookup(directories=directories,
input_encoding='utf-8',
output_encoding='utf-8')
@@ -182,3 +193,29 @@ DEFAULT_SAT_NOMINA = {
API = 'https://api.empresalibre.net{}'
CURRENCY_MN = 'MXN'
+
+# ~ v2
+EXT = {
+ 'CSS': 'css',
+ 'HTML': 'html',
+ 'ODS': 'ods',
+ 'PNG': 'png',
+ 'JSON': 'json',
+}
+MXN = 'MXN'
+PATHS = {
+ 'STATIC': path_static,
+ 'CSS': path_css,
+ 'IMG': path_img,
+ 'DOCS': path_docs,
+ 'USER': path_user_template,
+}
+VALUES_PDF = {
+ 'CANCEL': {True: 'inline', False: 'none'},
+ 'TYPE': {'I': 'Ingreso', 'E': 'Egreso', 'T': 'Traslado'},
+ 'TAX': {'001': 'ISR', '002': 'IVA', '003': 'IEPS'},
+ 'METHOD': {
+ 'PUE': 'Pago en una sola exhibición',
+ 'PPD': 'Pago en parcialidades o diferido',
+ },
+}
diff --git a/source/static/css/invoice.css b/source/static/css/invoice.css
new file mode 100644
index 0000000..3eaa732
--- /dev/null
+++ b/source/static/css/invoice.css
@@ -0,0 +1,519 @@
+@page{
+ size: Letter;
+ margin: 0.5cm;
+ background-color: white !important;
+}
+
+
+@media print {
+ thead {display: table-header-group;}
+ body {
+ background-color: #fff;
+ }
+}
+
+
+.emisor-cintilla{
+ background-color: #975759;
+ color: #fff;
+}
+
+.fiscales-emisor{
+ color: #7d1916;
+}
+
+.fiscales-emisor .telefono, .fiscales-emisor .correo, .fiscales-emisor .web{
+ color: #333;
+}
+
+header .titulo-vertical{
+ background-color: #dbc5c6;
+ color: #000;
+}
+
+.receptor .nombre{
+ color: #000;
+}
+
+.receptor .rfc{
+ color: #000;
+}
+
+.receptor .direccion{
+ color: #000;
+}
+
+.receptor .estado{
+ color: #000;
+}
+
+.cfdi .folio span{
+ color: #ed483d;
+}
+
+.cfdi .tipo span{
+ color: #ed483d;
+}
+
+.cfdi .folio-fiscal span{
+ color: #ed483d;
+}
+
+.conceptos th{
+ background-color: #975759;
+}
+
+.conceptos td.clave, .conceptos td.unidad, .conceptos td.valor-unitario{
+ background-color: #dbc5c6;
+}
+
+.conceptos td.descripcion, .conceptos td.cantidad, .conceptos td.importe{
+ background-color: #f0e7e7;
+}
+
+table.subtotal th{
+ background-color: #dbc5c6;
+}
+
+table.subtotal td{
+ background-color: #f0e7e7;
+}
+
+.sello .cadenas-sello .cadena div {
+ background-color: #dbc5c6;
+ color: #7d1916;
+}
+
+.rfc-pac {
+ background-color: #dbc5c6;
+ color: #7d1916;
+}
+
+.cancelada{
+ color: #ed3833;
+}
+
+@font-face {
+ font-family: 'Avenir Next';
+ src: url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.eot');
+ src: url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.eot?#iefix') format('embedded-opentype'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.woff2') format('woff2'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.woff') format('woff'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.ttf') format('truetype'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-Regular.svg#AvenirNextLTPro-Regular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Avenir Next';
+ src: url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.eot');
+ src: url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.eot?#iefix') format('embedded-opentype'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.woff2') format('woff2'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.woff') format('woff'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.ttf') format('truetype'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-Bold.svg#AvenirNextLTPro-Bold') format('svg');
+ font-weight: bold;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Avenir Next';
+ src: url('/static/fonts/avenir_next/AvenirNextLTPro-It.eot');
+ src: url('/static/fonts/avenir_next/AvenirNextLTPro-It.eot?#iefix') format('embedded-opentype'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-It.woff2') format('woff2'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-It.woff') format('woff'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-It.ttf') format('truetype'),
+ url('/static/fonts/avenir_next/AvenirNextLTPro-It.svg#AvenirNextLTPro-It') format('svg');
+ font-weight: normal;
+ font-style: italic;
+}
+
+
+body {
+/*
+ font-family: 'Avenir Next';
+*/
+}
+
+#plantilla {
+ border: 1px solid #fff;
+ border-color: rgb(204,204,204);
+ background: #fff;
+ display: block;
+ margin: 0 auto;
+ width: 19.5cm;
+ height: 25.9cm;
+ position: relative;
+}
+
+.cancelada{
+ -ms-transform: rotate(320deg);
+ -webkit-transform: rotate(320deg);
+ transform: rotate(320deg);
+ color: #ed3833;
+ font-size: 100px;
+ font-weight: bold;
+ left: 18%;
+ position: absolute;
+ text-align: center;
+ top: 30%;
+ z-index: 1000;
+}
+
+.cancelada div{
+ font-size: 20px;
+}
+
+.emisor-cintilla{
+ background-color: #975759;
+ color: #fff;
+ font-size: 7pt;
+ padding: 5px;
+ text-align: center;
+}
+
+.datos-emisor .logo{
+ margin-left: 20px;
+ max-width: 180px;
+}
+
+.datos-emisor .fiscales-emisor{
+ float: right;
+}
+
+.fiscales-emisor{
+ color: #7d1916;
+ font-weight: bold;
+ margin-top: 20px;
+ text-align: right;
+}
+
+.fiscales-emisor .nombre{
+ font-size: 18px;
+ padding-right: 30px;
+}
+
+.fiscales-emisor .rfc{
+ font-size: 16px;
+ padding-right: 30px;
+}
+
+.fiscales-emisor .regimen{
+ font-size: 12px;
+ padding-right: 30px;
+}
+
+.fiscales-emisor .telefono, .fiscales-emisor .correo, .fiscales-emisor .web{
+ color: #333;
+ font-size: 14px;
+ line-height: 15px;
+ padding-right: 10px;
+}
+
+.fiscales-emisor img{
+ margin-left: 10px;
+ width: 10px;
+}
+
+.clear{
+ clear: both;
+}
+
+header .titulo-vertical{
+ background-color: #dbc5c6;
+ -ms-transform: rotate(270deg);
+ -webkit-transform: rotate(270deg);
+ transform: rotate(270deg);
+ font-size: 15px;
+ font-weight: bold;
+ left: -31px;
+ line-height: 35px;
+ position: absolute;
+ text-align: center;
+ top: 35px;
+ width: 105px;
+}
+
+header .receptor{
+ box-sizing: border-box;
+ float: left;
+ font-size: 10px;
+ height: 106px;
+ margin-top: 10px;
+ position: relative;
+ padding-bottom: 5px;
+ padding-left: 45px;
+ padding-top: 5px;
+ width: 60%;
+}
+
+.receptor .nombre{
+ color: #000;
+ font-size: 17px;
+ font-weight: bold;
+}
+
+.receptor .rfc{
+ color: #000;
+ font-size: 15px;
+ font-weight: bold;
+}
+
+.receptor .direccion,
+.receptor .estado{
+ color: #000;
+ font-size: 10px;
+}
+
+.receptor img{
+ width: 10px;
+}
+
+.receptor .correo,
+.receptor .telefono{
+ float: right;
+ width: 40%;
+}
+
+.receptor .uso-cfdi{
+ color: #000;
+ font-size: 12px;
+ font-weight: bold;
+ float: left;
+ width: 60%;
+}
+
+header .cfdi{
+ box-sizing: border-box;
+ float: right;
+ font-size: 10px;
+ height: 100px;
+ margin-top: 10px;
+ position: relative;
+ padding-bottom: 5px;
+ padding-left: 45px;
+ padding-top: 5px;
+ width: 35%;
+}
+
+.cfdi .folio, .cfdi .tipo{
+ color: #000;
+ float: left;
+ font-size: 11px;
+ font-weight: bold;
+ line-height: 15px;
+ width: 50%;
+}
+
+.cfdi .folio span, .cfdi .tipo span{
+ color: #ed483d;
+}
+
+.cfdi .folio span{
+ font-size: 10px;
+}
+
+.cfdi .tipo span{
+ font-size: 10px;
+}
+
+.cfdi .folio-fiscal{
+ color: #000;
+ font-size: 9px;
+ font-weight: bold;
+ line-height: 15px;
+ text-align: center;
+}
+
+.cfdi .folio-fiscal span{
+ color: #ed483d;
+}
+
+.cfdi .fecha-emision, .cfdi .fecha-certificacion, .cfdi .lugar-expedicion{
+ color: #333;
+ font-size: 10px;
+ font-weight: bold;
+ line-height: 15px;
+}
+
+.conceptos{
+ margin-bottom: 10px;
+ margin-top: 20px;
+ width: 100%;
+ text-align: center;
+}
+
+.conceptos th{
+ background-color: #975759;
+ color: #fff;
+ font-size: 11px;
+ line-height: 15px;
+}
+
+.conceptos .clave{
+ width: 10%;
+}
+
+.conceptos .descripcion{
+ width: 45%;
+}
+.conceptos .descripcion div{
+ text-align: justify;
+}
+
+.conceptos .unidad{
+ width: 8%;
+}
+
+.conceptos .cantidad{
+ width: 9%;
+}
+
+.conceptos .valor-unitario{
+ width: 13%;
+}
+
+.conceptos .importe{
+ width: 15%;
+}
+
+.conceptos td{
+ background-color: #975759;
+ color: #000;
+ font-size: 11px;
+ line-height: 15px;
+}
+
+.conceptos td.clave, .conceptos td.unidad, .conceptos td.valor-unitario{
+ background-color: #dbc5c6;
+}
+
+.conceptos td.descripcion, .conceptos td.cantidad, .conceptos td.importe{
+ background-color: #f0e7e7;
+}
+
+.conceptos td.valor-unitario, .conceptos td.importe{
+ text-align: right;
+}
+
+.total-letras{
+ float: left;
+ font-size: 12px;
+ line-height: 15px;
+ margin: 5px 0;
+ width: 63%;
+}
+
+table.subtotal{
+ float: right;
+ font-size: 12px;
+ line-height: 15px;
+ text-align: right;
+ width: 37%;
+ font-weight: bold;
+}
+
+table.subtotal th{
+ background-color: #f0e7e7;
+ width: 60%;
+ padding: 3px 3px 3px 3px;
+}
+
+table.subtotal td{
+ background-color: #dbc5c6;
+ width: 40%;
+ padding: 3px 3px 3px 3px;
+}
+
+.condiciones-pago{
+ font-size: 11px;
+ line-height: 14px;
+}
+
+.notas{
+ float: left;
+ font-size: 11px;
+ margin: 20px 0;
+ width: 70%;
+}
+
+.formapago-metodopago, .moneda-tipocambio, .tiporelacion, .relacionados{
+ float: left;
+ font-size: 11px;
+ line-height: 12px;
+ width: 70%;
+}
+
+.factura-info{
+ float: left;
+ font-size: 11px;
+ line-height: 13px;
+ width: 70%;
+}
+
+.tipocomite, .tipoproceso, .idcontabilidad{
+ float: right;
+ font-size: 10px;
+ line-height: 12px;
+ width: 20%;
+}
+
+.sello{
+ margin: 10px 0;
+}
+
+.sello .cbb{
+ border: 1px solid #000;
+ height: auto;
+ margin-top: 10px;
+ margin-right: 1px;
+ width: 18%;
+}
+
+.sello .cadenas-sello{
+ color: #000;
+ float: right;
+ font-size: 8px;
+ font-weight: bold;
+ line-height: 15px;
+ width: 79%;
+ margin-left: 5px;
+ margin-right: 5px;
+ word-break: break-all;
+}
+
+.sello .cadenas-sello .cadena{
+ margin: 5px 0;
+}
+
+.sello .cadenas-sello .cadena div{
+ font-weight: normal;
+ background-color: #dbc5c6;
+ color: #7d1916;
+}
+
+.cadena-original{
+ color: #000;
+ float: right;
+ font-size: 8px;
+ font-weight: bold;
+ line-height: 15px;
+ width: 99%;
+ margin: 5px;
+ word-break: break-all;
+}
+.cadena-original div{
+ font-weight: normal;
+ background-color: #dbc5c6;
+ color: #7d1916;
+}
+
+.rfc-pac{
+ background-color: #dbc5c6;
+ color: #7d1916;
+ font-size: 10px;
+ line-height: 15px;
+ text-align: center;
+ margin: 5px;
+}
diff --git a/source/static/fonts/avenir_next/.DS_Store b/source/static/fonts/avenir_next/.DS_Store
new file mode 100644
index 0000000..7b40fe9
Binary files /dev/null and b/source/static/fonts/avenir_next/.DS_Store differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.eot b/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.eot
new file mode 100755
index 0000000..6ff4d6c
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.eot differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.svg b/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.svg
new file mode 100755
index 0000000..3d73033
--- /dev/null
+++ b/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.svg
@@ -0,0 +1,2842 @@
+
+
+
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.ttf b/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.ttf
new file mode 100755
index 0000000..d32eacb
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.ttf differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.woff b/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.woff
new file mode 100755
index 0000000..82bac28
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.woff differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.woff2 b/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.woff2
new file mode 100755
index 0000000..018ab10
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-Bold.woff2 differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-It.eot b/source/static/fonts/avenir_next/AvenirNextLTPro-It.eot
new file mode 100755
index 0000000..67eff78
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-It.eot differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-It.svg b/source/static/fonts/avenir_next/AvenirNextLTPro-It.svg
new file mode 100755
index 0000000..7187cbc
--- /dev/null
+++ b/source/static/fonts/avenir_next/AvenirNextLTPro-It.svg
@@ -0,0 +1,1956 @@
+
+
+
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-It.ttf b/source/static/fonts/avenir_next/AvenirNextLTPro-It.ttf
new file mode 100755
index 0000000..433a0a0
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-It.ttf differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-It.woff b/source/static/fonts/avenir_next/AvenirNextLTPro-It.woff
new file mode 100755
index 0000000..4afb82b
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-It.woff differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-It.woff2 b/source/static/fonts/avenir_next/AvenirNextLTPro-It.woff2
new file mode 100755
index 0000000..5c1f3b7
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-It.woff2 differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.eot b/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.eot
new file mode 100755
index 0000000..f91a884
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.eot differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.svg b/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.svg
new file mode 100755
index 0000000..ea10cd4
--- /dev/null
+++ b/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.svg
@@ -0,0 +1,2825 @@
+
+
+
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.ttf b/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.ttf
new file mode 100755
index 0000000..ad46859
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.ttf differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.woff b/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.woff
new file mode 100755
index 0000000..f19e41f
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.woff differ
diff --git a/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.woff2 b/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.woff2
new file mode 100755
index 0000000..32eccb0
Binary files /dev/null and b/source/static/fonts/avenir_next/AvenirNextLTPro-Regular.woff2 differ
diff --git a/source/static/img/correo.png b/source/static/img/correo.png
new file mode 100644
index 0000000..3e3233f
Binary files /dev/null and b/source/static/img/correo.png differ
diff --git a/source/static/img/tcm970625mb1.png b/source/static/img/tcm970625mb1.png
new file mode 100644
index 0000000..37dbe13
Binary files /dev/null and b/source/static/img/tcm970625mb1.png differ
diff --git a/source/static/img/telefono.png b/source/static/img/telefono.png
new file mode 100644
index 0000000..edccef9
Binary files /dev/null and b/source/static/img/telefono.png differ
diff --git a/source/static/img/web.png b/source/static/img/web.png
new file mode 100644
index 0000000..fe9fde2
Binary files /dev/null and b/source/static/img/web.png differ
diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js
index 9569a3c..7610f18 100644
--- a/source/static/js/controller/admin.js
+++ b/source/static/js/controller/admin.js
@@ -37,6 +37,7 @@ var controllers = {
$$('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)
+ $$('cmd_clean_email').attachEvent('onItemClick', cmd_clean_email_click)
$$('emisor_logo').attachEvent('onItemClick', emisor_logo_click)
$$('cmd_emisor_agregar_cuenta').attachEvent('onItemClick', cmd_emisor_agregar_cuenta_click)
$$('cmd_emisor_eliminar_cuenta').attachEvent('onItemClick', cmd_emisor_eliminar_cuenta_click)
@@ -69,11 +70,15 @@ var controllers = {
tb_options.attachEvent('onChange', tab_options_change)
$$('txt_plantilla_factura_32').attachEvent('onItemClick', txt_plantilla_factura_32_click)
$$('txt_plantilla_factura_33').attachEvent('onItemClick', txt_plantilla_factura_33_click)
- $$('txt_plantilla_factura_33j').attachEvent('onItemClick', txt_plantilla_factura_33j_click)
+ $$('txt_plantilla_factura_html').attachEvent('onItemClick', txt_plantilla_factura_html_click)
+ $$('txt_plantilla_factura_json').attachEvent('onItemClick', txt_plantilla_factura_json_click)
+ $$('txt_plantilla_factura_css').attachEvent('onItemClick', txt_plantilla_factura_css_click)
$$('txt_plantilla_ticket').attachEvent('onItemClick', txt_plantilla_ticket_click)
$$('txt_plantilla_donataria').attachEvent('onItemClick', txt_plantilla_donataria_click)
$$('txt_plantilla_nomina1233').attachEvent('onItemClick', txt_plantilla_nomina1233_click)
$$('txt_plantilla_pagos10').attachEvent('onItemClick', txt_plantilla_pagos10_click)
+ $$('make_pdf_from').attachEvent('onChange', opt_make_pdf_from_on_change)
+
//~ Partners
$$('chk_config_change_balance_partner').attachEvent('onItemClick', chk_config_item_click)
@@ -840,6 +845,46 @@ function cmd_guardar_correo_click(){
}
+function clean_config_mail(){
+ var form = $$('form_correo')
+
+ webix.ajax().sync().post('/config', {'opt': 'clean_email'}, {
+ 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){
+ form.setValues({})
+ msg_ok(values.msg)
+ }else{
+ msg_error(values.msg)
+ }
+ }
+ })
+}
+
+
+function cmd_clean_email_click(){
+ msg = '¿Estás seguro de limpiar todos los datos?
'
+ msg += 'ESTA ACCIÓN NO SE PUEDE DESHACER'
+
+ webix.confirm({
+ title: 'Configuración de correo',
+ ok: 'Si',
+ cancel: 'No',
+ type: 'confirm-error',
+ text: msg,
+ callback:function(result){
+ if(result){
+ clean_config_mail()
+ }
+ }
+ })
+}
+
+
function emisor_logo_click(id, e){
var w = webix.ui({
@@ -978,13 +1023,98 @@ function txt_plantilla_factura_32_click(e){
}
-function txt_plantilla_factura_33j_click(e){
+function txt_plantilla_factura_html_click(e){
var body_elements = [
- {cols: [{width: 100}, {view: 'uploader', id: 'up_template', autosend: true, link: 'lst_files',
- value: 'Seleccionar archivo', upload: '/files/txt_plantilla_factura_33j',
- width: 200}, {width: 100}]},
- {view: 'list', id: 'lst_files', type: 'uploader', autoheight:true,
+ {cols: [
+ {width: 100},
+ {view: 'uploader', id: 'up_template', autosend: true,
+ link: 'lst_files', value: 'Seleccionar archivo',
+ upload: '/files/txt_plantilla_factura_html', width: 200},
+ {width: 100}]},
+ {view: 'list', id: 'lst_files', type: 'uploader', autoheight: true,
+ borderless: true},
+ {},
+ {cols: [{}, {view: 'button', label: 'Cerrar', autowidth: true,
+ click:("$$('win_template').close();")}, {}]}
+ ]
+
+ var w = webix.ui({
+ view: 'window',
+ id: 'win_template',
+ modal: true,
+ position: 'center',
+ head: 'Subir Plantilla 3.3 HTML',
+ body: {
+ view: 'form',
+ elements: body_elements,
+ }
+ })
+
+ w.show()
+
+ $$('up_template').attachEvent('onUploadComplete', function(response){
+ if(response.ok){
+ $$('txt_plantilla_factura_html').setValue(response.name)
+ msg_ok('Plantilla cargada correctamente')
+ }else{
+ msg_error(response.name)
+ }
+ })
+}
+
+
+function txt_plantilla_factura_css_click(e){
+
+ var body_elements = [
+ {cols: [
+ {width: 100},
+ {view: 'uploader', id: 'up_template', autosend: true,
+ link: 'lst_files', value: 'Seleccionar archivo',
+ upload: '/files/txt_plantilla_factura_css', width: 200},
+ {width: 100}]},
+ {view: 'list', id: 'lst_files', type: 'uploader', autoheight: true,
+ borderless: true},
+ {},
+ {cols: [{}, {view: 'button', label: 'Cerrar', autowidth: true,
+ click:("$$('win_template').close();")}, {}]}
+ ]
+
+ var w = webix.ui({
+ view: 'window',
+ id: 'win_template',
+ modal: true,
+ position: 'center',
+ head: 'Subir Archivo de Estilos CSS',
+ body: {
+ view: 'form',
+ elements: body_elements,
+ }
+ })
+
+ w.show()
+
+ $$('up_template').attachEvent('onUploadComplete', function(response){
+ if(response.ok){
+ $$('txt_plantilla_factura_css').setValue(response.name)
+ msg_ok('Plantilla cargada correctamente')
+ }else{
+ msg_error(response.name)
+ }
+ })
+}
+
+
+function txt_plantilla_factura_json_click(e){
+
+ var body_elements = [
+ {cols: [
+ {width: 100},
+ {view: 'uploader', id: 'up_template', autosend: true,
+ link: 'lst_files', value: 'Seleccionar archivo',
+ upload: '/files/txt_plantilla_factura_json', width: 200},
+ {width: 100}]},
+ {view: 'list', id: 'lst_files', type: 'uploader', autoheight: true,
borderless: true},
{},
{cols: [{}, {view: 'button', label: 'Cerrar', autowidth: true,
@@ -1007,7 +1137,7 @@ function txt_plantilla_factura_33j_click(e){
$$('up_template').attachEvent('onUploadComplete', function(response){
if(response.ok){
- $$('txt_plantilla_factura_33j').setValue(response.name)
+ $$('txt_plantilla_factura_json').setValue(response.name)
msg_ok('Plantilla cargada correctamente')
}else{
msg_error(response.name)
@@ -2344,3 +2474,24 @@ function grid_admin_bancos_before_edit_stop(state, editor){
admin_sat_bank_update_rfc(row.id, state.value)
}
}
+
+
+function opt_make_pdf_from_on_change(new_value, old_value){
+ var values = {'make_pdf_from': new_value}
+
+ 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_error(values.msg)
+ }
+ }
+ })
+
+}
+
+
diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js
index fac40e6..893312e 100644
--- a/source/static/js/controller/invoices.js
+++ b/source/static/js/controller/invoices.js
@@ -1282,11 +1282,15 @@ function enviar_correo(row){
function get_pdf(id){
- //~ location = '/doc/pdf/' + id
window.open('/doc/pdf/' + id, '_blank')
}
+function get_html(id){
+ window.open('/doc/html/' + id, '_blank')
+}
+
+
function grid_invoices_click(id, e, node){
var row = this.getItem(id)
@@ -1294,6 +1298,8 @@ function grid_invoices_click(id, e, node){
location = '/doc/xml/' + row.id
}else if(id.column == 'pdf'){
get_pdf(row.id)
+ }else if(id.column == 'html'){
+ get_html(row.id)
}else if(id.column == 'ods'){
location = '/doc/ods/' + row.id
}else if(id.column == 'zip'){
diff --git a/source/static/js/controller/util.js b/source/static/js/controller/util.js
index bae66bd..e74b00e 100644
--- a/source/static/js/controller/util.js
+++ b/source/static/js/controller/util.js
@@ -59,6 +59,7 @@ function get_icon(tipo){
icons = {
xml: 'fa-file-code-o',
pdf: 'fa-file-pdf-o',
+ html: 'fa-html5',
zip: 'fa-file-zip-o',
email: 'fa-envelope-o',
print: 'fa-print',
diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js
index 30fd7e7..f04eebc 100644
--- a/source/static/js/ui/admin.js
+++ b/source/static/js/ui/admin.js
@@ -204,7 +204,7 @@ var emisor_otros_datos= [
{template: 'Generales', type: 'section'},
{cols: [
{view: 'search', id: 'emisor_logo', icon: 'file-image-o',
- name: 'emisor_logo', label: 'Logotipo: '},
+ name: 'emisor_logo', label: 'Logotipo: ', placeholder: 'Solo formato PNG'},
{view: 'text', id: 'emisor_nombre_comercial',
name: 'emisor_nombre_comercial', label: 'Nombre comercial: '},
]},
@@ -244,7 +244,7 @@ var emisor_otros_datos= [
{view: 'text', id: 'token_timbrado',
name: 'token_timbrado', label: 'Token de Timbrado: '},
{view: 'text', id: 'token_soporte',
- name: 'token_soporte', label: 'Token para Respaldos: '},
+ name: 'token_soporte', label: 'Token de Soporte: '},
]
@@ -511,9 +511,12 @@ var emisor_correo = [
{cols: [{},
{view: 'button', id: 'cmd_probar_correo', label: 'Probar Configuración',
autowidth: true, type: 'form'},
- {maxWidth: 100},
+ {maxWidth: 50},
{view: 'button', id: 'cmd_guardar_correo', label: 'Guardar Configuración',
autowidth: true, type: 'form'},
+ {maxWidth: 50},
+ {view: 'button', id: 'cmd_clean_email', label: 'Limpiar Configuración',
+ autowidth: true, type: 'form'},
{}]}
]
@@ -579,33 +582,49 @@ var form_correo = {
}
+var type_make_pdf = [
+ {id: 1, value: 'ODS'},
+ {id: 2, value: 'JSON'},
+]
+
+
var options_templates = [
{maxHeight: 15},
- {cols: [{maxWidth: 15},
+ {cols: [{maxWidth: 20},
{view: 'search', id: 'txt_plantilla_factura_32', name: 'plantilla_factura_32',
label: 'Plantilla Factura v3.2 (ODS): ', labelPosition: 'top',
- icon: 'file'}, {}]},
- {maxHeight: 20},
- {cols: [{maxWidth: 15},
+ icon: 'file'}, {maxWidth: 25},
{view: 'search', id: 'txt_plantilla_factura_33', labelPosition: 'top',
- label: 'Plantilla Factura v3.3 (ODS): ', icon: 'file'}, {}]},
+ label: 'Plantilla Factura v3.3 (ODS): ', icon: 'file'},
+ {maxWidth: 20} ]},
{maxHeight: 20},
- {cols: [{maxWidth: 15},
- {view: 'search', id: 'txt_plantilla_factura_33j', name: 'plantilla_factura_33j',
+ {cols: [{maxWidth: 20},
+ {view: 'search', id: 'txt_plantilla_factura_html', name: 'plantilla_factura_html',
+ label: 'Plantilla Factura v3.3 (HTML): ', labelPosition: 'top',
+ icon: 'file'}, {maxWidth: 25},
+ {view: 'search', id: 'txt_plantilla_factura_css', name: 'plantilla_factura_css',
+ label: 'Archivo de estilos (CSS): ', labelPosition: 'top',
+ icon: 'file'}, {maxWidth: 20} ]},
+
+ {maxHeight: 20},
+ {cols: [{maxWidth: 20},
+ {view: 'search', id: 'txt_plantilla_factura_json', name: 'plantilla_factura_json',
label: 'Plantilla Factura v3.3 (JSON): ', labelPosition: 'top',
- icon: 'file'}, {}]},
+ icon: 'file'}, {maxWidth: 25},
+ {}, {maxWidth: 20} ]},
+
{maxHeight: 20},
- {cols: [{maxWidth: 15},
+ {cols: [{maxWidth: 20},
{view: 'search', id: 'txt_plantilla_nomina1233', name: 'plantilla_nomina1233',
label: 'Plantilla Nomina v1.2 - Cfdi 3.3 (ODS): ', labelPosition: 'top',
- icon: 'file'}, {}]},
+ icon: 'file'}, {maxWidth: 40}, {}]},
{maxHeight: 20},
- {cols: [{maxWidth: 15},
+ {cols: [{maxWidth: 20},
{view: 'search', id: 'txt_plantilla_pagos10', name: 'plantilla_pagos10',
label: 'Plantilla Factura de Pagos v1.0 - Cfdi 3.3 (ODS): ',
- labelPosition: 'top', icon: 'file'}, {}]},
+ labelPosition: 'top', icon: 'file'}, {maxWidth: 40}, {}]},
{maxHeight: 20},
- {cols: [{maxWidth: 15},
+ {cols: [{maxWidth: 20},
{view: 'search', id: 'txt_plantilla_ticket', name: 'plantilla_ticket',
label: 'Plantilla para Tickets (ODS): ', labelPosition: 'top',
icon: 'file'},
@@ -614,6 +633,10 @@ var options_templates = [
icon: 'file'},
{}]},
{maxHeight: 20},
+ {cols: [{maxWidth: 20},
+ {view: 'radio', id: 'make_pdf_from', name: 'make_pdf_from', labelWidth: 150,
+ label: 'Generar PDF desde: ', value: 1, options: type_make_pdf},
+ {}]},
{}]
diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js
index 49fe2f4..09d7e1d 100644
--- a/source/static/js/ui/invoices.js
+++ b/source/static/js/ui/invoices.js
@@ -267,6 +267,7 @@ var grid_invoices_cols = [
fillspace: true, sort: 'string', footer: {text: '$ 0.00', colspan: 2}},
{id: 'xml', header: 'XML', adjust: 'data', template: get_icon('xml')},
{id: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')},
+ {id: 'html', header: 'HTML', adjust: 'data', template: get_icon('html')},
{id: 'ods', header: 'ODS', adjust: 'data', template: get_icon('table')},
{id: 'zip', header: 'ZIP', adjust: 'data', template: get_icon('zip')},
{id: 'email', header: '', adjust: 'data', template: get_icon('email')}
diff --git a/source/templates/plantilla_factura.html b/source/templates/plantilla_factura.html
new file mode 100644
index 0000000..f3b6d1c
--- /dev/null
+++ b/source/templates/plantilla_factura.html
@@ -0,0 +1,204 @@
+
+
+
Clave | +Descripción | +Unidad | +Cantidad | +Valor Unitario | +Importe | +
---|---|---|---|---|---|
${c['clave']} | +${c['descripcion']} |
+ ${c['unidad']} | +${c['cantidad']} | +${c['valorunitario']} | +${c['importe']} | +
${total[0]} | +${total[1]} | +
---|