From 7873a4376cf5a847d1c6ce113f6d91246a53e043 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Fri, 19 Jan 2018 22:16:19 -0600 Subject: [PATCH] Facturar productos en lote --- source/app/controllers/util.py | 46 ++++++++- source/app/models/db.py | 3 + source/app/models/main.py | 66 +++++++++++- source/static/js/controller/invoices.js | 131 ++++++++++++++++++++---- source/static/js/controller/main.js | 17 --- source/static/js/controller/products.js | 23 ++++- source/static/js/ui/invoices.js | 37 ++++++- 7 files changed, 275 insertions(+), 48 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 1463e5f..ffb2b7c 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -928,7 +928,7 @@ class LIBO(object): options = {'AsTemplate': True, 'Hidden': True} doc = self._doc_open(path, options) if doc is None: - return () + return (), 'No se pudo abrir la plantilla' data, msg = self._get_data(doc) doc.close(True) @@ -952,6 +952,22 @@ class LIBO(object): rows = [dict(zip(fields, r)) for r in data[1:]] return rows, '' + def invoice(self, path): + options = {'AsTemplate': True, 'Hidden': True} + doc = self._doc_open(path, options) + if doc is None: + return (), 'No se pudo abrir la plantilla' + + data, msg = self._get_data(doc) + doc.close(True) + + if len(data) == 1: + msg = 'Sin datos para importar' + return (), msg + + rows = tuple(data[1:]) + return rows, '' + def to_pdf(data, emisor_rfc, ods=False): rfc = data['emisor']['rfc'] @@ -1390,6 +1406,15 @@ def upload_file(rfc, opt, file_obj): name = '{}_products.ods'.format(rfc.lower()) path = _join(PATH_MEDIA, 'tmp', name) + elif opt == 'invoiceods': + 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 = '{}_invoice.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) if save_file(path, file_obj.file.read()): return {'status': 'server', 'name': file_obj.filename, 'ok': True} @@ -2601,11 +2626,26 @@ def import_products(rfc): name = '{}_products.ods'.format(rfc.lower()) path = _join(PATH_MEDIA, 'tmp', name) if not is_file(path): - return () + return (), 'No se encontró la plantilla' if APP_LIBO: app = LIBO() if app.is_running: return app.products(path) - return () \ No newline at end of file + return (), 'No se encontro LibreOffice' + + +def import_invoice(rfc): + name = '{}_invoice.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) + if not is_file(path): + return (), 'No se encontró la plantilla' + + if APP_LIBO: + app = LIBO() + if app.is_running: + return app.invoice(path) + + return (), 'No se encontro LibreOffice' + diff --git a/source/app/models/db.py b/source/app/models/db.py index a3d0923..cf12c63 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -25,6 +25,9 @@ class StorageEngine(object): return getattr(self, '_get_{}'.format(table))(values, session) return getattr(self, '_get_{}'.format(table))(values) + def _get_importinvoice(self, values): + return main.import_invoice() + def _get_main(self, values): return main.config_main() diff --git a/source/app/models/main.py b/source/app/models/main.py index 3bddc11..cc064b1 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -104,6 +104,62 @@ def validar_timbrar(): return {'ok': True, 'msg': msg} + +def _get_taxes_product(id): + model_pt = Productos.impuestos.get_through_model() + impuestos = tuple(model_pt + .select( + model_pt.productos_id.alias('product'), + model_pt.satimpuestos_id.alias('tax')) + .where(model_pt.productos_id==id).dicts()) + return impuestos + + +def import_invoice(): + log.info('Importando factura...') + emisor = Emisor.select()[0] + rows, msg = util.import_invoice(emisor.rfc) + if not rows: + return {'ok': False, 'msg': msg} + + # ~ clave, descripcion, precio, cantidad + products = {} + for row in rows: + try: + obj = Productos.get(Productos.clave==row[0]) + if obj.id in products: + vu = round(row[2], 2) + cant = round(row[3], 2) + pf = products[obj.id]['valor_unitario'] - float(obj.descuento) + products[obj.id]['cantidad'] += cant + products[obj.id]['importe'] = round( + pf * products[obj.id]['cantidad'], DECIMALES) + if vu != products[obj.id]['valor_unitario']: + msg = 'Precio diferente en producto: {}'.format(row[0]) + return {'ok': False, 'msg': msg} + else: + vu = round(row[2], 2) + cant = round(row[3], 2) + pf = vu - float(obj.descuento) + p = { + 'id': obj.id, + 'delete': '-', + 'clave': obj.clave, + 'descripcion': obj.descripcion, + 'unidad': obj.unidad.name, + 'cantidad': cant, + 'valor_unitario': vu, + 'descuento': obj.descuento, + 'importe': round(pf * cant, DECIMALES), + 'taxes': _get_taxes_product(obj.id), + } + products[obj.id] = p + except Productos.DoesNotExist: + pass + log.info('Factura importada...') + return {'ok': True, 'rows': tuple(products.values())} + + def get_doc(type_doc, id, rfc): types = { 'xml': 'application/xml', @@ -2231,21 +2287,21 @@ class Productos(BaseModel): q.execute() obj = Productos.get(w) obj.impuestos = taxes - log.info('Producto actualizado...') + log.info('\tProducto actualizado: {}'.format(data['clave'])) ap += 1 else: obj = Productos.create(**data) obj.impuestos = taxes - log.info('Producto agregado...') + log.info('\tProducto agregado: {}'.format(data['clave'])) np += 1 except Exception as e: msg = 'Error al importar producto: {}'.format(data['clave']) log.error(msg) log.error(e) - msg = 'Productos encontrados: {}\n'.format(len(rows)) - msg += 'Productos agregados: {}\n'.format(np) - msg += 'Productos actualizados: {}'.format(ap) + msg = 'Productos encontrados: {}
'.format(len(rows)) + msg += 'Productos agregados: {}
'.format(np) + msg += 'Productos actualizados: {}
'.format(ap) msg += 'Productos con problemas: {}'.format(len(rows) - np - ap) return {'ok': True, 'msg': msg} diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index f7b03b8..4bc2f6f 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -43,6 +43,7 @@ var invoices_controllers = { $$('cmd_cfdi_notes').attachEvent('onItemClick', cmd_cfdi_notes_click) $$('cmd_admin_invoice_notes').attachEvent('onItemClick', cmd_admin_invoice_notes_click) + $$('cmd_import_invoice').attachEvent('onItemClick', cmd_import_invoice_click) webix.extend($$('grid_invoices'), webix.ProgressBar) } @@ -858,7 +859,6 @@ function set_product(values){ table_pt.insert(v) } } - //~ calculate_taxes() calcular_impuestos() } @@ -868,25 +868,6 @@ function grid_products_found_click(obj){ } -//~ function search_product_by_id(id){ - //~ webix.ajax().get('/values/product', {'id': id}, { - //~ error: function(text, data, xhr) { - //~ msg_error('Error al consultar') - //~ }, - //~ success: function(text, data, xhr){ - //~ var values = data.json() - //~ if (values.ok){ - //~ set_product(values) - //~ } else { - //~ msg = 'No se encontró un producto con la clave: ' + id - //~ msg_error(msg) - //~ } - //~ } - //~ }) - -//~ } - - function search_product_by_key(key){ webix.ajax().get('/values/productokey', {'key': key}, { error: function(text, data, xhr) { @@ -1864,4 +1845,112 @@ function cmd_admin_invoice_notes_click(){ to_end('invoice_notes') }) cfg_invoice['notes'] = true -} \ No newline at end of file +} + + +function cmd_import_invoice_click(){ + win_import_invoice.init() + $$('win_import_invoice').show() +} + + +function cmd_upload_invoice_click(){ + var form = $$('form_upload_invoice') + + var values = form.getValues() + + if(!$$('lst_upload_invoice').count()){ + $$('win_import_invoice').close() + return + } + + if($$('lst_upload_invoice').count() > 1){ + msg = 'Selecciona solo un archivo' + msg_error(msg) + return + } + + var template = $$('up_invoice').files.getItem($$('up_invoice').files.getFirstId()) + + if(template.type.toLowerCase() != 'ods'){ + msg = 'Archivo inválido.\n\nSe requiere un archivo ODS' + msg_error(msg) + return + } + + msg = '¿Estás seguro de importar este archivo?' + webix.confirm({ + title: 'Importar Factura', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + $$('up_invoice').send() + } + } + }) +} + + +function add_import_product_taxes(taxes){ + //~ var taxes = values.taxes + //~ var row = grid.getItem(values.id) + + //~ values['delete'] = '-' + //~ if (row == undefined){ + //~ grid.add(values) + //~ } else { + //~ values['cantidad'] = parseFloat(row.cantidad) + parseFloat(values['cantidad']) + //~ values['valor_unitario'] = parseFloat(row.valor_unitario) + //~ values['descuento'] = parseFloat(row.descuento) + //~ var precio_final = values['valor_unitario'] - values['descuento'] + //~ values['importe'] = (precio_final * values['cantidad']).round(DECIMALES) + //~ grid.updateItem(row.id, values) + //~ } + + for(var v of taxes){ + var pt = table_pt.findOne(v) + if(pt === null){ + table_pt.insert(v) + } + } +} + + +function up_invoice_upload_complete(response){ + if(response.status != 'server'){ + msg = 'Ocurrio un error al subir el archivo' + msg_error(msg) + return + } + msg = 'Archivo subido correctamente.\n\nComenzando importación.' + msg_ok(msg) + $$('win_import_invoice').close() + + webix.ajax().get('/values/importinvoice', { + error: function(text, data, xhr) { + msg_error('Error al consultar') + }, + success: function(text, data, xhr){ + var values = data.json() + if (values.ok){ + for(var p of values.rows){ + add_import_product_taxes(p.taxes) + } + grid.clearAll() + grid.parse(values.rows, 'json') + grid.refresh() + calcular_impuestos() + }else{ + webix.alert({ + title: 'Error al importar', + text: values.msg, + type: 'alert-error', + }) + } + } + }) +} + diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index 0e68323..d2bbf9e 100644 --- a/source/static/js/controller/main.js +++ b/source/static/js/controller/main.js @@ -75,23 +75,6 @@ function get_partners(){ } -function get_products(){ - var grid = $$('grid_products') - webix.ajax().get('/products', {}, { - error: function(text, data, xhr) { - msg_error('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/controller/products.js b/source/static/js/controller/products.js index 1c081f4..d409a87 100644 --- a/source/static/js/controller/products.js +++ b/source/static/js/controller/products.js @@ -50,6 +50,24 @@ function get_categorias(){ } +function get_products(){ + var grid = $$('grid_products') + webix.ajax().get('/products', {}, { + error: function(text, data, xhr) { + msg_error('Error al consultar') + }, + success: function(text, data, xhr) { + var values = data.json(); + grid.clearAll(); + if (values.ok){ + grid.parse(values.rows, 'json'); + grid.refresh() + }; + } + }); +} + + function cmd_new_product_click(id, e, node){ get_taxes() configurar_productos(true) @@ -378,8 +396,11 @@ function up_products_upload_complete(response){ success: function(text, data, xhr) { var values = data.json(); if (values.ok){ - msg_ok(values.msg) get_products() + webix.alert({ + title: 'Importación terminada', + text: values.msg, + }) }else{ msg_error(values.msg) } diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index eb39161..754bf82 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -201,6 +201,8 @@ var toolbar_invoices_generate = {view: 'toolbar', elements: [{}, type: 'iconButton', autowidth: true, icon: 'commenting-o'}, {view: 'button', id: 'cmd_cfdi_relacionados', label: 'CFDI Relacionados', type: 'iconButton', autowidth: true, icon: 'file-o'}, + {view: 'button', id: 'cmd_import_invoice', label: 'Importar', + type: 'iconButton', autowidth: true, icon: 'upload'}, {view: 'checkbox', id: 'chk_cfdi_anticipo', labelRight: 'Es Anticipo', labelWidth: 0, width: 100, hidden: true}, {view: 'checkbox', id: 'chk_cfdi_donativo', labelRight: 'Es Donativo', @@ -277,7 +279,8 @@ var grid_invoices = { var grid_details_cols = [ {id: "id", header:"ID", hidden: true}, {id: 'delete', header: '', width: 30, css: 'delete'}, - {id: "clave", header:{text: 'Clave', css: 'center'}, width: 100}, + {id: "clave", header:{text: 'Clave', css: 'center'}, width: 100, + adjust: 'data'}, {id: "clave_sat", hidden: true}, {id: "descripcion", header:{text: 'Descripción', css: 'center'}, fillspace: true, editor: 'popup'}, @@ -661,3 +664,35 @@ var app_invoices = { multi_invoices ], } + + +var body_upload_invoice = {rows: [ + {view: 'form', id: 'form_upload_invoice', rows: [ + {cols: [{}, + {view: 'uploader', id: 'up_invoice', autosend: false, + link: 'lst_upload_invoice', value: 'Seleccionar Archivo', + upload: '/files/invoiceods'}, {}]}, + {cols: [ + {view: 'list', id: 'lst_upload_invoice', name: 'lst_upload_invoice', + type: 'uploader', autoheight: true, borderless: true}]}, + {cols: [{}, {view: 'button', id: 'cmd_upload_invoice', + label: 'Importar Factura'}, {}]}, + ]}, +]} + + +var win_import_invoice = { + init: function(){ + webix.ui({ + view: 'window', + id: 'win_import_invoice', + width: 400, + modal: true, + position: 'center', + head: 'Importar Factura de Plantilla', + body: body_upload_invoice, + }) + $$('cmd_upload_invoice').attachEvent('onItemClick', cmd_upload_invoice_click) + $$('up_invoice').attachEvent('onUploadComplete', up_invoice_upload_complete) + } +} \ No newline at end of file