diff --git a/source/app/conf.py.example b/source/app/conf.py.example index 03166fb..0892815 100644 --- a/source/app/conf.py.example +++ b/source/app/conf.py.example @@ -11,3 +11,15 @@ DEFAULT_PASSWORD = 'blades3.3' #~ Establece una ruta accesible para el servidor web LOG_PATH = '/srv/empresa/logs/empresalibre.log' + +# ~ Establece los valores para sincronizar los backups de la base de datos +# ~ por ejemplo +# ~ SEAFILE_SERVER = { + # ~ 'URL': 'https://tu_servidor_seafile', + # ~ 'USER': 'tu_usuario', + # ~ 'PASS': 'tu_contraseña', + # ~ 'REPO': 'id_repo', +# ~ } + + +SEAFILE_SERVER = {} \ No newline at end of file diff --git a/source/app/controllers/helper.py b/source/app/controllers/helper.py index 0ccfc90..64bda8b 100644 --- a/source/app/controllers/helper.py +++ b/source/app/controllers/helper.py @@ -12,6 +12,9 @@ from email.mime.text import MIMEText from email import encoders from email.utils import formatdate +import os +import requests + from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Image from reportlab.lib import colors from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle @@ -724,3 +727,200 @@ class ReportTemplate(BaseDocTemplate): return +class SeaFileAPI(object): + FILE_DOES_NOT_EXIST = 441 + + def __init__(self, url, username, password): + self._url = url + self._headers = self._get_auth(username, password) + + @property + def is_connect(self): + return bool(self._headers) + + def _open(self, path): + return open(path, 'rb') + + def _get_auth(self, username, password): + url = self._url + 'auth-token/' + data = { + 'username': username, + 'password': password, + } + resp = requests.post(url, data=data) + if resp.status_code != requests.codes.ok: + msg = 'Token Error: {}'.format(resp.status_code) + print (msg) + return {} + + headers = {'Authorization': 'Token {}'.format(resp.json()['token'])} + return headers + + def _get_upload_link(self, repo_id): + if not self._headers: + return '' + + url = '{}repos/{}/upload-link/'.format(self._url, repo_id) + resp = requests.get(url, headers=self._headers) + if resp.status_code != requests.codes.ok: + msg = 'Error: {}'.format(resp.status_code) + print (msg) + return '' + + return resp.json() + + def _get_update_link(self, repo_id): + if not self._headers: + return '' + + url = '{}repos/{}/update-link/'.format(self._url, repo_id) + data = {'p': '/'} + resp = requests.get(url, data=data, headers=self._headers) + if resp.status_code != requests.codes.ok: + msg = 'Error: {}'.format(resp.status_code) + print (msg) + return '' + + return resp.json() + + def _decrypt(self, repo_id, password): + if not self._headers: + return False + + url = '{}repos/{}/'.format(self._url, repo_id) + data = {'password': password} + resp = requests.post(url, data=data, headers=self._headers) + if resp.status_code != requests.codes.ok: + msg = 'Error: {}'.format(resp.status_code) + print (msg) + return False + + return True + + def upload_file(self, path_file, repo_id, relative_path, password=''): + if not self._headers: + return False + + if password: + if not self._decrypt(repo_id, password): + return False + + upload_link = self._get_upload_link(repo_id) + data = { + 'filename': path_file, + 'parent_dir': relative_path, + 'relative_path': '', + } + files = {'file': self._open(path_file)} + + resp = requests.post( + upload_link, data=data, files=files, headers=self._headers) + if resp.status_code != requests.codes.ok: + msg = 'Upload Code: {}\n{}'.format(resp.status_code, resp.text) + print (msg) + return False + + return True + + def _info_path(self, path): + path, filename = os.path.split(path) + return path, filename + + def list_directory(self, repo_id): + if not self._headers: + return False + + url = '{}repos/{}/dir/'.format(self._url, repo_id) + params = {'p': '/', 't': 'd', 'recursive': '1'} + try: + resp = requests.get(url, params=params, headers=self._headers) + return resp.json() + except Exception as e: + return False + + def create_dir(self, repo_id, name): + url = '{}repos/{}/dir/'.format(self._url, repo_id) + data = {'operation': 'mkdir'} + params = {'p': name} + resp = requests.post(url, data=data, headers=self._headers, params=params) + if resp.status_code != requests.codes.created: + msg = 'Create Dir Error: {}'.format(resp.status_code) + print (msg) + return False + + return True + + def _validate_directory(self, repo_id, target_file): + if target_file == '/': + return True + + names = target_file.split('/')[:-1] + directories = self.list_directory(repo_id) + + if isinstance(directories, bool): + return False + + exists = False + parent_dir = '/' + name_dir = '' + for name in names: + name_dir += '/' + name + for directory in directories: + if name == directory['name'] and parent_dir == directory['parent_dir']: + exists = True + break + if exists: + exists = False + else: + self.create_dir(repo_id, name_dir) + if parent_dir == '/': + parent_dir += name + else: + parent_dir += '/' + name + + return True + + def update_file(self, path_file, repo_id, target_file, password, create=True): + if not self._headers: + return False + + if not self._validate_directory(repo_id, target_file): + return False + + if password: + if not self._decrypt(repo_id, password): + return False + + update_link = self._get_update_link(repo_id) + _, filename = self._info_path(path_file) + files = { + 'file': (filename, self._open(path_file)), + 'filename': (None, filename), + 'target_file': (None, '{}{}'.format(target_file, filename)) + } + + resp = requests.post(update_link, files=files, headers=self._headers) + if resp.status_code != requests.codes.ok: + msg = 'Update Code: {}\n{}'.format(resp.status_code, resp.text) + print (msg) + if resp.status_code == self.FILE_DOES_NOT_EXIST and create: + relative_path = '/' + if target_file != '/': + relative_path += target_file + return self.upload_file(path_file, repo_id, relative_path, password) + else: + return False + + return True + + def ping(self): + url = self._url + 'ping/' + resp = requests.get(url) + return resp.json() + + + def auth_ping(self): + url = self._url + 'auth/ping/' + resp = requests.get(url, headers=self._headers) + return resp.json() + diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 85f2815..7f5c5c0 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -33,18 +33,13 @@ except: import pyqrcode from dateutil import parser -from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice +from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice, \ + SeaFileAPI from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \ PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PATH_MEDIA, PRE, \ PATH_XMLSEC, TEMPLATE_CANCEL, DEFAULT_SAT_PRODUCTO, DECIMALES - -#~ def _get_hash(password): - #~ return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() - - -#~ def validate_password(hashed, password): - #~ return bcrypt.hashpw(password.encode(), hashed.encode()) == hashed.encode() +from settings import SEAFILE_SERVER def _call(args): @@ -1389,6 +1384,92 @@ def crear_db(nombre): return False +def _to_seafile(path_db, data): + # ~ if DEBUG: + # ~ return + + _, filename = os.path.split(path_db) + if SEAFILE_SERVER: + msg = '\tSincronizando backup general...' + log.info(msg) + seafile = SeaFileAPI( + SEAFILE_SERVER['URL'], + SEAFILE_SERVER['USER'], + SEAFILE_SERVER['PASS']) + + if seafile.is_connect: + msg = '\tSincronizando: {} '.format(filename) + log.info(msg) + seafile.update_file( + path_db, SEAFILE_SERVER['REPO'], '/', SEAFILE_SERVER['PASS']) + msg = '\tRespaldo general de {} sincronizado'.format(filename) + log.info(msg) + + msg = '\tSin datos para sincronización particular de {}'.format(filename) + if len(data) < 2: + log.info(msg) + return + if not data[0] or not data[1] or not data[2]: + log.info(msg) + return + + msg = '\tSincronizando backup particular...' + log.info(msg) + seafile = SeaFileAPI(SEAFILE_SERVER['URL'], data[0], data[1]) + if seafile.is_connect: + msg = '\t\tSincronizando: {} '.format(filename) + log.info(msg) + seafile.update_file(path_db, data[2], 'Base de datos/', data[1]) + msg = '\t\tRespaldo partícular de {} sincronizado'.format(filename) + log.info(msg) + + return + + +@run_in_thread +def _backup_and_sync(rfc, data): + msg = 'Generando backup de: {}'.format(rfc) + log.info(msg) + + sql = 'select correo_timbrado, token_timbrado, token_soporte from emisor;' + path_bk = _join(PATH_MEDIA, 'tmp', '{}.bk'.format(rfc.lower())) + if data['type'] == 'postgres': + args = 'pg_dump -U postgres -Fc {} > "{}"'.format( + data['name'], path_bk) + sql = 'psql -U postgres -d {} -Atc "{}"'.format(data['name'], sql) + elif data['type'] == 'sqlite': + args = 'gzip -c "{}" > "{}"'.format(data['name'], path_bk) + sql = 'sqlite3 "{}" "{}"'.format(data['name'], sql) + try: + result = _call(args) + msg = '\tBackup generado de {}'.format(rfc) + log.info(msg) + result = _call(sql).strip().split('|') + _to_seafile(path_bk, result) + except Exception as e: + log.info(e) + + return + + +def backup_dbs(): + con = sqlite3.connect(COMPANIES) + cursor = con.cursor() + sql = "SELECT * FROM names" + cursor.execute(sql) + rows = cursor.fetchall() + if rows is None: + return + cursor.close() + con.close() + + for rfc, data in rows: + args = loads(data) + _backup_and_sync(rfc, args) + + return + + class ImportFacturaLibre(object): def __init__(self, path, rfc): @@ -1603,7 +1684,7 @@ class ImportFacturaLibre(object): '11': 0.11, '-10': 0.10, '0': 0.0, - '-2/3': 0.666667, + '-2/3': 0.106667, '-0.5': 0.005, } diff --git a/source/app/models/main.py b/source/app/models/main.py index 5f15c41..98a400a 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1719,7 +1719,6 @@ class Productos(BaseModel): @classmethod def get_by(cls, values): - # ~ id = int(values.get('id', 0)) clave = values.get('id', '') if clave: row = (Productos @@ -1732,7 +1731,8 @@ class Productos(BaseModel): Productos.valor_unitario, Productos.descuento) .join(SATUnidades).switch(Productos) - .where(Productos.clave==clave).dicts()) + .where((Productos.id==clave) | (Productos.clave==clave)) + .dicts()) if len(row): id = row[0]['id'] model_pt = Productos.impuestos.get_through_model() @@ -3003,6 +3003,7 @@ class PreFacturas(BaseModel): product['unidad'] = p.unidad.key product['clave'] = p.clave product['clave_sat'] = p.clave_sat + product['cuenta_predial'] = p.cuenta_predial product['factura'] = invoice.id product['producto'] = id_product @@ -3057,8 +3058,8 @@ class PreFacturas(BaseModel): invoice_tax = { 'factura': invoice.id, - 'impuesto': tax['id'], - 'base': tax['importe'], + 'impuesto': tax.id, + 'base': tax.importe, 'importe': import_tax, } PreFacturasImpuestos.create(**invoice_tax) @@ -3276,7 +3277,12 @@ class PreFacturasDetalle(BaseModel): producto = {} producto['noidentificacion'] = '{}\n(SAT {})'.format( p.producto.clave, p.producto.clave_sat) + producto['descripcion'] = p.descripcion + if p.cuenta_predial: + info = '\nCuenta Predial Número: {}'.format(p.cuenta_predial) + producto['descripcion'] += info + producto['unidad'] = '{}\n({})'.format( p.producto.unidad.name, p.producto.unidad.key) producto['cantidad'] = str(p.cantidad) @@ -4184,9 +4190,10 @@ help_lr = 'Listar RFCs' @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) +@click.option('-bk', '--backup-dbs', is_flag=True, default=False) def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, rfc, borrar_rfc, listar_rfc, importar_valores, archivo, factura_libre, test, - generar_archivo_productos, importar_productos): + generar_archivo_productos, importar_productos, backup_dbs): opt = locals() @@ -4278,6 +4285,9 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, rfc, _importar_productos(opt['archivo']) sys.exit(0) + if opt['backup_dbs']: + util.backup_dbs() + return diff --git a/source/app/settings.py b/source/app/settings.py index f5b88b1..db494d8 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -14,6 +14,11 @@ try: except ImportError: DEFAULT_PASSWORD = 'blades3.3' +try: + from conf import SEAFILE_SERVER +except ImportError: + SEAFILE_SERVER = {} + DEBUG = DEBUG VERSION = '0.2.1' diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 27a267e..20a2a77 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -148,7 +148,7 @@ var emisor_otros_datos= [ {view: 'text', id: 'token_timbrado', name: 'token_timbrado', label: 'Token de Timbrado: '}, {view: 'text', id: 'token_soporte', - name: 'token_soporte', label: 'Token de Soporte: '}, + name: 'token_soporte', label: 'Token para Respaldos: '}, ]