From 0d52e9b570d28687f943503cf1fcbdafb8f2d277 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 20 Nov 2018 00:03:07 -0600 Subject: [PATCH] Factura HTML v1 --- source/app/controllers/util.py | 37 ++-- source/app/main.py | 4 - source/app/middleware.py | 18 +- source/app/models/main.py | 167 +++++++++++------- source/app/settings.py | 24 ++- .../static/css/{estilos.css => invoice.css} | 34 ++-- source/static/js/ui/admin.js | 4 +- source/templates/plantilla_factura.html | 11 +- 8 files changed, 178 insertions(+), 121 deletions(-) rename source/static/css/{estilos.css => invoice.css} (95%) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index f681622..866b155 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 . +import base64 import datetime import getpass import hashlib +import io import json import locale import mimetypes @@ -68,6 +70,13 @@ from settings import SEAFILE_SERVER, USAR_TOKEN, API, DECIMALES_TAX from .configpac import AUTH +# ~ v2 +from settings import ( + MXN, + PATHS, +) + + def _call(args): return subprocess.check_output(args, shell=True).decode() @@ -650,7 +659,7 @@ class LIBO(object): self._ctx = None self._sm = None self._desktop = None - self._currency = 'MXN' + self._currency = MXN self._total_cantidades = 0 if self.is_running: ctx = uno.getComponentContext() @@ -1583,11 +1592,11 @@ def to_pdf(data, emisor_rfc, ods=False): def format_currency(value, currency, digits=2): c = { - 'MXN': '$', + MXN: '$', 'USD': '$', 'EUR': '€', } - s = c.get(currency, 'MXN') + s = c.get(currency, MXN) return f'{s} {float(value):,.{digits}f}' @@ -1597,7 +1606,7 @@ def to_html(data): template = template_lookup.get_template(name) except TopLevelLookupException: template = template_lookup.get_template('plantilla_factura.html') - data['rfc'] = 'estilos' + data['rfc'] = 'invoice' return template.render(**data) @@ -1651,11 +1660,16 @@ 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): @@ -1907,6 +1921,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 @@ -2110,11 +2125,7 @@ def get_date(value, next_day=False): 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': + if opt == 'txt_plantilla_factura_32': tmp = file_obj.filename.split('.') ext = tmp[-1].lower() if ext != 'ods': 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/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 2d0d909..7d5241f 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}) @@ -765,6 +773,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): @@ -817,12 +829,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['STATIC'], '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): @@ -850,6 +880,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, @@ -1369,6 +1400,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()) @@ -1949,6 +1984,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']) @@ -3674,26 +3713,40 @@ 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_others_values(self, invoice, emisor): - v_cancel = {True: 'inline', False: 'none'} - v_type = {'I': 'Ingreso', 'E': 'Egreso', 'T': 'Traslado'} - v_method = { - 'PUE': 'Pago en una sola exhibición', - 'PPD': 'Pago en parcialidades o diferido', - } - v_tax = { - '001': 'ISR', - '002': 'IVA', - '003': 'IEPS', - } + 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 = FORMAT.format(float(d['tasaocuota'])) + 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': v_cancel.get(invoice.cancelada), + 'cancelada': VALUES_PDF['CANCEL'].get(invoice.cancelada), 'cfdi_notas': invoice.notas, } @@ -3704,17 +3757,18 @@ class Facturas(BaseModel): data.update({f'cfdi_{k.lower()}': v for k, v in d.items()}) - data['cfdi_tipodecomprobante'] = v_type[data['cfdi_tipodecomprobante']] + data['cfdi_tipodecomprobante'] = \ + VALUES_PDF['TYPE'][data['cfdi_tipodecomprobante']] if data.get('cfdi_formapago', ''): - obj = SATFormaPago.get(SATFormaPago.key==invoice.forma_pago) - data['cfdi_formapago'] = str(obj) + 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'], v_method[data['cfdi_metodopago']]) + data['cfdi_metodopago'], + VALUES_PDF['METHOD'][data['cfdi_metodopago']]) - obj = SATMonedas.get(SATMonedas.key==currency) - data['cfdi_moneda'] = str(obj) + 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( @@ -3725,60 +3779,30 @@ class Facturas(BaseModel): for node in xml: if 'Emisor' in node.tag: - d = util.get_dict(node.attrib) - data.update( - {f'emisor_{k.lower()}': v for k, v in d.items()} - ) + d = {f'emisor_{k.lower()}': v for k, v in node.attrib.items()} + data.update(d) elif 'Receptor' in node.tag: - d = util.get_dict(node.attrib) - data.update( - {f'receptor_{k.lower()}': v for k, v in d.items()} - ) + 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: - d = util.get_dict(subnode.attrib) - concepto = { - 'clave': f"{d['noidentificacion']}
({d['claveprodserv']})", - 'descripcion': self._get_description(self, subnode, d), - 'unidad': f"{d['unidad']}
({d['claveunidad']})", - 'cantidad': d['cantidad'], - 'valorunitario': util.format_currency(d['valorunitario'], currency), - 'importe': util.format_currency(d['importe'], currency), - } + concepto = self._get_concepto(self, subnode, currency) data['conceptos'].append(concepto) elif 'Impuestos' in node.tag: for subnode in node: - if 'Traslados' in subnode.tag: - for t in subnode: - d = util.get_dict(t.attrib) - name = v_tax.get(d['impuesto']) - tasa = FORMAT.format(float(d['tasaocuota'])) - title = f"Traslado {name} {tasa}" - importe = util.format_currency(d['importe'], currency) - data['totales'].append((title, importe)) - elif 'Retenciones' in subnode.tag: - for r in subnode: - d = util.get_dict(r.attrib) - name = v_tax.get(d['impuesto']) - tasa = FORMAT.format(float(d['tasaocuota'])) - title = f"Retención {name} {tasa}" - importe = util.format_currency(d['importe'], currency) - data['totales'].append((title, importe)) + self._get_tax(self, subnode, data, currency) elif 'Complemento' in node.tag: - for subnode in node: - if 'TimbreFiscalDigital' in subnode.tag: - d = util.get_dict(subnode.attrib) - data.update( - {f'timbre_{k.lower()}': v for k, v in d.items()} - ) + 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) - obj = SATRegimenes.get(SATRegimenes.key==data['emisor_regimenfiscal']) - data['emisor_regimenfiscal'] = str(obj) data['emisor_logo'] = data['emisor_rfc'].lower() - - obj = SATUsoCfdi.get(SATUsoCfdi.key==data['receptor_usocfdi']) - data['receptor_usocfdi'] = str(obj) + 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))) @@ -3786,7 +3810,16 @@ class Facturas(BaseModel): data['timbre_cadenaoriginal'] = f"""||{data['timbre_version']}| {data['timbre_uuid']}|{data['timbre_fechatimbrado']}| {data['timbre_sellocfd']}|{data['timbre_nocertificadosat']}||""" - # ~ print(data) + + 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 diff --git a/source/app/settings.py b/source/app/settings.py index 6897b81..13c166c 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -47,13 +47,15 @@ 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_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')) @@ -185,3 +187,21 @@ DEFAULT_SAT_NOMINA = { API = 'https://api.empresalibre.net{}' CURRENCY_MN = 'MXN' + +# ~ v2 +EXT = { + 'PNG': 'png', +} +MXN = 'MXN' +PATHS = { + 'STATIC': path_static, +} +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/estilos.css b/source/static/css/invoice.css similarity index 95% rename from source/static/css/estilos.css rename to source/static/css/invoice.css index 0dbd2a0..788ac7c 100644 --- a/source/static/css/estilos.css +++ b/source/static/css/invoice.css @@ -129,11 +129,12 @@ table.subtotal td{ } body { - background-color: rgb(204,204,204); font-family: 'Avenir Next'; } #plantilla { + border: 1px solid #fff; + border-color: rgb(204,204,204); background: #fff; display: block; margin: 0 auto; @@ -435,26 +436,26 @@ table.subtotal td{ float: left; font-size: 10px; line-height: 12px; - width: 85%; + width: 80%; } .tipocomite, .tipoproceso, .idcontabilidad{ float: right; font-size: 10px; line-height: 12px; - width: 30%; + width: 20%; } .sello{ - margin: 20px 0; + margin: 10px 0; } .sello .cbb{ border: 1px solid #000; height: auto; - margin-top: 20px; - margin-right: 5%; - width: 15%; + margin-top: 10px; + margin-right: 1%; + width: 18%; } .sello .cadenas-sello{ @@ -462,7 +463,7 @@ table.subtotal td{ float: right; font-size: 8px; font-weight: bold; - line-height: 16px; + line-height: 15px; width: 79%; margin-left: 2px; margin-right: 2px; @@ -479,20 +480,17 @@ table.subtotal td{ color: #7d1916; } -.sello .cadena-original{ +.cadena-original{ color: #000; float: right; font-size: 8px; font-weight: bold; - line-height: 16px; - width: 100%; - margin: 10px; - margin-left: 5px; - margin-right: 2px; + line-height: 15px; + width: 99%; + margin: 5px; word-break: break-all; } - -.sello .cadena-original div{ +.cadena-original div{ font-weight: normal; background-color: #dbc5c6; color: #7d1916; @@ -502,7 +500,7 @@ table.subtotal td{ background-color: #dbc5c6; color: #7d1916; font-size: 10px; - line-height: 16px; + line-height: 15px; text-align: center; - margin-right: 2px; + margin: 5px; } diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 30fd7e7..c70d80c 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: '}, ] diff --git a/source/templates/plantilla_factura.html b/source/templates/plantilla_factura.html index 84639cb..c66689a 100644 --- a/source/templates/plantilla_factura.html +++ b/source/templates/plantilla_factura.html @@ -2,10 +2,10 @@ - + Empresa Libre - +
Cancelada @@ -122,6 +122,7 @@ % endfor +
${cfdi_condicionespago} @@ -153,8 +154,7 @@
- - +
Sello Digital del CFDI: @@ -171,14 +171,13 @@
-
+
Cadena original del complemento de certificación digital del SAT:
${timbre_cadenaoriginal}
-