Merge branch 'develop'
402f54f
Sincrionizacion SeaFile partícular37aea44
Sincrionizacion con SeaFile generalf2c9de3
Backups in threadf99a0da
Generar backups de sqlite50a8c27
Generar backups de base de datos6644eef
Fix - Issue 48de675a0
Fix - Mostrar cuenta prefial en prefactura38f2b5a
Fix - Issue 47bb14b54
Fix - Issue 46
This commit is contained in:
commit
efec0c1396
|
@ -11,3 +11,15 @@ DEFAULT_PASSWORD = 'blades3.3'
|
||||||
|
|
||||||
#~ Establece una ruta accesible para el servidor web
|
#~ Establece una ruta accesible para el servidor web
|
||||||
LOG_PATH = '/srv/empresa/logs/empresalibre.log'
|
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 = {}
|
|
@ -12,6 +12,9 @@ from email.mime.text import MIMEText
|
||||||
from email import encoders
|
from email import encoders
|
||||||
from email.utils import formatdate
|
from email.utils import formatdate
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Image
|
from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Image
|
||||||
from reportlab.lib import colors
|
from reportlab.lib import colors
|
||||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
@ -724,3 +727,200 @@ class ReportTemplate(BaseDocTemplate):
|
||||||
return
|
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()
|
||||||
|
|
||||||
|
|
|
@ -33,18 +33,13 @@ except:
|
||||||
import pyqrcode
|
import pyqrcode
|
||||||
from dateutil import parser
|
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, \
|
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, DEFAULT_SAT_PRODUCTO, DECIMALES
|
PATH_XMLSEC, TEMPLATE_CANCEL, DEFAULT_SAT_PRODUCTO, DECIMALES
|
||||||
|
|
||||||
|
from settings import SEAFILE_SERVER
|
||||||
#~ 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()
|
|
||||||
|
|
||||||
|
|
||||||
def _call(args):
|
def _call(args):
|
||||||
|
@ -1389,6 +1384,92 @@ def crear_db(nombre):
|
||||||
return False
|
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):
|
class ImportFacturaLibre(object):
|
||||||
|
|
||||||
def __init__(self, path, rfc):
|
def __init__(self, path, rfc):
|
||||||
|
@ -1603,7 +1684,7 @@ class ImportFacturaLibre(object):
|
||||||
'11': 0.11,
|
'11': 0.11,
|
||||||
'-10': 0.10,
|
'-10': 0.10,
|
||||||
'0': 0.0,
|
'0': 0.0,
|
||||||
'-2/3': 0.666667,
|
'-2/3': 0.106667,
|
||||||
'-0.5': 0.005,
|
'-0.5': 0.005,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1719,7 +1719,6 @@ class Productos(BaseModel):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_by(cls, values):
|
def get_by(cls, values):
|
||||||
# ~ id = int(values.get('id', 0))
|
|
||||||
clave = values.get('id', '')
|
clave = values.get('id', '')
|
||||||
if clave:
|
if clave:
|
||||||
row = (Productos
|
row = (Productos
|
||||||
|
@ -1732,7 +1731,8 @@ class Productos(BaseModel):
|
||||||
Productos.valor_unitario,
|
Productos.valor_unitario,
|
||||||
Productos.descuento)
|
Productos.descuento)
|
||||||
.join(SATUnidades).switch(Productos)
|
.join(SATUnidades).switch(Productos)
|
||||||
.where(Productos.clave==clave).dicts())
|
.where((Productos.id==clave) | (Productos.clave==clave))
|
||||||
|
.dicts())
|
||||||
if len(row):
|
if len(row):
|
||||||
id = row[0]['id']
|
id = row[0]['id']
|
||||||
model_pt = Productos.impuestos.get_through_model()
|
model_pt = Productos.impuestos.get_through_model()
|
||||||
|
@ -3003,6 +3003,7 @@ class PreFacturas(BaseModel):
|
||||||
product['unidad'] = p.unidad.key
|
product['unidad'] = p.unidad.key
|
||||||
product['clave'] = p.clave
|
product['clave'] = p.clave
|
||||||
product['clave_sat'] = p.clave_sat
|
product['clave_sat'] = p.clave_sat
|
||||||
|
product['cuenta_predial'] = p.cuenta_predial
|
||||||
|
|
||||||
product['factura'] = invoice.id
|
product['factura'] = invoice.id
|
||||||
product['producto'] = id_product
|
product['producto'] = id_product
|
||||||
|
@ -3057,8 +3058,8 @@ class PreFacturas(BaseModel):
|
||||||
|
|
||||||
invoice_tax = {
|
invoice_tax = {
|
||||||
'factura': invoice.id,
|
'factura': invoice.id,
|
||||||
'impuesto': tax['id'],
|
'impuesto': tax.id,
|
||||||
'base': tax['importe'],
|
'base': tax.importe,
|
||||||
'importe': import_tax,
|
'importe': import_tax,
|
||||||
}
|
}
|
||||||
PreFacturasImpuestos.create(**invoice_tax)
|
PreFacturasImpuestos.create(**invoice_tax)
|
||||||
|
@ -3276,7 +3277,12 @@ class PreFacturasDetalle(BaseModel):
|
||||||
producto = {}
|
producto = {}
|
||||||
producto['noidentificacion'] = '{}\n(SAT {})'.format(
|
producto['noidentificacion'] = '{}\n(SAT {})'.format(
|
||||||
p.producto.clave, p.producto.clave_sat)
|
p.producto.clave, p.producto.clave_sat)
|
||||||
|
|
||||||
producto['descripcion'] = p.descripcion
|
producto['descripcion'] = p.descripcion
|
||||||
|
if p.cuenta_predial:
|
||||||
|
info = '\nCuenta Predial Número: {}'.format(p.cuenta_predial)
|
||||||
|
producto['descripcion'] += info
|
||||||
|
|
||||||
producto['unidad'] = '{}\n({})'.format(
|
producto['unidad'] = '{}\n({})'.format(
|
||||||
p.producto.unidad.name, p.producto.unidad.key)
|
p.producto.unidad.name, p.producto.unidad.key)
|
||||||
producto['cantidad'] = str(p.cantidad)
|
producto['cantidad'] = str(p.cantidad)
|
||||||
|
@ -4184,9 +4190,10 @@ help_lr = 'Listar RFCs'
|
||||||
@click.option('-t', '--test', 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('-gap', '--generar-archivo-productos', is_flag=True, default=False)
|
||||||
@click.option('-ip', '--importar-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,
|
def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, rfc,
|
||||||
borrar_rfc, listar_rfc, importar_valores, archivo, factura_libre, test,
|
borrar_rfc, listar_rfc, importar_valores, archivo, factura_libre, test,
|
||||||
generar_archivo_productos, importar_productos):
|
generar_archivo_productos, importar_productos, backup_dbs):
|
||||||
|
|
||||||
opt = locals()
|
opt = locals()
|
||||||
|
|
||||||
|
@ -4278,6 +4285,9 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, rfc,
|
||||||
_importar_productos(opt['archivo'])
|
_importar_productos(opt['archivo'])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
if opt['backup_dbs']:
|
||||||
|
util.backup_dbs()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,11 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
DEFAULT_PASSWORD = 'blades3.3'
|
DEFAULT_PASSWORD = 'blades3.3'
|
||||||
|
|
||||||
|
try:
|
||||||
|
from conf import SEAFILE_SERVER
|
||||||
|
except ImportError:
|
||||||
|
SEAFILE_SERVER = {}
|
||||||
|
|
||||||
|
|
||||||
DEBUG = DEBUG
|
DEBUG = DEBUG
|
||||||
VERSION = '0.2.1'
|
VERSION = '0.2.1'
|
||||||
|
|
|
@ -148,7 +148,7 @@ var emisor_otros_datos= [
|
||||||
{view: 'text', id: 'token_timbrado',
|
{view: 'text', id: 'token_timbrado',
|
||||||
name: 'token_timbrado', label: 'Token de Timbrado: '},
|
name: 'token_timbrado', label: 'Token de Timbrado: '},
|
||||||
{view: 'text', id: 'token_soporte',
|
{view: 'text', id: 'token_soporte',
|
||||||
name: 'token_soporte', label: 'Token de Soporte: '},
|
name: 'token_soporte', label: 'Token para Respaldos: '},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue