From 82edc2a75a2c920dc3bc5761cd3d5add105f13b3 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Fri, 27 Oct 2017 10:27:21 -0500 Subject: [PATCH 01/12] Validado el timbrado con postgres --- source/app/conf.py.example | 6 -- source/app/controllers/pac.py | 103 ++++++++++++++++++++++++++++------ source/app/models/main.py | 8 ++- 3 files changed, 94 insertions(+), 23 deletions(-) diff --git a/source/app/conf.py.example b/source/app/conf.py.example index 1f8e28d..70d42ef 100644 --- a/source/app/conf.py.example +++ b/source/app/conf.py.example @@ -1,10 +1,4 @@ #!/usr/bin/env python -from peewee import SqliteDatabase DEBUG = True -ID_SUPPORT = '' - -DATABASE = None -if DEBUG: - DATABASE = SqliteDatabase('empresalibre.sqlite') diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py index 25b7fa4..d123861 100644 --- a/source/app/controllers/pac.py +++ b/source/app/controllers/pac.py @@ -443,21 +443,41 @@ class Finkok(object): return result def add_token(self, rfc, email): - """ + """Agrega un nuevo token al cliente para timbrado. Se requiere cuenta de reseller para usar este método + + Args: + rfc (str): El RFC del cliente, ya debe existir + email (str): El correo del cliente, funciona como USER al timbrar + + Returns: + dict + 'username': 'username', + 'status': True or False + 'name': 'name', + 'success': True or False + 'token': 'Token de timbrado', + 'message': None """ + auth = AUTH['RESELLER'] + method = 'util' client = Client( URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': AUTH['USER'], - 'password': AUTH['PASS'], + 'username': auth['USER'], + 'password': auth['PASS'], 'name': rfc, 'token_username': email, 'taxpayer_id': rfc, 'status': True, } - result = client.service.add_token(**args) + try: + result = client.service.add_token(**args) + except Fault as e: + self.error = str(e) + return '' + return result def get_date(self): @@ -477,17 +497,31 @@ class Finkok(object): return result.datetime def add_client(self, rfc, type_user=False): - """ + """Agrega un nuevo cliente para timbrado. Se requiere cuenta de reseller para usar este método - type_user: False == 'P' == Prepago or True == 'O' == On demand + + Args: + rfc (str): El RFC del nuevo cliente + + Kwargs: + type_user (bool): False == 'P' == Prepago or True == 'O' == On demand + + Returns: + dict + 'message': + 'Account Created successfully' + 'Account Already exists' + 'success': True or False """ + auth = AUTH['RESELLER'] + tu = {False: 'P', True: 'O'} method = 'client' client = Client( URL[method], transport=self._transport, plugins=self._plugins) args = { - 'reseller_username': AUTH['USER'], - 'reseller_password': AUTH['PASS'], + 'reseller_username': auth['USER'], + 'reseller_password': auth['PASS'], 'taxpayer_id': rfc, 'type_user': tu[type_user], 'added': datetime.datetime.now().isoformat()[:19], @@ -505,13 +539,15 @@ class Finkok(object): Se requiere cuenta de reseller para usar este método status = 'A' or 'S' """ + auth = AUTH['RESELLER'] + sv = {False: 'S', True: 'A'} method = 'client' client = Client( URL[method], transport=self._transport, plugins=self._plugins) args = { - 'reseller_username': AUTH['USER'], - 'reseller_password': AUTH['PASS'], + 'reseller_username': auth['USER'], + 'reseller_password': auth['PASS'], 'taxpayer_id': rfc, 'status': sv[status], } @@ -524,15 +560,35 @@ class Finkok(object): return result def get_client(self, rfc): - """ + """Regresa el estatus del cliente + . Se requiere cuenta de reseller para usar este método + + Args: + rfc (str): El RFC del emisor + + Returns: + dict + 'message': None, + 'users': { + 'ResellerUser': [ + { + 'status': 'A', + 'counter': 0, + 'taxpayer_id': '', + 'credit': 0 + } + ] + } or None si no existe """ + auth = AUTH['RESELLER'] + method = 'client' client = Client( URL[method], transport=self._transport, plugins=self._plugins) args = { - 'reseller_username': AUTH['USER'], - 'reseller_password': AUTH['PASS'], + 'reseller_username': auth['USER'], + 'reseller_password': auth['PASS'], 'taxpayer_id': rfc, } @@ -548,15 +604,30 @@ class Finkok(object): return result def assign_client(self, rfc, credit): - """ + """Agregar credito a un emisor + Se requiere cuenta de reseller para usar este método + + Args: + rfc (str): El RFC del emisor, debe existir + credit (int): Cantidad de folios a agregar + + Returns: + dict + 'success': True or False, + 'credit': nuevo credito despues de agregar or None + 'message': + 'Success, added {credit} of credit to {RFC}' + 'RFC no encontrado' """ + auth = AUTH['RESELLER'] + method = 'client' client = Client( URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': AUTH['USER'], - 'password': AUTH['PASS'], + 'username': auth['USER'], + 'password': auth['PASS'], 'taxpayer_id': rfc, 'credit': credit, } diff --git a/source/app/models/main.py b/source/app/models/main.py index bc4ae98..cb6e36c 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -818,7 +818,11 @@ class Productos(BaseModel): @classmethod def next_key(cls): - value = Productos.select(fn.Max(Productos.id)).scalar() + value = (Productos + .select(fn.Max(Productos.id)) + .group_by(Productos.id) + .order_by(Productos.id) + .scalar()) if value is None: value = 1 else: @@ -1198,6 +1202,8 @@ class Facturas(BaseModel): inicio = (Facturas .select(fn.Max(Facturas.folio)) .where(Facturas.serie==serie) + .group_by(Facturas.folio) + .order_by(Facturas.folio) .scalar()) if inicio is None: inicio = inicio_serie From 14ccf4fe1fbb7c90d2a95bd01bb54f8169cbfd06 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sat, 28 Oct 2017 00:30:42 -0500 Subject: [PATCH 02/12] Filtro por fechas en UI --- source/app/models/db.py | 3 +++ source/app/models/main.py | 13 ++++++++++ source/static/js/controller/invoices.js | 15 ++++++++++++ source/static/js/controller/main.js | 32 ++++++++++++++++++++++++- source/static/js/ui/invoices.js | 31 +++++++++++++++++++++++- source/static/js/ui/main.js | 3 ++- 6 files changed, 94 insertions(+), 3 deletions(-) diff --git a/source/app/models/db.py b/source/app/models/db.py index f5a460d..76c54de 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -32,6 +32,9 @@ class StorageEngine(object): def send_email(self, values, session): return main.Facturas.send(values['id'], session['rfc']) + def _get_filteryears(self, values): + return main.Facturas.filter_years() + def _get_cert(self, values): return main.Certificado.get_data() diff --git a/source/app/models/main.py b/source/app/models/main.py index cb6e36c..d5764b5 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1015,6 +1015,18 @@ class Facturas(BaseModel): class Meta: order_by = ('fecha',) + @classmethod + def filter_years(cls): + data = [{'id': -1, 'value': 'Todos'}] + rows = (Facturas + .select(Facturas.fecha.year) + .group_by(Facturas.fecha.year) + .order_by(Facturas.fecha.year) + .scalar(as_tuple=True) + ) + data += [{'id': int(row), 'value': int(row)} for row in rows] + return tuple(data) + @classmethod def get_xml(cls, id): obj = Facturas.get(Facturas.id==id) @@ -1175,6 +1187,7 @@ class Facturas(BaseModel): @classmethod def get_(cls, values): + #~ print (values) rows = tuple(Facturas .select(Facturas.id, Facturas.serie, Facturas.folio, Facturas.uuid, Facturas.fecha, Facturas.tipo_comprobante, Facturas.estatus, diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index a79cdc2..2ff408f 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -732,3 +732,18 @@ function cmd_invoice_cancelar_click(){ } }) } + + +function filter_year_change(nv, ov){ + show(nv) +} + + +function filter_month_change(nv, ov){ + show(nv) +} + + +function filter_dates_change(range){ + show(range) +} diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index 4dd4c24..f8416be 100644 --- a/source/static/js/controller/main.js +++ b/source/static/js/controller/main.js @@ -46,6 +46,9 @@ var controllers = { $$('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) + $$('filter_year').attachEvent('onChange', filter_year_change) + $$('filter_month').attachEvent('onChange', filter_month_change) + $$('filter_dates').attachEvent('onChange', filter_dates_change) } } @@ -93,8 +96,14 @@ function get_products(){ function get_invoices(){ + var fy = $$('filter_year') + var fm = $$('filter_month') + + var y = fy.getValue() + var m = fm.getValue() + var grid = $$('grid_invoices') - webix.ajax().get('/invoices', {}, { + webix.ajax().get('/invoices', {'year': y, 'month': m}, { error: function(text, data, xhr) { webix.message({type: 'error', text: 'Error al consultar'}) }, @@ -117,6 +126,26 @@ function menu_user_click(id, e, node){ } +function current_dates(){ + var fy = $$('filter_year') + var fm = $$('filter_month') + var d = new Date() + + fy.blockEvent() + fm.blockEvent() + + fm.setValue(d.getMonth() + 1) + webix.ajax().sync().get('/values/filteryears', function(text, data){ + var values = data.json() + fy.getList().parse(values) + fy.setValue(d.getFullYear()) + }) + + fy.unblockEvent() + fm.unblockEvent() +} + + function multi_change(prevID, nextID){ //~ webix.message(nextID) if(nextID == 'app_partners'){ @@ -138,6 +167,7 @@ function multi_change(prevID, nextID){ if(nextID == 'app_invoices'){ active = $$('multi_invoices').getActiveId() if(active == 'invoices_home'){ + current_dates() get_invoices() } gi = $$('grid_invoices') diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 9e110c6..2a2fb46 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -20,6 +20,33 @@ var toolbar_invoices_util = [ ] +var months = [ + {id: -1, value: 'Todos'}, + {id: 1, value: 'Enero'}, + {id: 2, value: 'Febrero'}, + {id: 3, value: 'Marzo'}, + {id: 4, value: 'Abril'}, + {id: 5, value: 'Mayo'}, + {id: 6, value: 'Junio'}, + {id: 7, value: 'Julio'}, + {id: 8, value: 'Agosto'}, + {id: 9, value: 'Septiembre'}, + {id: 10, value: 'Octubre'}, + {id: 11, value: 'Noviembre'}, + {id: 12, value: 'Diciembre'}, +] + + +var toolbar_invoices_filter = [ + {view: 'richselect', id: 'filter_year', label: 'Año', labelAlign: 'right', + labelWidth: 50, width: 150, options: []}, + {view: 'richselect', id: 'filter_month', label: 'Mes', labelAlign: 'right', + labelWidth: 50, width: 200, options: months}, + {view: 'daterangepicker', id: 'filter_dates', label: 'Fechas', + labelAlign: 'right', width: 300}, +] + + function get_icon(tipo){ var node = "" return node @@ -34,7 +61,8 @@ var grid_invoices_cols = [ sort:"int", css: "cell_right"}, {id: "uuid", header: ["UUID", {content: "textFilter"}], adjust: "data", sort:"string", hidden:true}, - {id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort:"string"}, + {id: "fecha", header: ["Fecha y Hora"], + adjust: "data", sort:"date"}, {id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}], adjust: 'header', sort: 'string'}, {id: "estatus", header: ["Estatus", {content: "selectFilter"}], @@ -316,6 +344,7 @@ var multi_invoices = { {id: 'invoices_home', rows:[ {view: 'toolbar', elements: toolbar_invoices}, {view: 'toolbar', elements: toolbar_invoices_util}, + {view: 'toolbar', elements: toolbar_invoices_filter}, grid_invoices, ]}, {id: 'invoices_new', rows:[form_invoice, {}]} diff --git a/source/static/js/ui/main.js b/source/static/js/ui/main.js index f594323..9ba1099 100644 --- a/source/static/js/ui/main.js +++ b/source/static/js/ui/main.js @@ -62,7 +62,8 @@ var ui_main = { {view: 'label', label: 'Empresa Libre'}, {}, menu_user, - {view: 'button', type: 'icon', width: 45, css: 'app_button', icon: 'bell-o', badge: 1} + {view: 'button', type: 'icon', width: 45, css: 'app_button', + icon: 'bell-o', badge: 0} ] }, { From 2f62a2debef34bce9975f1e590f34a8f96f1777c Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sat, 28 Oct 2017 21:58:18 -0500 Subject: [PATCH 03/12] =?UTF-8?q?Filtro=20por=20a=C3=B1o=20y=20mes=20en=20?= =?UTF-8?q?facturas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/models/main.py | 18 ++++++++++++++- source/static/js/controller/invoices.js | 30 +++++++++++++++++++++++-- source/static/js/controller/main.js | 23 ------------------- source/static/js/ui/invoices.js | 4 ++-- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index d5764b5..c0b0291 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1187,11 +1187,27 @@ class Facturas(BaseModel): @classmethod def get_(cls, values): - #~ print (values) + print (values) + + if 'dates' in values: + print (values) + else: + if values['year'] == '-1': + fy = (Facturas.fecha.year > 0) + else: + fy = (Facturas.fecha.year == int(values['year'])) + if values['month'] == '-1': + fm = (Facturas.fecha.month > 0) + else: + fm = (Facturas.fecha.month == int(values['month'])) + + filters = (fy & fm) + rows = tuple(Facturas .select(Facturas.id, Facturas.serie, Facturas.folio, Facturas.uuid, Facturas.fecha, Facturas.tipo_comprobante, Facturas.estatus, Facturas.total_mn, Socios.nombre.alias('cliente')) + .where(filters) .join(Socios) .switch(Facturas).dicts() ) diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 2ff408f..f29bff5 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -734,13 +734,39 @@ function cmd_invoice_cancelar_click(){ } +function get_invoices(rango){ + if(rango == undefined){ + var fy = $$('filter_year') + var fm = $$('filter_month') + + var y = fy.getValue() + var m = fm.getValue() + rango = {'year': y, 'month': m} + } + + var grid = $$('grid_invoices') + webix.ajax().get('/invoices', rango, { + error: function(text, data, xhr) { + webix.message({type: 'error', text: 'Error al consultar'}) + }, + success: function(text, data, xhr) { + var values = data.json(); + grid.clearAll(); + if (values.ok){ + grid.parse(values.rows, 'json'); + }; + } + }); +} + + function filter_year_change(nv, ov){ - show(nv) + get_invoices() } function filter_month_change(nv, ov){ - show(nv) + get_invoices() } diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index f8416be..3647b85 100644 --- a/source/static/js/controller/main.js +++ b/source/static/js/controller/main.js @@ -95,29 +95,6 @@ function get_products(){ } -function get_invoices(){ - var fy = $$('filter_year') - var fm = $$('filter_month') - - var y = fy.getValue() - var m = fm.getValue() - - var grid = $$('grid_invoices') - webix.ajax().get('/invoices', {'year': y, 'month': m}, { - error: function(text, data, xhr) { - webix.message({type: 'error', text: 'Error al consultar'}) - }, - success: function(text, data, xhr) { - var values = data.json(); - grid.clearAll(); - if (values.ok){ - grid.parse(values.rows, 'json'); - }; - } - }); -} - - function menu_user_click(id, e, node){ if (id == 1){ window.location = '/logout'; diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 2a2fb46..6092c15 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -43,7 +43,7 @@ var toolbar_invoices_filter = [ {view: 'richselect', id: 'filter_month', label: 'Mes', labelAlign: 'right', labelWidth: 50, width: 200, options: months}, {view: 'daterangepicker', id: 'filter_dates', label: 'Fechas', - labelAlign: 'right', width: 300}, + labelAlign: 'right', width: 300, hidden: true}, ] @@ -62,7 +62,7 @@ var grid_invoices_cols = [ {id: "uuid", header: ["UUID", {content: "textFilter"}], adjust: "data", sort:"string", hidden:true}, {id: "fecha", header: ["Fecha y Hora"], - adjust: "data", sort:"date"}, + adjust: "data", sort: "date"}, {id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}], adjust: 'header', sort: 'string'}, {id: "estatus", header: ["Estatus", {content: "selectFilter"}], From 63c7b4e458511632bdd8ed8227cf4653b7905c83 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sat, 28 Oct 2017 22:21:39 -0500 Subject: [PATCH 04/12] Filtro por rango de fechas en facturas --- source/app/controllers/util.py | 7 +++++++ source/app/models/main.py | 12 ++++++------ source/static/js/controller/invoices.js | 4 +++- source/static/js/ui/invoices.js | 2 +- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 2b4fea4..0d72190 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1036,6 +1036,13 @@ def get_path_temp(): return tempfile.mkstemp()[1] +def get_date(value, next_day=False): + d = parser.parse(value) + if next_day: + return d + datetime.timedelta(days=1) + return d + + class ImportFacturaLibre(object): def __init__(self, path): diff --git a/source/app/models/main.py b/source/app/models/main.py index c0b0291..370269e 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1187,10 +1187,11 @@ class Facturas(BaseModel): @classmethod def get_(cls, values): - print (values) - - if 'dates' in values: - print (values) + if 'start' in values: + filters = Facturas.fecha.between( + util.get_date(values['start']), + util.get_date(values['end'], True) + ) else: if values['year'] == '-1': fy = (Facturas.fecha.year > 0) @@ -1200,8 +1201,7 @@ class Facturas(BaseModel): fm = (Facturas.fecha.month > 0) else: fm = (Facturas.fecha.month == int(values['month'])) - - filters = (fy & fm) + filters = (fy & fm) rows = tuple(Facturas .select(Facturas.id, Facturas.serie, Facturas.folio, Facturas.uuid, diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index f29bff5..0fdba87 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -771,5 +771,7 @@ function filter_month_change(nv, ov){ function filter_dates_change(range){ - show(range) + if(range.start != null && range.end != null){ + get_invoices(range) + } } diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 6092c15..687a644 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -43,7 +43,7 @@ var toolbar_invoices_filter = [ {view: 'richselect', id: 'filter_month', label: 'Mes', labelAlign: 'right', labelWidth: 50, width: 200, options: months}, {view: 'daterangepicker', id: 'filter_dates', label: 'Fechas', - labelAlign: 'right', width: 300, hidden: true}, + labelAlign: 'right', width: 300}, ] From f958227f56fca67c413d9ebf75d5c8733bd1aa47 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sat, 28 Oct 2017 23:37:08 -0500 Subject: [PATCH 05/12] Obtener estatus SAT --- source/app/controllers/pac.py | 4 ++-- source/app/controllers/util.py | 5 +++++ source/app/models/db.py | 3 +++ source/app/models/main.py | 8 ++++++++ source/static/js/controller/invoices.js | 27 +++++++++++++++++++++++-- source/static/js/controller/main.js | 1 + source/static/js/ui/invoices.js | 2 ++ 7 files changed, 46 insertions(+), 4 deletions(-) diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py index d123861..89c37a3 100644 --- a/source/app/controllers/pac.py +++ b/source/app/controllers/pac.py @@ -648,7 +648,7 @@ def _get_data_sat(path): if os.path.isfile(path): tree = etree.parse(path).getroot() else: - tree = etree.fromstring(path) + tree = etree.fromstring(path.encode()) data = {} emisor = escape( @@ -673,7 +673,7 @@ def _get_data_sat(path): def get_status_sat(xml): data = _get_data_sat(xml) if not data: - return + return 'XML inválido' URL = 'https://consultaqr.facturaelectronica.sat.gob.mx/ConsultaCFDIService.svc?wsdl' client = Client(URL, transport=Transport(cache=SqliteCache())) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 0d72190..275a4f6 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -473,6 +473,11 @@ def timbra_xml(xml, auth): return result +def get_sat(xml): + from .pac import get_status_sat + return get_status_sat(xml) + + class LIBO(object): HOST = 'localhost' PORT = '8100' diff --git a/source/app/models/db.py b/source/app/models/db.py index 76c54de..9cf565c 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -32,6 +32,9 @@ class StorageEngine(object): def send_email(self, values, session): return main.Facturas.send(values['id'], session['rfc']) + def _get_statussat(self, values): + return main.Facturas.get_status_sat(values['id']) + def _get_filteryears(self, values): return main.Facturas.filter_years() diff --git a/source/app/models/main.py b/source/app/models/main.py index 370269e..654ac54 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1004,6 +1004,7 @@ class Facturas(BaseModel): xml = TextField(default='') uuid = UUIDField(null=True) estatus = TextField(default='Guardada') + estatus_sat = TextField(default='Vigente') regimen_fiscal = TextField(default='') notas = TextField(default='') pagada = BooleanField(default=False) @@ -1472,6 +1473,13 @@ class Facturas(BaseModel): } return util.make_xml(data, certificado) + @classmethod + def get_status_sat(cls, id): + obj = Facturas.get(Facturas.id == id) + obj.estatus_sat = util.get_sat(obj.xml) + obj.save() + return obj.estatus_sat + @classmethod def timbrar(cls, id): obj = Facturas.get(Facturas.id == id) diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 0fdba87..29dd002 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -27,9 +27,7 @@ function get_series(){ function get_forma_pago(){ webix.ajax().get('/values/formapago', {key: true}, function(text, data){ var values = data.json() - //~ pre = values[0] $$('lst_forma_pago').getList().parse(values) - //~ $$('lst_forma_pago').setValue(pre.id) }) } @@ -775,3 +773,28 @@ function filter_dates_change(range){ get_invoices(range) } } + + +function cmd_invoice_sat_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 consultar \ + el estatus en el SAT de facturas timbradas') + return + } + + webix.ajax().get('/values/statussat', {id: row.id}, function(text, data){ + var values = data.json() + show(values) + }) + +} diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index 3647b85..cea3ed5 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_sat').attachEvent('onItemClick', cmd_invoice_sat_click) $$('cmd_invoice_cancelar').attachEvent('onItemClick', cmd_invoice_cancelar_click) $$('grid_invoices').attachEvent('onItemClick', grid_invoices_click) $$('filter_year').attachEvent('onChange', filter_year_change) diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 687a644..7844dcd 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -14,6 +14,8 @@ 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_sat', label: 'SAT', + type: 'iconButton', autowidth: true, icon: 'check-circle'}, {}, {view: 'button', id: 'cmd_invoice_cancelar', label: 'Cancelar', type: 'iconButton', autowidth: true, icon: 'ban'}, From 9e50033b9f70a6b7198b868da3df5d8508d3b025 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 29 Oct 2017 16:53:10 -0600 Subject: [PATCH 06/12] Cancelar facturas con xml firmado --- source/app/controllers/pac.py | 10 +++-- source/app/controllers/util.py | 49 ++++++++++++++++++++++++- source/app/models/db.py | 3 ++ source/app/models/main.py | 32 +++++++++++++--- source/app/settings.py | 4 ++ source/static/js/controller/invoices.js | 19 +++++++++- source/templates/cancel_template.xml | 28 ++++++++++++++ 7 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 source/templates/cancel_template.xml diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py index 89c37a3..b99fb87 100644 --- a/source/app/controllers/pac.py +++ b/source/app/controllers/pac.py @@ -371,7 +371,7 @@ class Finkok(object): if os.path.isfile(file_xml): root = etree.parse(file_xml).getroot() else: - root = etree.fromstring(file_xml) + root = etree.fromstring(file_xml.encode()) xml = etree.tostring(root) @@ -385,8 +385,12 @@ class Finkok(object): 'store_pending': True, } - result = client.service.cancel_signature(**args) - return result + try: + result = client.service.cancel_signature(**args) + return result + except Fault as e: + self.error = str(e) + return '' def get_acuse(self, rfc, uuids, type_acuse='C'): for u in uuids: diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 275a4f6..318fe6f 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -33,7 +33,8 @@ 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_TEMPLATES, PATH_MEDIA, PRE + PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PATH_MEDIA, PRE, \ + PATH_XMLSEC, TEMPLATE_CANCEL #~ def _get_hash(password): @@ -1048,6 +1049,52 @@ def get_date(value, next_day=False): return d +def cancel_cfdi(uuid, pk12, rfc, auth): + from .pac import Finkok as PAC + + template = read_file(TEMPLATE_CANCEL, 'r') + data = { + 'rfc': rfc, + 'fecha': datetime.datetime.now().isoformat()[:19], + 'uuid': str(uuid).upper(), + } + template = template.format(**data) + + data = { + 'xmlsec': PATH_XMLSEC, + 'pk12': _save_temp(pk12), + 'pass': _get_md5(rfc), + 'template': _save_temp(template, 'w'), + } + args = '"{xmlsec}" --sign --pkcs12 "{pk12}" --pwd {pass} ' \ + '"{template}"'.format(**data) + xml_sign = _call(args) + + if DEBUG: + auth = {} + else: + if not auth: + msg = 'Sin datos para cancelar' + result = {'ok': False, 'error': msg} + return result + + msg = 'Factura cancelada correctamente' + data = {'ok': True, 'msg': msg, 'row': {'estatus': 'Cancelada'}} + pac = PAC(auth) + result = pac.cancel_signature(xml_sign) + if result: + codes = {None: '', + 'Could not get UUID Text': 'UUID no encontrado'} + if not result['CodEstatus'] is None: + data['ok'] = False + data['msg'] = codes.get(result['CodEstatus'], result['CodEstatus']) + else: + data['ok'] = False + data['msg'] = pac.error + + return data, result + + class ImportFacturaLibre(object): def __init__(self, path): diff --git a/source/app/models/db.py b/source/app/models/db.py index 9cf565c..ba042c8 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -32,6 +32,9 @@ class StorageEngine(object): def send_email(self, values, session): return main.Facturas.send(values['id'], session['rfc']) + def _get_cancelinvoice(self, values): + return main.Facturas.cancel(values['id']) + def _get_statussat(self, values): return main.Facturas.get_status_sat(values['id']) diff --git a/source/app/models/main.py b/source/app/models/main.py index 654ac54..cb8e79f 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -723,7 +723,7 @@ class Socios(BaseModel): .where((Socios.id==id) & (Socios.es_cliente==True)) .dicts() ) - print (id, row) + #~ print (id, row) if len(row): return {'ok': True, 'row': row[0]} return {'ok': False} @@ -1009,6 +1009,8 @@ class Facturas(BaseModel): notas = TextField(default='') pagada = BooleanField(default=False) cancelada = BooleanField(default=False) + fecha_cancelacion = DateTimeField(null=True) + acuse = TextField(default='') donativo = BooleanField(default=False) tipo_relacion = TextField(default='') error = TextField(default='') @@ -1016,6 +1018,25 @@ class Facturas(BaseModel): class Meta: order_by = ('fecha',) + @classmethod + def cancel(cls, id): + msg = 'Factura cancelada correctamente' + auth = Emisor.get_auth() + certificado = Certificado.select()[0] + obj = Facturas.get(Facturas.id==id) + data, result = util.cancel_cfdi( + obj.uuid, certificado.p12, certificado.rfc, auth) + if data['ok']: + obj.estatus = 'Cancelada' + obj.error = '' + obj.cancelada = True + obj.fecha_cancelacion = result['Fecha'] + obj.acuse = result['Acuse'] + else: + obj.error = data['msg'] + obj.save() + return data + @classmethod def filter_years(cls): data = [{'id': -1, 'value': 'Todos'}] @@ -1025,7 +1046,8 @@ class Facturas(BaseModel): .order_by(Facturas.fecha.year) .scalar(as_tuple=True) ) - data += [{'id': int(row), 'value': int(row)} for row in rows] + if not rows is None: + data += [{'id': int(row), 'value': int(row)} for row in rows] return tuple(data) @classmethod @@ -1489,7 +1511,7 @@ class Facturas(BaseModel): auth = Emisor.get_auth() - error = False + #~ error = False msg = 'Factura timbrada correctamente' result = util.timbra_xml(obj.xml, auth) if result['ok']: @@ -1498,10 +1520,10 @@ class Facturas(BaseModel): obj.fecha_timbrado = result['fecha'] obj.estatus = 'Timbrada' obj.error = '' - obj.save() + #~ obj.save() row = {'uuid': obj.uuid, 'estatus': 'Timbrada'} else: - error = True + #~ error = True msg = result['error'] obj.estatus = 'Error' obj.error = msg diff --git a/source/app/settings.py b/source/app/settings.py index 1776135..ad1d0cf 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -25,6 +25,8 @@ 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)) +CT = 'cancel_template.xml' +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')) @@ -69,9 +71,11 @@ log = Logger(LOG_NAME) PATH_XSLTPROC = 'xsltproc' PATH_OPENSSL = 'openssl' +PATH_XMLSEC = 'xmlsec1' if 'win' in sys.platform: PATH_XSLTPROC = os.path.join(PATH_BIN, 'xsltproc.exe') PATH_OPENSSL = os.path.join(PATH_BIN, 'openssl.exe') + PATH_XMLSEC = os.path.join(PATH_BIN, 'xmlsec.exe') PRE = { diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 29dd002..91b6ae2 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -695,7 +695,19 @@ function grid_invoices_click(id, e, node){ function send_cancel(id){ - show(id) + webix.ajax().get('/values/cancelinvoice', {id: id}, function(text, data){ + var values = data.json() + if(values.ok){ + msg_sucess(values.msg) + gi.updateItem(id, values.row) + }else{ + webix.alert({ + title: 'Error al Cancelar', + text: values.msg, + type: 'alert-error' + }) + } + }) } function cmd_invoice_cancelar_click(){ @@ -715,6 +727,11 @@ function cmd_invoice_cancelar_click(){ return } + if(row.estatus == 'Cancelada'){ + msg_error('La factura ya esta cancelada') + return + } + msg = '¿Estás seguro de enviar a cancelar esta factura?

\ ESTA ACCIÓN NO SE PUEDE DESHACER' webix.confirm({ diff --git a/source/templates/cancel_template.xml b/source/templates/cancel_template.xml new file mode 100644 index 0000000..6b58286 --- /dev/null +++ b/source/templates/cancel_template.xml @@ -0,0 +1,28 @@ + + + + {uuid} + + + + + + + + + + + + + + + + + + + + + + + + From 8a06e3f4bbd885971c2e764d6ae0778654523f3f Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 29 Oct 2017 22:50:47 -0600 Subject: [PATCH 07/12] Fix - Importes grandes en campos Decimales --- README.md | 5 +++- source/app/models/main.py | 58 +++++++++++++++++++++++++-------------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 49ffdad..fe9bc4f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Este proyecto está en continuo desarrollo, contratar un esquema de soporte, nos ayuda a continuar su desarrollo. Ponte en contacto con nosotros para -contratar. +contratar: administracion@empresalibre.net ### Requerimientos: @@ -16,6 +16,9 @@ contratar. * Servidor web, recomendado Nginx * uwsgi * python3 +* xsltproc +* openssl +* xmlsec Debería de funcionar con cualquier combinación servidor-wsgi que soporte aplicaciones Python. diff --git a/source/app/models/main.py b/source/app/models/main.py index cb8e79f..192ff2c 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -666,8 +666,10 @@ 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) + saldo_cliente = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + saldo_proveedor = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) web = TextField(default='') correo_facturas = TextField(default='') forma_pago = ForeignKeyField(SATFormaPago, null=True) @@ -801,12 +803,17 @@ class Productos(BaseModel): clave_sat = TextField(default='') descripcion = TextField(index=True) unidad = ForeignKeyField(SATUnidades) - valor_unitario = DecimalField(default=0.0, decimal_places=6, auto_round=True) - ultimo_costo = DecimalField(default=0.0, decimal_places=6, auto_round=True) - descuento = DecimalField(default=0.0, decimal_places=6, auto_round=True) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + ultimo_costo = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) inventario = BooleanField(default=False) - existencia = DoubleField(default=0.0) - minimo = DoubleField(default=0.0) + existencia = DecimalField(default=0.0, max_digits=18, decimal_places=2, + auto_round=True) + minimo = DecimalField(default=0.0, max_digits=18, decimal_places=2, + auto_round=True) codigo_barras = TextField(default='') cuenta_predial = TextField(default='') es_activo = BooleanField(default=True) @@ -986,21 +993,25 @@ class Facturas(BaseModel): fecha_timbrado = DateTimeField(null=True) forma_pago = TextField(default='') condiciones_pago = TextField(default='') - subtotal = DecimalField(default=0.0, decimal_places=6, auto_round=True) - descuento = DecimalField(default=0.0, decimal_places=6, auto_round=True) + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) moneda = TextField(default='MXN') tipo_cambio = DecimalField(default=1.0, decimal_places=6, auto_round=True) - total = DecimalField(default=0.0, decimal_places=6, auto_round=True) - total_mn = DecimalField(default=0.0, decimal_places=6, auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) tipo_comprobante = TextField(default='I') metodo_pago = TextField(default='PUE') lugar_expedicion = TextField(default='') confirmacion = TextField(default='') uso_cfdi = TextField(default='') total_retenciones = DecimalField( - decimal_places=6, auto_round=True, null=True) + max_digits=20, decimal_places=6, auto_round=True, null=True) total_trasladados = DecimalField( - decimal_places=6, auto_round=True, null=True) + max_digits=20, decimal_places=6, auto_round=True, null=True) xml = TextField(default='') uuid = UUIDField(null=True) estatus = TextField(default='Guardada') @@ -1546,11 +1557,16 @@ class FacturasRelacionadas(BaseModel): class FacturasDetalle(BaseModel): factura = ForeignKeyField(Facturas) producto = ForeignKeyField(Productos, null=True) - cantidad = DecimalField(default=0.0, decimal_places=6, auto_round=True) - valor_unitario = DecimalField(default=0.0, decimal_places=6, auto_round=True) - descuento = DecimalField(default=0.0, decimal_places=6, auto_round=True) - precio_final = DecimalField(default=0.0, decimal_places=6, auto_round=True) - importe = DecimalField(default=0.0, decimal_places=6, auto_round=True) + cantidad = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + precio_final = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) descripcion = TextField(default='') unidad = TextField(default='') clave = TextField(default='') @@ -1572,8 +1588,10 @@ class FacturasDetalle(BaseModel): class FacturasImpuestos(BaseModel): factura = ForeignKeyField(Facturas) impuesto = ForeignKeyField(SATImpuestos) - base = DecimalField(default=0.0, decimal_places=6, auto_round=True) - importe = DecimalField(default=0.0, decimal_places=6, auto_round=True) + base = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) class Meta: order_by = ('factura',) From 4f26f820cc76051c737a965e01fd8b87290c7d0f Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 30 Oct 2017 00:03:02 -0600 Subject: [PATCH 08/12] UI - Prefacturas --- source/app/models/main.py | 90 ++++++++++++++++++++++++- source/static/js/controller/invoices.js | 16 +++++ source/static/js/controller/main.js | 1 + source/static/js/ui/invoices.js | 77 ++++++++++++++++++--- 4 files changed, 173 insertions(+), 11 deletions(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index 192ff2c..29de88b 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1257,6 +1257,8 @@ class Facturas(BaseModel): q.execute() q = FacturasImpuestos.delete().where(FacturasImpuestos.factura==obj) q.execute() + q = FacturasRelacionadas.delete().where(FacturasRelacionadas.factura==obj) + q.execute() return bool(obj.delete_instance()) def _get_folio(self, serie): @@ -1543,15 +1545,54 @@ class Facturas(BaseModel): return {'ok': result['ok'], 'msg': msg, 'row': row} +class PreFacturas(BaseModel): + cliente = ForeignKeyField(Socios) + serie = TextField(default='PRE') + folio = IntegerField(default=0) + fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) + forma_pago = TextField(default='') + condiciones_pago = TextField(default='') + subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + moneda = TextField(default='MXN') + tipo_cambio = DecimalField(default=1.0, decimal_places=6, auto_round=True) + total = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + tipo_comprobante = TextField(default='I') + metodo_pago = TextField(default='PUE') + lugar_expedicion = TextField(default='') + uso_cfdi = TextField(default='') + total_retenciones = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + total_trasladados = DecimalField( + max_digits=20, decimal_places=6, auto_round=True, null=True) + estatus = TextField(default='Generada') + regimen_fiscal = TextField(default='') + notas = TextField(default='') + donativo = BooleanField(default=False) + tipo_relacion = TextField(default='') + + class Meta: + order_by = ('fecha',) + + 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 PreFacturasRelacionadas(BaseModel): + factura = ForeignKeyField(PreFacturas, related_name='original') + factura_origen = ForeignKeyField(PreFacturas, related_name='relacion') + + class Meta: + order_by = ('factura',) class FacturasDetalle(BaseModel): @@ -1585,6 +1626,32 @@ class FacturasDetalle(BaseModel): order_by = ('factura',) +class PreFacturasDetalle(BaseModel): + factura = ForeignKeyField(PreFacturas) + producto = ForeignKeyField(Productos, null=True) + cantidad = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + precio_final = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + aduana = TextField(default='') + pedimento = TextField(default='') + fecha_pedimento = DateField(null=True) + alumno = TextField(default='') + curp = TextField(default='') + nivel = TextField(default='') + autorizacion = TextField(default='') + cuenta_predial = TextField(default='') + + class Meta: + order_by = ('factura',) + + class FacturasImpuestos(BaseModel): factura = ForeignKeyField(Facturas) impuesto = ForeignKeyField(SATImpuestos) @@ -1600,6 +1667,21 @@ class FacturasImpuestos(BaseModel): ) +class PreFacturasImpuestos(BaseModel): + factura = ForeignKeyField(PreFacturas) + impuesto = ForeignKeyField(SATImpuestos) + base = DecimalField(default=0.0, max_digits=20, decimal_places=6, + auto_round=True) + importe = DecimalField(default=0.0, max_digits=18, decimal_places=6, + auto_round=True) + + class Meta: + order_by = ('factura',) + indexes = ( + (('factura', 'impuesto'), True), + ) + + def authenticate(args): respuesta = {'login': False, 'msg': 'No Autorizado', 'user': ''} values = util.get_con(args['rfc']) @@ -1700,6 +1782,8 @@ def _crear_tablas(rfc): tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion, Emisor, Facturas, FacturasDetalle, FacturasImpuestos, Folios, FacturasRelacionadas, Productos, + PreFacturas, PreFacturasDetalle, PreFacturasImpuestos, + PreFacturasRelacionadas, SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes, SATTipoRelacion, SATUnidades, SATUsoCfdi, Socios, Tags, Usuarios, diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 91b6ae2..b858d12 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -815,3 +815,19 @@ function cmd_invoice_sat_click(){ }) } + + +function cmd_prefactura_click(){ + var form = this.getFormView() + + if(!form.validate()) { + msg_error('Valores inválidos') + return + } + + var values = form.getValues() + if(!validate_invoice(values)){ + return + } + show('PreFactura') +} diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index cea3ed5..b17e8ee 100644 --- a/source/static/js/controller/main.js +++ b/source/static/js/controller/main.js @@ -50,6 +50,7 @@ var controllers = { $$('filter_year').attachEvent('onChange', filter_year_change) $$('filter_month').attachEvent('onChange', filter_month_change) $$('filter_dates').attachEvent('onChange', filter_dates_change) + $$('cmd_prefactura').attachEvent('onItemClick', cmd_prefactura_click) } } diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 7844dcd..bfa028f 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -258,6 +258,7 @@ var body_regimen_fiscal = { var controls_generate = [ + {minHeight: 15, maxHeight: 15}, {cols: [ {rows:[ {view: 'fieldset', label: 'Buscar Cliente', body: {rows: [ {cols: [ @@ -302,26 +303,86 @@ var controls_generate = [ {view: 'label', label: 'Detalle', height: 30, align: 'left'}, grid_details, {minHeight: 15, maxHeight: 15}, - {cols: [{}, grid_totals]} + {cols: [{}, grid_totals]}, + {minHeight: 15, maxHeight: 15}, + {margin: 20, cols: [{}, + {view: "button", id: "cmd_timbrar", label: "Timbrar", + type: "form", autowidth: true, align:"center"}, + {view: "button", id: 'cmd_prefactura', label: "PreFactura", + type: "form", autowidth: true, align:"center"}, + {}] + } +] + + +var toolbar_preinvoices = [ + {view: 'button', id: 'cmd_facturar_preinvoice', label: 'Facturar', + type: 'iconButton', autowidth: true, icon: 'pencil'}, + {}, + {view: "button", id: "cmd_delete_preinvoice", label: "Eliminar", + type: "iconButton", autowidth: true, icon: "minus"}, +] + + +var toolbar_prefilter = [ + {view: 'richselect', id: 'prefilter_year', label: 'Año', labelAlign: 'right', + labelWidth: 50, width: 150, options: []}, + {view: 'richselect', id: 'prefilter_month', label: 'Mes', labelAlign: 'right', + labelWidth: 50, width: 200, options: months}, +] + + +var grid_preinvoices_cols = [ + {id: "id", header:"ID", hidden:true}, + {id: "folio", header: ["Folio", {content: "numberFilter"}], adjust: "data", + sort:"int", css: "cell_right"}, + {id: "fecha", header: ["Fecha y Hora"], + adjust: "data", sort: "date"}, + {id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}], + adjust: 'header', sort: 'string'}, + {id: 'total_mn', header: ['Total M.N.'], width: 150, + sort: 'int', format: webix.i18n.priceFormat, css: 'right'}, + {id: "cliente", header: ["Razón Social", {content: "selectFilter"}], + fillspace:true, sort:"string"}, + {id: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')}, + {id: 'email', header: '', adjust: 'data', template: get_icon('email')} +] + + +var grid_preinvoices = { + view: 'datatable', + id: 'grid_preinvoices', + select: 'row', + adjust: true, + footer: true, + resizeColumn: true, + headermenu: true, + columns: grid_preinvoices_cols, +} + + +var controls_prefactura = [ + {view: 'toolbar', elements: toolbar_preinvoices}, + {view: 'toolbar', elements: toolbar_prefilter}, + grid_preinvoices, ] var controls_invoices = [ { - view: "tabview", - tabbar: {options: ["Generar"]}, animate: true, + view: 'tabview', + tabbar: {options: ['Generar', 'PreFacturas']}, animate: true, cells: [ - {id: "Generar", rows: controls_generate}, + {id: 'Generar', rows: controls_generate}, + {id: 'PreFacturas', rows: controls_prefactura}, ] }, {rows: [ {template:"", type: "section" }, {margin: 10, cols: [{}, - {view: "button", id: "cmd_timbrar", label: "Timbrar", - type: "form", autowidth: true, align:"center"}, {view: 'button', id: 'cmd_close_invoice', label: 'Cancelar', - type: 'danger', autowidth: true, align: 'center'}, - {}] + type: 'danger', autowidth: true, align: 'center'} + ] }, ]} ] From 9eb5b63dc825f7de04bebd207946d7ed387c6659 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 30 Oct 2017 13:57:02 -0600 Subject: [PATCH 09/12] Fix - Guardar certificados --- source/app/controllers/util.py | 44 +++++++++++++++++---------------- source/app/main.ini | 2 ++ source/app/main_debug.ini | 1 - source/app/models/db.py | 4 +-- source/app/models/main.py | 45 +++++++++++++++++++++------------- 5 files changed, 55 insertions(+), 41 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 318fe6f..9f5d07e 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -53,7 +53,7 @@ def _get_md5(data): return hashlib.md5(data.encode()).hexdigest() -def _save_temp(data, modo='wb'): +def save_temp(data, modo='wb'): path = tempfile.mkstemp()[1] with open(path, modo) as f: f.write(data) @@ -277,21 +277,22 @@ def to_slug(string): class Certificado(object): - def __init__(self, key, cer): - self._key = key - self._cer = cer + def __init__(self, paths): + self._path_key = paths['path_key'] + self._path_cer = paths['path_cer'] self._modulus = '' - self._save_files() + #~ self._save_files() self.error = '' - def _save_files(self): - try: - self._path_key = _save_temp(self._key) - self._path_cer = _save_temp(self._cer) - except: - self._path_key = '' - self._path_cer = '' - return + #~ def _save_files(self): + #~ try: + #~ self._path_key = _save_temp(bytes(self._key)) + #~ self._path_cer = _save_temp(bytes(self._cer)) + #~ except Exception as e: + #~ log.error(e) + #~ self._path_key = '' + #~ self._path_cer = '' + #~ return def _kill(self, path): try: @@ -342,7 +343,7 @@ class Certificado(object): hasta = parser.parse(dates[1].split('=')[1]) self._modulus = _call(args.format(self._path_cer, 'modulus')) - data['cer'] = self._cer + data['cer'] = read_file(self._path_cer) data['cer_tmp'] = None data['cer_pem'] = cer_pem data['cer_txt'] = cer_txt.replace('\n', '') @@ -366,7 +367,8 @@ class Certificado(object): 'pass:"{}" -out "{}"' _call(args.format(tmp_cer, tmp_key, rfc, hashlib.md5(rfc.encode()).hexdigest(), tmp_p12)) - data = open(tmp_p12, 'rb').read() + #~ data = open(tmp_p12, 'rb').read() + data = read_file(tmp_p12) self._kill(tmp_cer) self._kill(tmp_key) @@ -397,7 +399,7 @@ class Certificado(object): self._path_key, password, _get_md5(rfc)) key_enc = _call(args) - data['key'] = self._key + data['key'] = read_file(self._path_key) data['key_tmp'] = None data['key_enc'] = key_enc data['p12'] = self._get_p12(password, rfc) @@ -405,7 +407,7 @@ class Certificado(object): def validate(self, password, rfc): if not self._path_key or not self._path_cer: - self.error = 'Error al cargar el certificado' + self.error = 'Error en las rutas temporales del certificado' return {} data = self._get_info_cer(rfc) @@ -433,9 +435,9 @@ def make_xml(data, certificado): data = { 'xsltproc': PATH_XSLTPROC, 'xslt': _join(PATH_XSLT, 'cadena.xslt'), - 'xml': _save_temp(xml, 'w'), + 'xml': save_temp(xml, 'w'), 'openssl': PATH_OPENSSL, - 'key': _save_temp(certificado.key_enc, 'w'), + 'key': save_temp(certificado.key_enc, 'w'), 'pass': _get_md5(certificado.rfc) } args = '"{xsltproc}" "{xslt}" "{xml}" | ' \ @@ -1062,9 +1064,9 @@ def cancel_cfdi(uuid, pk12, rfc, auth): data = { 'xmlsec': PATH_XMLSEC, - 'pk12': _save_temp(pk12), + 'pk12': save_temp(pk12), 'pass': _get_md5(rfc), - 'template': _save_temp(template, 'w'), + 'template': save_temp(template, 'w'), } args = '"{xmlsec}" --sign --pkcs12 "{pk12}" --pwd {pass} ' \ '"{template}"'.format(**data) diff --git a/source/app/main.ini b/source/app/main.ini index 6041275..9c5472f 100644 --- a/source/app/main.ini +++ b/source/app/main.ini @@ -2,6 +2,7 @@ socket = 127.0.0.1:3033 uid = nginx gid = nginx +#~ Establece una ruta accesible para nginx chdir = /srv/app/empresa-libre/app wsgi-file = main.py callable = app @@ -10,4 +11,5 @@ processes = 4 threads = 4 thunder-lock = true #~ stats = 127.0.0.1:9191 +#~ Establece una ruta accesible para nginx logger = file:/srv/log/empresalibre-uwsgi.log diff --git a/source/app/main_debug.ini b/source/app/main_debug.ini index 89cb94f..065dcfc 100644 --- a/source/app/main_debug.ini +++ b/source/app/main_debug.ini @@ -1,6 +1,5 @@ [uwsgi] http = 127.0.0.1:8000 -#~ http = 37.228.132.181:9000 wsgi-file = main.py callable = app master = true diff --git a/source/app/models/db.py b/source/app/models/db.py index ba042c8..1b09e1f 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -20,8 +20,8 @@ class StorageEngine(object): def add_config(self, values): return main.Configuracion.add(values) - def add_cert(self, file_object): - return main.Certificado.add(file_object) + def add_cert(self, file_obj): + return main.Certificado.add(file_obj) def validate_cert(self, values, session): return main.Certificado.validate(values, session) diff --git a/source/app/models/main.py b/source/app/models/main.py index 29de88b..93274c7 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -64,6 +64,9 @@ class Configuracion(BaseModel): clave = TextField(unique=True) valor = TextField(default='') + def __str__(self): + return '{} = {}'.format(self.clave, self.valor) + @classmethod def get_(cls, keys): if keys['fields'] == 'correo': @@ -74,7 +77,14 @@ class Configuracion(BaseModel): .select() .where(Configuracion.clave.in_(fields)) ) - values = {r.clave: r.valor for r in data} + elif keys['fields'] == 'path_cer': + fields = ('path_key', 'path_cer') + data = (Configuracion + .select() + .where(Configuracion.clave.in_(fields)) + ) + + values = {r.clave: r.valor for r in data} return values @classmethod @@ -316,28 +326,26 @@ class Certificado(BaseModel): return row def get_(cls): - if Certificado.select().count(): - obj = Certificado.select()[0] - else: - obj = Certificado() - return obj + return Certificado.select()[0] @classmethod - def add(cls, file_object): - obj = cls.get_(cls) - if file_object.filename.endswith('key'): - obj.key_tmp = file_object.file.read() - elif file_object.filename.endswith('cer'): - obj.cer_tmp = file_object.file.read() - obj.save() + def add(cls, file_obj): + if file_obj.filename.endswith('key'): + path_key = util.save_temp(file_obj.file.read()) + Configuracion.add({'path_key': path_key}) + elif file_obj.filename.endswith('cer'): + path_cer = util.save_temp(file_obj.file.read()) + Configuracion.add({'path_cer': path_cer}) return {'status': 'server'} @classmethod def validate(cls, values, session): row = {} result = False + obj = cls.get_(cls) - cert = util.Certificado(obj.key_tmp, obj.cer_tmp) + paths = Configuracion.get_({'fields': 'path_cer'}) + cert = util.Certificado(paths) data = cert.validate(values['contra'], session['rfc']) if data: msg = 'Certificado guardado correctamente' @@ -352,9 +360,12 @@ class Certificado(BaseModel): } else: msg = cert.error - obj.key_tmp = None - obj.cer_tmp = None - obj.save() + #~ obj.key_tmp = None + #~ obj.cer_tmp = None + #~ obj.save() + + Configuracion.add({'path_key': ''}) + Configuracion.add({'path_cer': ''}) return {'ok': result, 'msg': msg, 'data': row} From 5b4183133ec6d514ade017f85fa15a1bfd2e67fc Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 30 Oct 2017 15:11:10 -0600 Subject: [PATCH 10/12] Fix - Certificado al inicar valores --- source/app/controllers/util.py | 2 -- source/app/models/main.py | 15 ++++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 9f5d07e..8180e79 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -344,7 +344,6 @@ class Certificado(object): self._modulus = _call(args.format(self._path_cer, 'modulus')) data['cer'] = read_file(self._path_cer) - data['cer_tmp'] = None data['cer_pem'] = cer_pem data['cer_txt'] = cer_txt.replace('\n', '') data['serie'] = serie @@ -400,7 +399,6 @@ class Certificado(object): key_enc = _call(args) data['key'] = read_file(self._path_key) - data['key_tmp'] = None data['key_enc'] = key_enc data['p12'] = self._get_p12(password, rfc) return data diff --git a/source/app/models/main.py b/source/app/models/main.py index 93274c7..946d09f 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -299,10 +299,8 @@ class Emisor(BaseModel): class Certificado(BaseModel): key = BlobField(null=True) - key_tmp = BlobField(null=True) key_enc = TextField(default='') cer = BlobField(null=True) - cer_tmp = BlobField(null=True) cer_pem = TextField(default='') cer_txt = TextField(default='') p12 = BlobField(null=True) @@ -360,9 +358,6 @@ class Certificado(BaseModel): } else: msg = cert.error - #~ obj.key_tmp = None - #~ obj.cer_tmp = None - #~ obj.save() Configuracion.add({'path_key': ''}) Configuracion.add({'path_cer': ''}) @@ -1772,12 +1767,14 @@ def test_correo(values): return util.send_mail(data) -def _init_values(): +def _init_values(rfc): data = ( {'clave': 'version', 'valor': VERSION}, {'clave': 'rfc_publico', 'valor': 'XAXX010101000'}, {'clave': 'rfc_extranjero', 'valor': 'XEXX010101000'}, {'clave': 'decimales', 'valor': '2'}, + {'clave': 'path_key', 'valor': ''}, + {'clave': 'path_cer', 'valor': ''}, ) for row in data: try: @@ -1785,6 +1782,10 @@ def _init_values(): Configuracion.create(**row) except IntegrityError: pass + + if not Certificado.select().count(): + Certificado.create(rfc=rfc) + log.info('Valores iniciales insertados...') return @@ -1817,7 +1818,7 @@ def _crear_tablas(rfc): msg = 'El usuario ya existe' log.error(msg) - _init_values() + _init_values(rfc) _importar_valores('', rfc) return True From 1b9e6e16382c980645e7e151ba274af1a99f66c8 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 30 Oct 2017 18:56:50 -0600 Subject: [PATCH 11/12] =?UTF-8?q?Fix=20-=20Verificar=20certificado=20de=20?= =?UTF-8?q?usuario=20en=20producci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/controllers/util.py | 3 +++ source/app/main.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 8180e79..76a007d 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -409,6 +409,9 @@ class Certificado(object): return {} data = self._get_info_cer(rfc) + if not data: + return {} + llave = self._get_info_key(password, data['rfc']) if not llave: return {} diff --git a/source/app/main.py b/source/app/main.py index a35833f..66da61d 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -45,7 +45,6 @@ api.add_route('/products', AppProducts(db)) api.add_route('/invoices', AppInvoices(db)) - if DEBUG: api.add_sink(static, '/static') From 00ff9198597f0d996a34ca7e7acf691b07c09b43 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 30 Oct 2017 19:01:49 -0600 Subject: [PATCH 12/12] Agregar notas para uwsgi --- source/app/main.ini | 4 ++-- source/app/settings.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/source/app/main.ini b/source/app/main.ini index 9c5472f..e4a68ca 100644 --- a/source/app/main.ini +++ b/source/app/main.ini @@ -2,7 +2,7 @@ socket = 127.0.0.1:3033 uid = nginx gid = nginx -#~ Establece una ruta accesible para nginx +#~ Establece una ruta accesible para nginx o el servidor web que uses chdir = /srv/app/empresa-libre/app wsgi-file = main.py callable = app @@ -11,5 +11,5 @@ processes = 4 threads = 4 thunder-lock = true #~ stats = 127.0.0.1:9191 -#~ Establece una ruta accesible para nginx +#~ Establece una ruta accesible para nginx o el servidor web que uses logger = file:/srv/log/empresalibre-uwsgi.log diff --git a/source/app/settings.py b/source/app/settings.py index ad1d0cf..b968e87 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -11,7 +11,7 @@ from conf import DEBUG DEBUG = DEBUG -VERSION = '0.1.0' +VERSION = '0.2.0' EMAIL_SUPPORT = ('soporte@empresalibre.net',) BASE_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -52,6 +52,7 @@ if DEBUG: level=LOG_LEVEL, format_string=format_string).push_application() else: + #~ Establece una ruta con acceso para nginx o el servidor web que uses LOG_PATH = '/srv/log/empresalibre.log' RotatingFileHandler( LOG_PATH,