diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d58d3c..85e9105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ + +v 1.33.0 [11-ene-2020] +---------------------- + - Mejora: Cambio del mensaje para cuando se intenta dar de alta un cliente ya existente. + - Mejora: Solo los admins pueden ver la nómina. + + +v 1.32.0 [05-ene-2020] +---------------------- + - Mejora: Recuperar facturas no aceptas para cancelación por el receptor + + v 1.31.2 [28-oct-2019] ---------------------- - Error: Al generar PDF con tags en las series diff --git a/VERSION b/VERSION index 359c410..7aa332e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.32.0 +1.33.0 diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index a5772b4..70a465c 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -2738,521 +2738,6 @@ def sync_cfdi(auth, files): return -class ImportFacturaLibreGambas(object): - - def __init__(self, conexion, rfc): - self._rfc = rfc - self._con = None - self._cursor = None - self._error = '' - self._is_connect = self._connect(conexion) - self._clientes = [] - self._clientes_rfc = [] - - @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 not DEBUG: - if obj['rfc'] != self._rfc: - self._error = 'Los datos no corresponden al RFC: {}'.format(self._rfc) - return False - - return True - - def _connect(self, conexion): - import psycopg2 - import psycopg2.extras - try: - self._con = psycopg2.connect(conexion) - self._cursor = self._con.cursor(cursor_factory=psycopg2.extras.DictCursor) - 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): - try: - self._cursor.close() - self._con.close() - except: - pass - return - - def import_data(self): - data = {} - tables = ( - ('receptores', 'Socios'), - ('cfdifacturas', 'Facturas'), - ('categorias', 'Categorias'), - ('productos', 'Productos'), - ('tickets', 'Tickets'), - ) - for source, target in tables: - data[target] = self._get_table(source) - - data['Socios'] += self._clientes - - return data - - def _get_table(self, table): - return getattr(self, '_{}'.format(table))() - - def _tickets(self): - sql = "SELECT * FROM tickets" - self._cursor.execute(sql) - rows = self._cursor.fetchall() - - fields = ( - ('serie', 'serie'), - ('folio', 'folio'), - ('fecha', 'fecha'), - ('formadepago', 'forma_pago'), - ('subtotal', 'subtotal'), - ('descuento', 'descuento'), - ('total', 'total'), - ('notas', 'notas'), - ('factura', 'factura'), - ('cancelada', 'cancelado'), - ('vendedor', 'vendedor'), - ) - data = [] - totals = len(rows) - for i, row in enumerate(rows): - msg = '\tImportando ticket {} de {}'.format(i+1, totals) - log.info(msg) - - new = {t: row[s] for s, t in fields} - - new['notas'] = '' - new['fecha'] = new['fecha'].replace(microsecond=0) - new['estatus'] = 'Generado' - if new['cancelado']: - new['estatus'] = 'Cancelado' - new['factura'] = self._get_invoice_ticket(new['factura']) - - new['details'] = self._get_details_ticket(row['id']) - new['taxes'] = self._get_taxes_ticket(row['id']) - data.append(new) - - return data - - def _get_invoice_ticket(self, invoice): - if not invoice: - return None - - sql = "SELECT serie, folio FROM cfdifacturas WHERE id=%s" - self._cursor.execute(sql, [invoice]) - row = self._cursor.fetchone() - if row is None: - return {} - - return dict(row) - - def _get_details_ticket(self, id): - sql = "SELECT * FROM t_detalle WHERE id_cfdi=%s" - self._cursor.execute(sql, [id]) - rows = self._cursor.fetchall() - - fields = ( - ('descripcion', 'descripcion'), - ('cantidad', 'cantidad'), - ('valorunitario', 'valor_unitario'), - ('importe', 'importe'), - ('precio', 'precio_final'), - ) - - data = [] - for row in rows: - new = {t: row[s] for s, t in fields if row[s]} - data.append(new) - - return data - - def _get_taxes_ticket(self, id): - sql = "SELECT * FROM t_impuestos WHERE id_cfdi=%s" - self._cursor.execute(sql, [id]) - rows = self._cursor.fetchall() - - tasas = { - '0': 0.0, - '16': 0.16, - '16.00': 0.16, - '0.16': 0.16, - '11': 0.11, - '-10': 0.10, - '-2': 0.02, - '-0.5': 0.005, - '-2/3': 0.106667, - '-10.6667': 0.106667, - '-10.6666': 0.106667, - '-10.666666': 0.106667, - '-10.66660': 0.106667, - } - - data = [] - for row in rows: - filtro = { - 'name': row['impuesto'], - 'tasa': tasas[row['tasa']], - 'tipo': row['tipo'][0], - } - new = { - 'import': row['importe'], - 'filter': filtro - } - data.append(new) - - return data - - def _productos(self): - UNIDADES = { - 'k': 'KGM', - 'kg': 'KGM', - 'kg.': 'KGM', - 'pieza': 'H87', - 'pza': 'H87', - 'pz': 'H87', - 'bulto': 'H87', - 'b': 'H87', - 'exb': 'H87', - 'ex': 'H87', - 'caja': 'XBX', - 'c': 'XBX', - 'rollo': 'XRO', - 'tira': 'SR', - 't': 'SR', - 'cono': 'XAJ', - 'paquete': 'XPK', - 'pq': 'XPK', - } - sql = "SELECT * FROM productos" - self._cursor.execute(sql) - rows = self._cursor.fetchall() - - fields = ( - ('id_categoria', 'categoria'), - ('noidentificacion', 'clave'), - ('descripcion', 'descripcion'), - # ~ ('unidad', 'unidad'), - ('id_unidad', 'unidad'), - # ~ ('costo', 'ultimo_costo'), - ('valorunitario', 'valor_unitario'), - # ~ ('existencia', 'existencia'), - # ~ ('minimo', 'minimo'), - ('inventario', 'inventario'), - ('codigobarras', 'codigo_barras'), - ('cuentapredial', 'cuenta_predial'), - ) - data = [] - - sql = """ - SELECT nombre, tasa, tipo - FROM impuestos - WHERE id=%s - """ - totals = len(rows) - for i, row in enumerate(rows): - msg = '\tImportando producto {} de {}'.format(i+1, totals) - log.info(msg) - # ~ print (i, dict(row)) - new = {t: row[s] for s, t in fields} - - # ~ print (new['unidad']) - if new['unidad'] == 2: - new['unidad'] = 'servicio' - - u = new['unidad'].lower().strip() - if u in ('sin',): - continue - if not u: - u = 'pieza' - - if not new['categoria']: - new['categoria'] = None - new['codigo_barras'] = new['codigo_barras'] or '' - new['cuenta_predial'] = new['cuenta_predial'] or '' - new['descripcion'] = ' '.join(new['descripcion'].split()) - new['clave_sat'] = DEFAULT_SAT_PRODUCTO - - new['unidad'] = UNIDADES.get(u, new['unidad']) - self._cursor.execute(sql, [row['id_impuesto1']]) - impuestos = self._cursor.fetchall() - new['impuestos'] = tuple(impuestos) - data.append(new) - - return data - - def _categorias(self): - sql = "SELECT * FROM categorias ORDER BY id_padre" - self._cursor.execute(sql) - rows = self._cursor.fetchall() - - fields = ( - ('id', 'id'), - ('categoria', 'categoria'), - ('id_padre', 'padre'), - ) - data = [] - - for row in rows: - new = {t: row[s] for s, t in fields} - if new['padre'] == 0: - new['padre'] = None - data.append(new) - - return data - - def _get_cliente(self, invoice): - sql = "SELECT rfc, nombre FROM receptores WHERE id=%s" - self._cursor.execute(sql, [invoice['id_cliente']]) - obj = self._cursor.fetchone() - if not obj is None: - data = { - 'rfc': obj['rfc'], - 'slug': to_slug(obj['nombre']), - } - return data - - if not invoice['xml']: - return {} - - doc = parse_xml(invoice['xml']) - version = doc.attrib['version'] - node = doc.find('{}Receptor'.format(PRE[version])) - rfc = node.attrib['rfc'] - nombre = node.attrib['nombre'] - - tipo_persona = 1 - if rfc == 'XEXX010101000': - tipo_persona = 4 - elif rfc == 'XAXX010101000': - tipo_persona = 3 - elif len(rfc) == 12: - tipo_persona = 2 - - data = { - 'tipo_persona': tipo_persona, - 'rfc': rfc, - 'nombre': nombre, - 'slug': to_slug(nombre), - 'es_cliente': True, - 'es_activo': False, - } - if not rfc in self._clientes_rfc: - self._clientes_rfc.append(rfc) - self._clientes.append(data) - - data = { - 'rfc': data['rfc'], - 'slug': data['slug'], - } - return data - - def _get_detalles(self, id): - sql = "SELECT * FROM cfdidetalle WHERE id_cfdi=%s" - self._cursor.execute(sql, [id]) - rows = self._cursor.fetchall() - - fields = ( - ('categoria', 'categoria'), - ('cantidad', 'cantidad'), - ('unidad', 'unidad'), - ('noidentificacion', 'clave'), - ('descripcion', 'descripcion'), - ('valorunitario', 'valor_unitario'), - ('importe', 'importe'), - ('numero', 'pedimento'), - ('fecha', 'fecha_pedimento'), - ('aduana', 'aduana'), - ('cuentapredial', 'cuenta_predial'), - ('descuento', 'descuento'), - ('precio', 'precio_final'), - ) - - data = [] - for row in rows: - new = {t: row[s] for s, t in fields if row[s]} - data.append(new) - - return data - - def _get_impuestos(self, id): - sql = "SELECT * FROM cfdiimpuestos WHERE id_cfdi=%s" - self._cursor.execute(sql, [id]) - rows = self._cursor.fetchall() - - tasas = { - '0': 0.0, - '16': 0.16, - '16.00': 0.16, - '11': 0.11, - '-10': 0.10, - '-2': 0.02, - '-0.5': 0.005, - '-2/3': 0.106667, - '-10.6666': 0.106667, - '-10.666666': 0.106667, - '-10.66660': 0.106667, - '-4': 0.04, - } - - data = [] - for row in rows: - filtro = { - 'name': row['impuesto'], - 'tasa': tasas[row['tasa']], - 'tipo': row['tipo'][0], - } - new = { - 'importe': row['importe'], - 'filtro': filtro - } - data.append(new) - - return data - - def _cfdifacturas(self): - sql = "SELECT * FROM cfdifacturas" - self._cursor.execute(sql) - rows = self._cursor.fetchall() - fields = ( - ('version', 'version'), - ('serie', 'serie'), - ('folio', 'folio'), - ('fecha', 'fecha'), - ('fecha_timbrado', 'fecha_timbrado'), - ('formadepago', 'forma_pago'), - ('condicionesdepago', 'condiciones_pago'), - ('subtotal', 'subtotal'), - ('descuento', 'descuento'), - ('tipocambio', 'tipo_cambio'), - ('moneda', 'moneda'), - ('total', 'total'), - ('tipodecomprobante', 'tipo_comprobante'), - ('metododepago', 'metodo_pago'), - ('lugarexpedicion', 'lugar_expedicion'), - ('totalimpuestosretenidos', 'total_retenciones'), - ('totalimpuestostrasladados', 'total_traslados'), - ('xml', 'xml'), - ('id_cliente', 'cliente'), - ('notas', 'notas'), - ('uuid', 'uuid'), - ('cancelada', 'cancelada'), - ) - data = [] - totals = len(rows) - for i, row in enumerate(rows): - msg = '\tImportando factura {} de {}'.format(i+1, totals) - log.info(msg) - - new = {t: row[s] for s, t in fields} - - for _, f in fields: - new[f] = new[f] or '' - - - new['fecha'] = new['fecha'].replace(microsecond=0) - if new['fecha_timbrado']: - new['fecha_timbrado'] = new['fecha_timbrado'].replace(microsecond=0) - else: - new['fecha_timbrado'] = None - - new['estatus'] = 'Timbrada' - if new['cancelada']: - new['estatus'] = 'Cancelada' - - if not new['uuid']: - new['uuid'] = None - elif new['uuid'] in('ok', '123', '??', 'X'): - new['uuid'] = None - new['estatus'] = 'Cancelada' - new['cancelada'] = True - - if new['xml'] is None: - new['xml'] = '' - - new['pagada'] = True - new['total_mn'] = round(row['tipocambio'] * row['total'], 2) - new['detalles'] = self._get_detalles(row['id']) - new['impuestos'] = self._get_impuestos(row['id']) - new['cliente'] = self._get_cliente(row) - data.append(new) - return data - - def _receptores(self): - sql = "SELECT * FROM receptores" - self._cursor.execute(sql) - rows = self._cursor.fetchall() - - fields = ( - ('rfc', 'rfc'), - ('nombre', 'nombre'), - ('calle', 'calle'), - ('noexterior', 'no_exterior'), - ('nointerior', 'no_interior'), - ('colonia', 'colonia'), - ('municipio', 'municipio'), - ('estado', 'estado'), - ('pais', 'pais'), - ('codigopostal', 'codigo_postal'), - ('extranjero', 'es_extranjero'), - ('activo', 'es_activo'), - ('fechaalta', 'fecha_alta'), - ('notas', 'notas'), - ) - data = [] - - sql1 = "SELECT correo FROM correos WHERE id_padre=%s" - sql2 = "SELECT telefono FROM telefonos WHERE id_padre=%s" - totals = len(rows) - for i, row in enumerate(rows): - msg = '\tImportando cliente {} de {}'.format(i+1, totals) - log.info(msg) - new = {t: row[s] for s, t in fields} - new['slug'] = to_slug(new['nombre']) - new['es_cliente'] = True - if new['fecha_alta'] is None: - new['fecha_alta'] = str(now()) - else: - new['fecha_alta'] = str(new['fecha_alta']) - - for _, f in fields: - new[f] = new[f] or '' - if new['es_extranjero']: - new['tipo_persona'] = 4 - elif new['rfc'] == 'XAXX010101000': - new['tipo_persona'] = 3 - elif len(new['rfc']) == 12: - new['tipo_persona'] = 2 - - self._cursor.execute(sql1, (row['id'],)) - tmp = self._cursor.fetchall() - if tmp: - new['correo_facturas'] = ', '.join([r[0] for r in tmp]) - - self._cursor.execute(sql2, (row['id'],)) - tmp = self._cursor.fetchall() - if tmp: - new['telefonos'] = ', '.join([r[0] for r in tmp]) - - data.append(new) - return data - - class ImportFacturaLibre(object): def __init__(self, path, rfc): diff --git a/source/app/controllers/utils.py b/source/app/controllers/utils.py index f653679..5baaf16 100644 --- a/source/app/controllers/utils.py +++ b/source/app/controllers/utils.py @@ -23,6 +23,7 @@ import json import logging import math import os +import shlex import shutil import smtplib import sqlite3 @@ -48,6 +49,8 @@ from dateutil import parser import seafileapi +from settings import DEBUG, DB_COMPANIES, PATHS + LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' LOG_DATE = '%d/%m/%Y %H:%M:%S' @@ -61,6 +64,11 @@ logging.getLogger('peewee').setLevel(logging.WARNING) TIMEOUT = 10 PATH_INVOICES = 'facturas' +PG_DUMP = 'pg_dump -U postgres' +PSQL = 'psql -U postgres' +if DEBUG: + PG_DUMP = 'pg_dump -h localhost -U postgres' + PSQL = 'psql -h localhost -U postgres' #~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37 @@ -259,6 +267,16 @@ def _call(args): return subprocess.check_output(args, shell=True).decode() +def _run(args, wait=False): + result = '' + cmd = shlex.split(args) + if wait: + result = subprocess.run(cmd, shell=True, check=True).stdout.decode() + else: + subprocess.run(cmd) + return result + + def _join(*paths): return os.path.join(*paths) @@ -332,29 +350,96 @@ def to_zip(files): return zip_buffer.getvalue() -def db_delete(user, path): - dt = datetime.datetime.now().strftime('%y%m%d_%H%M') - path_bk = _join(path, 'tmp', '{}_{}.bk'.format(user, dt)) - args = 'pg_dump -U postgres -Fc {} > "{}"'.format(user, path_bk) - try: - _call(args) - except: - pass +def dumps(data): + return json.dumps(data, default=str) - args = 'psql -U postgres -c "DROP DATABASE {0};"'.format(user) - try: - _call(args) - except: - pass - args = 'psql -U postgres -c "DROP ROLE {0};"'.format(user) - try: - _call(args) - except: - pass +def loads(data): + return json.loads(data) + + +def _validate_db_rfc(): + con = sqlite3.connect(DB_COMPANIES) + sql = """ + CREATE TABLE IF NOT EXISTS names( + rfc TEXT NOT NULL COLLATE NOCASE UNIQUE, + con TEXT NOT NULL + ); + """ + cursor = con.cursor() + cursor.executescript(sql) + cursor.close() + con.close() return +def _sql_companies(sql, args=()): + _validate_db_rfc() + con = sqlite3.connect(DB_COMPANIES) + cursor = con.cursor() + try: + cursor.execute(sql, args) + data = cursor.fetchall() + except sqlite3.IntegrityError as e: + log.error(e) + return False + + con.commit() + cursor.close() + con.close() + return data + + +def rfc_get(): + sql = "SELECT * FROM names" + data = _sql_companies(sql) + return data + + +def rfc_exists(rfc): + sql = "SELECT rfc FROM names WHERE rfc = ?" + data = _sql_companies(sql, (rfc,)) + if isinstance(data, bool): + return + return bool(data) + + +def rfc_add(rfc, con): + sql = "INSERT INTO names VALUES (?, ?)" + data = _sql_companies(sql, (rfc.upper(), dumps(con))) + return True + + +def db_create(user): + args = f'{PSQL} -c "CREATE ROLE {user} WITH LOGIN ENCRYPTED PASSWORD \'{user}\';"' + _run(args) + args = f'{PSQL} -c "CREATE DATABASE {user} WITH OWNER {user};"' + _run(args) + return True + + +def db_delete(user, path, no_database=False): + sql = "DELETE FROM names WHERE rfc = ?" + data = _sql_companies(sql, (user,)) + + if no_database: + return True + + user = user.replace('&', '').lower() + dt = now().strftime('%y%m%d_%H%M') + path_bk = _join(path, f'{user}_{dt}.bk') + + args = f'{PG_DUMP} -d {user} -Fc -f "{path_bk}"' + _run(args) + + args = f'{PSQL} -c "DROP DATABASE {user};"' + _run(args) + + args = f'{PSQL} -c "DROP ROLE {user};"' + _run(args) + return True + + def _get_pass(rfc): return rfc @@ -417,6 +502,35 @@ def db_backup(path_companies, path_bk, is_mv, url_seafile): return +def _validate_path_local(): + path_bk = _join(str(Path.home()), PATHS['LOCAL']) + if not os.path.isdir(path_bk): + path_bk = '' + return path_bk + + +def db_backup_local(): + path_bk = _validate_path_local() + if not path_bk: + msg = 'No existe la carpeta local' + return {'ok': False, 'msg': msg} + + data = rfc_get() + if not len(data): + msg = 'Sin bases de datos a respaldar' + return {'ok': False, 'msg': msg} + + for row in data: + user = row[0].lower() + db = loads(row[1])['name'] + path = _join(path_bk, '{}.bk'.format(user)) + args = f'{PG_DUMP} -d {user} -Fc -f "{path}"' + _run(args) + + msg = 'Bases de datos respaldadas correctamente' + result = {'ok': True, 'msg': msg} + return result + def now(): return datetime.datetime.now().replace(microsecond=0) diff --git a/source/app/models/db.py b/source/app/models/db.py index 78c04da..b302198 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -34,19 +34,18 @@ class StorageEngine(object): return main.CfdiNomina.get_by(values) def empresa_agregar(self, values): - return main.empresa_agregar(values['alta_rfc'], False) + # ~ return main.empresa_agregar(values['alta_rfc'], False) + return main._new_client(values['alta_rfc'], False) def empresa_borrar(self, values): - return main.empresa_borrar(values['rfc']) + # ~ return main.empresa_borrar(values['rfc']) + return main._delete_client(values['rfc'], False, False) def respaldar_dbs(self): return main.respaldar_dbs() - def _get_empresas(self, values): - return main.get_empresas() - def get_values(self, table, values=None, session=None): - if table in ('allusuarios', 'usuarioupdate'): + if table in ('allusuarios', 'usuarioupdate', 'main'): return getattr(self, '_get_{}'.format(table))(values, session) return getattr(self, '_get_{}'.format(table))(values) @@ -71,8 +70,8 @@ class StorageEngine(object): def _get_importinvoice(self, values): return main.import_invoice() - def _get_main(self, values): - return main.config_main() + def _get_main(self, values, session): + return main.config_main(session['userobj']) def _get_configtimbrar(self, values): return main.config_timbrar() @@ -463,3 +462,7 @@ class StorageEngine(object): def nomina(self, values, user): return main.CfdiNomina.post(values, user) + # Companies only in MV + def _get_empresas(self, values): + return main.companies_get() + diff --git a/source/app/models/main.py b/source/app/models/main.py index df26dba..0d60249 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -16,6 +16,7 @@ # ~ You should have received a copy of the GNU General Public License # ~ along with this program. If not, see . +import argparse from decimal import Decimal import sqlite3 import click @@ -31,7 +32,7 @@ if __name__ == '__main__': from controllers import util -from settings import log, DEBUG, COMPANIES, VERSION, PATH_CP, PRE, CURRENT_CFDI, \ +from settings import log, COMPANIES, VERSION, PATH_CP, PRE, CURRENT_CFDI, \ INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \ CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \ DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \ @@ -40,6 +41,7 @@ from settings import log, DEBUG, COMPANIES, VERSION, PATH_CP, PRE, CURRENT_CFDI, # ~ v2 from controllers import utils from settings import ( + DEBUG, DB_COMPANIES, EXT, IS_MV, @@ -249,14 +251,18 @@ def get_doc(type_doc, id, rfc): return data, file_name, content_type -def config_main(): +def config_main(user): try: obj = Emisor.select()[0] except IndexError: obj = None punto_de_venta = Configuracion.get_bool('chk_usar_punto_de_venta') + nomina = Configuracion.get_bool('chk_usar_nomina') + if not user.es_admin: + nomina = False + data = { 'empresa': get_title_app(3), 'punto_de_venta': punto_de_venta, @@ -2632,10 +2638,17 @@ class Socios(BaseModel): def add(cls, values): accounts = util.loads(values.pop('accounts', '[]')) fields = cls._clean(cls, values) + + w = ((Socios.rfc==fields['rfc']) & (Socios.slug==fields['slug'])) + if Socios.select().where(w).exists(): + msg = 'Ya existe el RFC y Razón Social' + data = {'ok': False, 'row': {}, 'new': True, 'msg': msg} + return data + try: obj = Socios.create(**fields) except IntegrityError as e: - msg = 'Ya existe el RFC y Razón Social' + msg = 'Ocurrio un error, al dar de alta el emisor' data = {'ok': False, 'row': {}, 'new': True, 'msg': msg} return data @@ -9349,49 +9362,20 @@ def _iniciar_bd(): return -def _agregar_rfc(no_bd): - rfc = input('Introduce el nuevo RFC: ').strip().upper() - if not rfc: - msg = 'El RFC es requerido' - log.error(msg) - return - - datos = input('Introduce los datos de conexión: ').strip() - if not datos: - msg = 'Los datos de conexión son requeridos' - log.error(msg) - return - - opt = util.parse_con(datos) - if not opt: - log.error('Datos de conexión incompletos') - return - - args = opt.copy() - if conectar(args): - if _add_emisor(rfc, util.dumps(opt)): - if no_bd: - log.info('RFC agregado correctamente...') - return - _crear_tablas(rfc) - log.info('RFC agregado correctamente...') - return - - log.error('No se pudo agregar el RFC') - return - - def _borrar_rfc(): - rfc = input('Introduce el RFC a borrar: ').strip().upper() + rfc = input('Introduce el RFC a borrar: ').strip().lower() if not rfc: msg = 'El RFC es requerido' log.error(msg) return - confirm = input('¿Estás seguro de borrar el RFC?') + confirm = input('¿Estás seguro de borrar el RFC? [si]') + if confirm != 'si': + log.info('Proceso cancelado...') + return - if _delete_emisor(rfc): - util.delete_db(rfc.lower()) + if _delete_emisor(rfc.upper()): + utils.db_delete(rfc, PATHS['BK']) log.info('RFC borrado correctamente...') return @@ -9460,9 +9444,7 @@ def empresa_agregar(rfc, no_bd): def empresa_borrar(rfc): - if _delete_emisor(rfc): - # ~ util.delete_db(rfc.lower()) - utils.db_delete(rfc.lower(), PATHS['DOCS']) + utils.db_delete(rfc.lower(), PATHS['BK']) return True @@ -9471,21 +9453,8 @@ def respaldar_dbs(): msg = 'Solo MV' return {'ok': False, 'msg': msg} - result = util.validate_path_bk() - if not result['ok']: - return result - path_bk = result['msg'] - - data = util.get_rfcs() - if not len(data): - msg = 'Sin bases de datos a respaldar' - return {'ok': False, 'msg': msg} - - for row in data: - util.respaldar_db(row, path_bk) - - msg = 'Bases de datos respaldadas correctamente' - return {'ok': True, 'msg': msg} + result = utils.db_backup_local() + return result def _importar_valores(archivo='', rfc=''): @@ -9776,37 +9745,6 @@ def _importar_factura_libre(archivo): return -def _importar_factura_libre_gambas(conexion): - rfc = input('Introduce el RFC: ').strip().upper() - if not rfc: - msg = 'El RFC es requerido' - log.error(msg) - return - - args = util.get_con(rfc) - if not args: - return - - conectar(args) - - log.info('Importando datos...') - app = util.ImportFacturaLibreGambas(conexion, rfc) - if not app.is_connect: - log.error('\t{}'.format(app._error)) - return - - data = app.import_data() - - _importar_socios(data['Socios']) - _importar_facturas(data['Facturas']) - _importar_categorias(data['Categorias']) - _importar_productos_gambas(data['Productos']) - _import_tickets(data['Tickets']) - - log.info('Importación terminada...') - return - - def _exist_ticket(row): filters = ( (Tickets.serie==row['serie']) & @@ -10074,18 +10012,126 @@ def _exportar_documentos(): return -def _test(): - rfc = input('Introduce el RFC: ').strip().upper() +# ~ v2 + +def companies_get(): + data = utils.rfc_get() + rows = [] + for row in data: + rows.append({'delete': '-', 'rfc': row[0].upper()}) + return tuple(rows) + + +def _list_clients(): + rows = utils.rfc_get() + for row in rows: + msg = f'RFC: {row[0].upper()}' + print(msg) + return + + +def _new_client(rfc, no_database): + rfc = rfc.lower() if not rfc: - msg = 'El RFC es requerido' + log.error('Falta el RFC') + return + + if utils.rfc_exists(rfc): + msg = 'El RFC ya esta dado de alta' log.error(msg) + return {'ok': False, 'msg': msg} + + user = rfc.replace('&', '').lower() + if not no_database: + if not utils.db_create(user): + msg = 'No se pudo crear la base de datos' + log.error(msg) + return {'ok': False, 'msg': msg} + + args = { + 'type': 'postgres', + 'name': user, + 'user': user, + 'password': user, + } + if not conectar(args.copy()): + msg = 'No se pudo conectar a la base de datos' + log.error(msg) + return {'ok': False, 'msg': msg} + + if not utils.rfc_add(rfc, args): + msg = 'No se pudo guardar el nuevo emisor' + log.error(msg) + return {'ok': False, 'msg': msg} + + if not no_database: + if not _crear_tablas(rfc): + msg = 'No se pudo crear las tablas' + log.error(msg) + return {'ok': False, 'msg': msg} + + desconectar() + msg = 'Emisor dado de alta correctamente' + row = {'delete': '-', 'rfc': rfc.upper()} + result = {'ok': True, 'msg': msg, 'row': row} + return result + + +def _delete_client(rfc, no_database, ask=True): + rfc = rfc.lower() + if not rfc: + log.error('Falta el RFC') return - args = util.get_con(rfc) - if not args: + if not utils.rfc_exists(rfc): + msg = 'El RFC no esta dado de alta' + log.error(msg) + return {'ok': False, 'msg': msg} + + if ask: + confirm = input('¿Estás seguro de borrar el RFC? [si]') + if confirm != 'si': + log.info('Proceso cancelado...') + return False + + if utils.db_delete(rfc, PATHS['BK'], no_database): + log.info('RFC borrado correctamente...') + result = True + else: + log.error('No se pudo borrar el RFC') + result = False + + return result + + +def _process_command_line_arguments(): + parser = argparse.ArgumentParser( + description='Empresa Libre') + parser.add_argument('-lc', '--list-clients', dest='list_clients', + action='store_true', default=False, required=False) + parser.add_argument('-nc', '--new-client', dest='new_client', + action='store_true', default=False, required=False) + parser.add_argument('-dc', '--delete-client', dest='delete_client', + action='store_true', default=False, required=False) + parser.add_argument('-ndb', '--no-database', dest='no_database', + action='store_true', default=False, required=False) + + parser.add_argument('-r', '--rfc', dest='rfc', default='') + return parser.parse_args() + + +def main2(args): + if args.list_clients: + _list_clients() return - conectar(args) + if args.new_client: + _new_client(args.rfc, args.no_database) + return + + if args.delete_client: + _delete_client(args.rfc, args.no_database) + return return @@ -10108,14 +10154,12 @@ help_lr = 'Listar RFCs' is_flag=True, default=False) @click.option('-cc', '--cambiar-contraseña', help=help_change_pass, is_flag=True, default=False) -@click.option('-ar', '--agregar-rfc', help=help_rfc, is_flag=True, default=False) @click.option('-br', '--borrar-rfc', help=help_br, is_flag=True, default=False) @click.option('-lr', '--listar-rfc', help=help_lr, is_flag=True, default=False) @click.option('-i', '--importar-valores', is_flag=True, default=False) @click.option('-f', '--archivo') @click.option('-c', '--conexion') @click.option('-fl', '--factura-libre', is_flag=True, default=False) -@click.option('-flg', '--factura-libre-gambas', is_flag=True, default=False) @click.option('-t', '--test', is_flag=True, default=False) @click.option('-gap', '--generar-archivo-productos', is_flag=True, default=False) @click.option('-ip', '--importar-productos', is_flag=True, default=False) @@ -10127,22 +10171,19 @@ help_lr = 'Listar RFCs' @click.option('-id', '--importar-directorio') @click.option('-ed', '--exportar_documentos', is_flag=True, default=False) def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, - agregar_rfc, borrar_rfc, listar_rfc, importar_valores, archivo, conexion, - factura_libre, factura_libre_gambas, test, generar_archivo_productos, + borrar_rfc, listar_rfc, importar_valores, archivo, conexion, + factura_libre, test, generar_archivo_productos, importar_productos, backup_dbs, no_bd, alta, rfc, detalle, importar_directorio, exportar_documentos): opt = locals() - if opt['test']: - _test() - sys.exit(0) - if opt['alta']: if not opt['rfc']: msg = 'Falta el RFC' raise click.ClickException(msg) - empresa_agregar(opt['rfc'], no_bd) + # ~ empresa_agregar(opt['rfc'], no_bd) + _new_client(opt['rfc'], no_bd) sys.exit(0) if opt['iniciar_bd']: @@ -10161,10 +10202,6 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, _cambiar_contraseña() sys.exit(0) - if opt['agregar_rfc']: - _agregar_rfc(no_bd) - sys.exit(0) - if opt['borrar_rfc']: _borrar_rfc() sys.exit(0) @@ -10199,13 +10236,6 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, _importar_factura_libre(opt['archivo']) sys.exit(0) - if opt['factura_libre_gambas']: - if not opt['conexion']: - msg = 'Falta los datos de conexión' - raise click.ClickException(msg) - _importar_factura_libre_gambas(opt['conexion']) - sys.exit(0) - if opt['generar_archivo_productos']: if not opt['archivo']: msg = 'Falta la ruta de la base de datos' @@ -10252,5 +10282,8 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, if __name__ == '__main__': - main() + args = _process_command_line_arguments() + main2(args) + # ~ main() + diff --git a/source/app/settings.py b/source/app/settings.py index fcb3570..4b9a4ba 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -47,7 +47,7 @@ except ImportError: DEBUG = DEBUG -VERSION = '1.32.0' +VERSION = '1.33.0' EMAIL_SUPPORT = ('soporte@empresalibre.mx',) TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION) @@ -200,6 +200,7 @@ CURRENCY_MN = 'MXN' IS_MV = MV DB_COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db')) path_bk = os.path.join(path_docs, 'tmp') +path_local = 'facturas' EXT = { 'CSS': 'css', @@ -218,6 +219,7 @@ PATHS = { 'USER': path_user_template, 'LOGOS': path_user_logos, 'BK': path_bk, + 'LOCAL': path_local, } VALUES_PDF = {