From 65a3bc931f2b1bce654851a9414c9bfa85c42b25 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Fri, 19 Jan 2018 15:42:00 -0600 Subject: [PATCH] Importar productos en lote --- source/app/controllers/main.py | 2 +- source/app/controllers/util.py | 61 +++++++++++++++++ source/app/models/db.py | 7 +- source/app/models/main.py | 90 ++++++++++++++++++++++++- source/static/js/controller/products.js | 75 +++++++++++++++++++++ source/static/js/ui/products.js | 33 +++++++++ 6 files changed, 265 insertions(+), 3 deletions(-) diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index 551d00e..d702b42 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -221,7 +221,7 @@ class AppProducts(object): def on_post(self, req, resp): values = req.params - req.context['result'] = self._db.product(values) + req.context['result'] = self._db.products(values) resp.status = falcon.HTTP_200 def on_delete(self, req, resp): diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 3141d54..1463e5f 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -914,6 +914,44 @@ class LIBO(object): return self._read(path) + def _get_data(self, doc, name=0): + try: + sheet = doc.getSheets()[name] + cursor = sheet.createCursorByRange(sheet['A1']) + cursor.collapseToCurrentRegion() + except KeyError: + msg = 'Hoja no existe' + return (), msg + return cursor.getDataArray(), '' + + def products(self, path): + options = {'AsTemplate': True, 'Hidden': True} + doc = self._doc_open(path, options) + if doc is None: + return () + + data, msg = self._get_data(doc) + doc.close(True) + + if len(data) == 1: + msg = 'Sin datos para importar' + return (), msg + + fields = ( + 'categoria', + 'clave', + 'clave_sat', + 'descripcion', + 'unidad', + 'valor_unitario', + 'inventario', + 'existencia', + 'codigo_barras', + 'impuestos', + ) + rows = [dict(zip(fields, r)) for r in data[1:]] + return rows, '' + def to_pdf(data, emisor_rfc, ods=False): rfc = data['emisor']['rfc'] @@ -1343,6 +1381,15 @@ def upload_file(rfc, opt, file_obj): name = '{}.sqlite'.format(rfc.lower()) path = _join('/tmp', name) + elif opt == 'products': + 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 = '{}_products.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} @@ -2548,3 +2595,17 @@ class ImportFacturaLibre(object): def print_ticket(data, info): p = PrintTicket(info) return p.printer(data) + + +def import_products(rfc): + name = '{}_products.ods'.format(rfc.lower()) + path = _join(PATH_MEDIA, 'tmp', name) + if not is_file(path): + return () + + if APP_LIBO: + app = LIBO() + if app.is_running: + return app.products(path) + + return () \ No newline at end of file diff --git a/source/app/models/db.py b/source/app/models/db.py index 67f2c80..a3d0923 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -258,10 +258,15 @@ class StorageEngine(object): def get_products(self, values): return main.Productos.get_(values) - def product(self, values): + def products(self, values): id = int(values.pop('id', '0')) if id: return main.Productos.actualizar(values, id) + + opt = values.get('opt', '') + if opt: + return main.Productos.opt(values) + return main.Productos.add(values) def invoice(self, values, user): diff --git a/source/app/models/main.py b/source/app/models/main.py index db7d097..3bddc11 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -894,6 +894,13 @@ class Categorias(BaseModel): def exists(cls, filters): return Categorias.select().where(filters).exists() + @classmethod + def get_by_id(cls, id): + try: + return Categorias.get(Categorias.id==id) + except: + return None + @classmethod def get_all(cls): rows = (Categorias.select( @@ -945,6 +952,13 @@ class SATUnidades(BaseModel): def __str__(self): return '{} ({})'.format(self.name, self.key) + @classmethod + def get_by_name(self, name): + try: + return SATUnidades.get(SATUnidades.name==name) + except: + return None + @classmethod def get_(self): rows = SATUnidades.select().dicts() @@ -2167,6 +2181,81 @@ class Productos(BaseModel): return {'ok': False} + def _validate_import(self, product): + product['categoria'] = Categorias.get_by_id(int(product['categoria'])) + product['unidad'] = SATUnidades.get_by_name(product['unidad']) + product['inventario'] = bool(product['inventario']) + if not product['inventario']: + product['existencia'] = 0.0 + + impuestos = product['impuestos'].split('|') + if not impuestos: + taxes = [SATImpuestos.select().where(SATImpuestos.id==6)] + else: + taxes = [] + for i in range(0, len(impuestos), 3): + w = ( + (SATImpuestos.key == impuestos[i]) & + (SATImpuestos.name == impuestos[i+1]) & + (SATImpuestos.tasa == float(impuestos[i+2])) + ) + try: + taxes.append(SATImpuestos.get(w)) + except: + pass + + product['impuestos'] = taxes + w = (Productos.clave==product['clave']) + return product, w + + def _import(self): + emisor = Emisor.select()[0] + rows, msg = util.import_products(emisor.rfc) + if not rows: + return {'ok': False, 'msg': msg} + + np = 0 + ap = 0 + for p in rows: + data, w = self._validate_import(self, p) + if data['unidad'] is None: + msg = 'Producto: {} - No se encontró la unidad'.format(data['clave']) + log.error(msg) + continue + # ~ print (data) + taxes = data.pop('impuestos') + try: + with database_proxy.transaction(): + if Productos.select().where(w).exists(): + q = Productos.update(**data).where(w) + q.execute() + obj = Productos.get(w) + obj.impuestos = taxes + log.info('Producto actualizado...') + ap += 1 + else: + obj = Productos.create(**data) + obj.impuestos = taxes + log.info('Producto agregado...') + 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 con problemas: {}'.format(len(rows) - np - ap) + return {'ok': True, 'msg': msg} + + @classmethod + def opt(cls, values): + if values['opt'] == 'import': + return cls._import(cls) + + return {'ok': False, 'msg': 'Sin opción'} + @classmethod def get_by(cls, values): clave = values.get('id', '') @@ -2197,7 +2286,6 @@ class Productos(BaseModel): return {'ok': False} name = values.get('name', '') - print (name) if name: rows = (Productos .select( diff --git a/source/static/js/controller/products.js b/source/static/js/controller/products.js index eaf69f8..1c081f4 100644 --- a/source/static/js/controller/products.js +++ b/source/static/js/controller/products.js @@ -7,6 +7,7 @@ var products_controllers = { $$("cmd_delete_product").attachEvent("onItemClick", cmd_delete_product_click) $$("cmd_save_product").attachEvent("onItemClick", cmd_save_product_click) $$("cmd_cancel_product").attachEvent("onItemClick", cmd_cancel_product_click) + $$("cmd_import_products").attachEvent("onItemClick", cmd_import_products_click) $$("chk_automatica").attachEvent("onChange", chk_automatica_change) $$("valor_unitario").attachEvent("onChange", valor_unitario_change) $$('precio_con_impuestos').attachEvent('onChange', precio_con_impuestos_change) @@ -311,3 +312,77 @@ function precio_con_impuestos_key_up(){ } calcular_sin_impuestos(parseFloat(value), taxes) } + + +function cmd_import_products_click(){ + win_import_products.init() + $$('win_import_products').show() +} + + +function cmd_upload_products_click(){ + var form = $$('form_upload_products') + + var values = form.getValues() + + if(!$$('lst_upload_products').count()){ + $$('win_import_products').close() + return + } + + if($$('lst_upload_products').count() > 1){ + msg = 'Selecciona solo un archivo' + msg_error(msg) + return + } + + var template = $$('up_products').files.getItem($$('up_products').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 Productos', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + $$('up_products').send() + } + } + }) +} + + +function up_products_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_products').close() + + webix.ajax().post('/products', {opt: 'import'}, { + error: function(text, data, xhr) { + msg = 'Error al importar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if (values.ok){ + msg_ok(values.msg) + get_products() + }else{ + msg_error(values.msg) + } + } + }) +} \ No newline at end of file diff --git a/source/static/js/ui/products.js b/source/static/js/ui/products.js index 64f593a..f40ff4e 100644 --- a/source/static/js/ui/products.js +++ b/source/static/js/ui/products.js @@ -7,6 +7,9 @@ var toolbar_products = [ autowidth: true, icon: "pencil"}, {view: "button", id: "cmd_delete_product", label: "Eliminar", type: "iconButton", autowidth: true, icon: "minus"}, + {}, + {view: 'button', id: 'cmd_import_products', label: 'Importar', + type: 'iconButton', autowidth: true, icon: 'upload'}, ] @@ -213,3 +216,33 @@ var app_products = { } +var body_import_products = {rows: [ + {view: 'form', id: 'form_upload_products', rows: [ + {cols: [{}, + {view: 'uploader', id: 'up_products', autosend: false, + link: 'lst_upload_products', value: 'Seleccionar Archivo', + upload: '/files/products'}, {}]}, + {cols: [ + {view: 'list', id: 'lst_upload_products', name: 'lst_upload_products', + type: 'uploader', autoheight: true, borderless: true}]}, + {cols: [{}, {view: 'button', id: 'cmd_upload_products', + label: 'Importar Productos'}, {}]}, + ]}, +]} + + +var win_import_products = { + init: function(){ + webix.ui({ + view: 'window', + id: 'win_import_products', + width: 400, + modal: true, + position: 'center', + head: 'Importar Productos', + body: body_import_products, + }) + $$('cmd_upload_products').attachEvent('onItemClick', cmd_upload_products_click) + $$('up_products').attachEvent('onUploadComplete', up_products_upload_complete) + } +} \ No newline at end of file