diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py index fb61c4b..dee3bfa 100644 --- a/source/app/controllers/cfdi_xml.py +++ b/source/app/controllers/cfdi_xml.py @@ -117,7 +117,7 @@ class CFDI(object): return def _relacionados(self, datos): - if not datos['tipo'] or not datos['cfdis']: + if not datos or not datos['tipo'] or not datos['cfdis']: return node_name = '{}:CfdiRelacionados'.format(self._pre) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 70266d7..a73090d 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -4,6 +4,7 @@ import datetime import getpass import hashlib import json +import locale import mimetypes import os import re @@ -853,7 +854,7 @@ def to_letters(value, moneda): 'USD': 'dólar', 'EUR': 'euro', } - return NumLet(value, monedas[moneda]).letras + return NumLet(value, monedas.get(moneda, moneda)).letras def get_qr(data): @@ -863,8 +864,17 @@ def get_qr(data): return path -def _comprobante(values, options): - data = CaseInsensitiveDict(values) +def _get_relacionados(doc, version): + node = doc.find('{}CfdiRelacionados'.format(PRE[version])) + if node is None: + return '' + + uuids = ['UUID: {}'.format(n.attrib['UUID']) for n in node.getchildren()] + return '\n'.join(uuids) + + +def _comprobante(doc, options): + data = CaseInsensitiveDict(doc.attrib.copy()) del data['certificado'] data['totalenletras'] = to_letters(float(data['total']), data['moneda']) @@ -883,21 +893,49 @@ def _comprobante(values, options): data['condicionesdepago'] = \ 'Condiciones de pago: {}'.format(data['condicionesdepago']) data['moneda'] = options['moneda'] + data['tiporelacion'] = options.get('tiporelacion', '') + data['relacionados'] = _get_relacionados(doc, data['version']) + else: + fields = { + 'formaDePago': 'Forma de Pago: {}\n', + 'metodoDePago': 'Método de pago: {}\n', + 'condicionesDePago': 'Condiciones de Pago: {}\n', + 'NumCtaPago': 'Número de Cuenta de Pago: {}\n', + 'Moneda': 'Moneda: {}\n', + 'TipoCambio': 'Tipo de Cambio: {}', + } + datos = '' + for k, v in fields.items(): + if k in data: + datos += v.format(data[k]) + data['datos'] = datos + + fecha = parser.parse(data['fecha']) + try: + locale.setlocale(locale.LC_TIME, "es_MX.UTF-8") + except: + pass + data['fechaformato'] = fecha.strftime('%A, %d de %B de %Y') 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])) + emisor = doc.find('{}Emisor'.format(PRE[version])) + data = CaseInsensitiveDict(emisor.attrib.copy()) + node = emisor.find('{}DomicilioFiscal'.format(PRE[version])) if not node is None: data.update(CaseInsensitiveDict(node.attrib.copy())) - data['regimenfiscal'] = values['regimenfiscal'] + + if version == '3.2': + node = emisor.find('{}RegimenFiscal'.format(PRE[version])) + if not node is None: + data['regimenfiscal'] = node.attrib['Regimen'] + else: + data['regimenfiscal'] = values['regimenfiscal'] path = _join(PATH_MEDIA, 'logos', '{}.png'.format(data['rfc'].lower())) if is_file(path): @@ -912,6 +950,10 @@ def _receptor(doc, version, values): node = node.find('{}Domicilio'.format(PRE[version])) if not node is None: data.update(node.attrib.copy()) + + if version == '3.2': + return data + data['usocfdi'] = values['usocfdi'] return data @@ -1021,7 +1063,7 @@ def _timbre(doc, version, values): def get_data_from_xml(invoice, values): data = {'cancelada': invoice.cancelada} doc = parse_xml(invoice.xml) - data['comprobante'] = _comprobante(doc.attrib.copy(), values) + data['comprobante'] = _comprobante(doc, values) version = data['comprobante']['version'] data['emisor'] = _emisor(doc, version, values) data['receptor'] = _receptor(doc, version, values) @@ -1111,6 +1153,15 @@ def upload_file(rfc, opt, file_obj): name = '{}_3.3.ods'.format(rfc.lower()) path = _join(PATH_MEDIA, 'templates', 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' + return {'status': 'server', 'name': msg, 'ok': False} + + name = '{}_3.2.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'templates', name) if save_file(path, file_obj.file.read()): return {'status': 'server', 'name': file_obj.filename, 'ok': True} @@ -1182,25 +1233,46 @@ def get_bool(value): class ImportFacturaLibre(object): - def __init__(self, path): + def __init__(self, path, rfc): + self._rfc = rfc self._con = None self._cursor = None self._is_connect = self._connect(path) self._clientes = [] self._clientes_rfc = [] + self._error = '' + + @property + def error(self): + return self._error @property def is_connect(self): return self._is_connect + def _validate_rfc(self): + sql = "SELECT rfc FROM emisor LIMIT 1" + self._cursor.execute(sql) + obj = self._cursor.fetchone() + if obj is None: + self._error = 'No se encontró al emisor: {}'.format(self._rfc) + return False + + if obj['rfc'] != self._rfc: + self._error = 'Los datos no corresponden al emisor: {}'.format(self._rfc) + return False + + return True + def _connect(self, path): try: self._con = sqlite3.connect(path) self._con.row_factory = sqlite3.Row self._cursor = self._con.cursor() - return True + return self._validate_rfc() except Exception as e: log.error(e) + self._error = 'No se pudo conectar a la base de datos' return False def close(self): diff --git a/source/app/models/db.py b/source/app/models/db.py index c53fdfc..b7db743 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -14,6 +14,9 @@ class StorageEngine(object): def get_values(self, table, values=None): return getattr(self, '_get_{}'.format(table))(values) + def _get_validartimbrar(self, values): + return main.validar_timbrar() + def _get_preproductos(self, values): return main.PreFacturasDetalle.facturar(values['id']) diff --git a/source/app/models/main.py b/source/app/models/main.py index 6048a89..2a353a1 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -67,6 +67,40 @@ def upload_file(rfc, opt, file_obj): return result +def validar_timbrar(): + try: + obj = Emisor.select()[0] + except IndexError: + msg = 'Es necesario agregar los datos del emisor' + return {'ok': False, 'msg': msg} + + try: + obj = Folios.select()[0] + except IndexError: + msg = 'Es necesaria al menos una serie de folios' + return {'ok': False, 'msg': msg} + + msg = 'Es necesario configurar un certificado de sellos' + try: + obj = Certificado.select()[0] + except IndexError: + return {'ok': False, 'msg': msg} + + if not obj.serie: + return {'ok': False, 'msg': msg} + + dias = obj.hasta - util.now() + if dias.days < 0: + msg = 'El certificado ha vencido, es necesario cargar uno nuevo' + return {'ok': False, 'msg': msg} + + msg = '' + if dias.days < 15: + msg = 'El certificado vence en: {} días.'.format(dias.days) + + return {'ok': True, 'msg': msg} + + class Configuracion(BaseModel): clave = TextField(unique=True) valor = TextField(default='') @@ -288,7 +322,11 @@ class Emisor(BaseModel): @classmethod def get_regimenes(cls): - obj = Emisor.select()[0] + try: + obj = Emisor.select()[0] + except IndexError: + return () + rows = [{'id': row.key, 'value': row.name} for row in obj.regimenes] return tuple(rows) @@ -1248,6 +1286,10 @@ class Facturas(BaseModel): def _get_not_in_xml(self, invoice): values = {} + + if invoice.version == '3.2': + return values + obj = SATRegimenes.get(SATRegimenes.key==invoice.regimen_fiscal) values['regimenfiscal'] = str(obj) @@ -1267,6 +1309,12 @@ class Facturas(BaseModel): obj = SATMonedas.get(SATMonedas.key==invoice.moneda) values['moneda'] = str(obj) + if invoice.tipo_relacion: + obj = SATTipoRelacion.get(SATTipoRelacion.key==invoice.tipo_relacion) + values['tiporelacion'] = str(obj) + + print ('\nTR', invoice.tipo_relacion) + return values @classmethod @@ -2695,9 +2743,9 @@ def _importar_factura_libre(archivo): conectar(args) log.info('Importando datos...') - app = util.ImportFacturaLibre(archivo) + app = util.ImportFacturaLibre(archivo, rfc) if not app.is_connect: - log.error('\tNo se pudo conectar a la base de datos') + log.error('\t{}'.format(app.error)) return data = app.import_data() diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index 130e675..408e280 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -29,6 +29,7 @@ var controllers = { //~ Opciones tb_options = $$('tab_options').getTabbar() 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) } } @@ -392,9 +393,9 @@ function cmd_subir_certificado_click(){ return } - var rfc = $$('form_cert').getValues()['cert_rfc'] + var serie = $$('form_cert').getValues()['cert_serie'] - if(rfc){ + if(serie){ msg = 'Ya existe un certificado guardado

¿Deseas reemplazarlo?' webix.confirm({ title: 'Certificado Existente', @@ -686,7 +687,7 @@ function txt_plantilla_factura_33_click(e){ id: 'win_template', modal: true, position: 'center', - head: 'Subir Plantilla', + head: 'Subir Plantilla 3.3 ODT', body: { view: 'form', elements: body_elements, @@ -706,6 +707,44 @@ function txt_plantilla_factura_33_click(e){ } +function txt_plantilla_factura_32_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_32', + 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.2 ODT', + body: { + view: 'form', + elements: body_elements, + } + }) + + w.show() + + $$('up_template').attachEvent('onUploadComplete', function(response){ + if(response.ok){ + $$('txt_plantilla_factura_32').setValue(response.name) + msg_sucess('Plantilla cargada correctamente') + }else{ + msg_error(response.name) + } + }) +} + + function tab_options_change(nv, ov){ var cv = { Plantillas: 'templates', diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 9bf0193..f486f06 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -95,6 +95,19 @@ function default_config(){ get_regimen_fiscal() table_pt.clear() table_totals.clear() + + webix.ajax().sync().get('/values/validartimbrar', function(text, data){ + var values = data.json() + if(!values.ok){ + msg_error(values.msg) + $$('cmd_timbrar').disable() + }else{ + if(values.msg){ + msg_error(values.msg) + } + $$('cmd_timbrar').enable() + } + }) }