This commit is contained in:
Mauricio Baeza 2020-01-07 00:32:48 -06:00
parent cff2294601
commit e1b7e525df
17 changed files with 841 additions and 41 deletions

View File

@ -1 +1 @@
1.31.2
1.32.0

View File

@ -1,4 +1,4 @@
falcon
falcon==1.4.1
falcon-multipart
Beaker
Mako

View File

@ -311,6 +311,12 @@ class AppInvoices(object):
req.context['result'] = self._db.invoice(values, session['userobj'])
resp.status = falcon.HTTP_200
def on_put(self, req, resp):
values = req.params
session = req.env['beaker.session']
req.context['result'] = self._db.invoice_put(values, session['userobj'])
resp.status = falcon.HTTP_200
def on_delete(self, req, resp):
values = req.params
session = req.env['beaker.session']

View File

@ -19,9 +19,17 @@
import base64
import collections
import datetime
import json
import logging
import math
import os
import shutil
import smtplib
import sqlite3
import subprocess
import threading
import zipfile
from pathlib import Path
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
@ -38,8 +46,21 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from dateutil import parser
import seafileapi
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
LOG_DATE = '%d/%m/%Y %H:%M:%S'
logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m')
logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m')
logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m')
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE)
log = logging.getLogger(__name__)
logging.getLogger('peewee').setLevel(logging.WARNING)
TIMEOUT = 10
PATH_INVOICES = 'facturas'
#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
@ -233,6 +254,23 @@ class CfdiToDict(object):
self._values.update({'divisas': d})
return
def _call(args):
return subprocess.check_output(args, shell=True).decode()
def _join(*paths):
return os.path.join(*paths)
def run_in_thread(fn):
def run(*k, **kw):
t = threading.Thread(target=fn, args=k, kwargs=kw)
t.start()
return t
return run
def send_mail(data):
msg = ''
ok = True
@ -293,3 +331,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
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
return
def _get_pass(rfc):
return rfc
def _backup_db(rfc, data, path_bk, is_mv, url_seafile):
if data['type'] != 'postgres':
return
log.info('Generando backup de: {}'.format(rfc))
bk_name = '{}.bk'.format(rfc.lower())
path_db = _join(path_bk, bk_name)
args = 'pg_dump -U postgres -Fc {} > "{}"'.format(data['name'], path_db)
result = _call(args)
log.info('\tBackup local generado...')
if is_mv:
path_target = _join(Path.home(), PATH_INVOICES)
if Path(path_target).exists():
path_target = _join(path_target, bk_name)
shutil.copy(path_db, path_target)
else:
log.error('\tNo existe la carpeta compartida...')
sql = 'select correo_timbrado, token_soporte from emisor;'
args = 'psql -U postgres -d {} -Atc "{}"'.format(data['name'], sql)
result = _call(args)
if not result:
log.error('\tSin datos para backup remoto')
return
data = result.strip().split('|')
if not data[1]:
log.error('\tSin token de soporte')
return
# ~ email = data[0]
# ~ uuid = data[1]
# ~ email = 'hola@elmau.net'
# ~ uuid = 'cc42c591-cf66-499a-ae70-c09df5646be9'
# ~ log.debug(url_seafile, email, _get_pass(rfc))
# ~ client = seafileapi.connect(url_seafile, email, _get_pass(rfc))
# ~ repo = client.repos.get_repo(uuid)
# ~ print(repo)
return
def db_backup(path_companies, path_bk, is_mv, url_seafile):
con = sqlite3.connect(path_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:
_backup_db(rfc, json.loads(data), path_bk, is_mv, url_seafile)
return
def now():
return datetime.datetime.now().replace(microsecond=0)
def get_days(date):
return (now() - date).days

View File

@ -124,8 +124,8 @@ class StorageEngine(object):
# ~ def _get_cancelinvoice(self, values):
# ~ return main.Facturas.cancel(values['id'])
def _get_statussat(self, values):
return main.Facturas.get_status_sat(values['id'])
# ~ def _get_statussat(self, values):
# ~ return main.Facturas.get_status_sat(values['id'])
def _get_verifysat(self, values):
return main.Facturas.get_verify_sat(values['id'])
@ -349,6 +349,9 @@ class StorageEngine(object):
return main.Facturas.add(values, user)
def invoice_put(self, values, user):
return main.Facturas.put(values, user)
def preinvoice(self, values):
id = int(values.pop('id', '0'))
#~ if id:

View File

@ -29,22 +29,23 @@ if __name__ == '__main__':
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, parent_dir)
# ~ v2
from controllers import utils
# ~ v1
from controllers import util
from settings import log, DEBUG, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \
from settings import log, DEBUG, 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, \
DEFAULT_CFDIPAY, CURRENCY_MN
# ~ v2
from controllers import utils
from settings import (
DB_COMPANIES,
EXT,
IS_MV,
MXN,
PATHS,
URL,
VALUES_PDF,
)
@ -75,7 +76,8 @@ def conectar(opt):
if not db_type in db:
log.error('Tipo de base de datos no soportado')
return False
#~ print ('DB NAME', db_name)
opt['host'] = opt.get('host', 'localhost')
database = db[db_type](db_name, **opt)
try:
database_proxy.initialize(database)
@ -4284,6 +4286,9 @@ class Facturas(BaseModel):
if values['opt'] == 'detalle':
return FacturasDetalle.get_detalle(int(values['id']))
if values['opt'] == 'statussat':
return self.get_status_sat(int(values['id']))
cfdis = util.loads(values['cfdis'])
if values['year'] == '-1':
@ -4907,6 +4912,12 @@ class Facturas(BaseModel):
if obj.estatus_sat != estatus_sat:
obj.estatus_sat = estatus_sat
obj.save()
if obj.estatus_sat == 'Vigente' and obj.estatus == 'Cancelada':
days = utils.get_days(obj.fecha_cancelacion)
if days > 3:
estatus_sat = 'uncancel'
return estatus_sat
@classmethod
@ -4964,21 +4975,6 @@ class Facturas(BaseModel):
}
return
@util.run_in_thread
def _update_inventory(self, invoice, cancel=False):
if invoice.tipo_comprobante != 'I':
return
products = FacturasDetalle.get_by_invoice(invoice.id)
for p in products:
if p.producto.inventario:
if cancel:
p.producto.existencia += Decimal(p.cantidad)
else:
p.producto.existencia -= Decimal(p.cantidad)
p.producto.save()
return
@classmethod
def timbrar(cls, values, user):
id = int(values['id'])
@ -5278,15 +5274,64 @@ class Facturas(BaseModel):
return {'ok': False}
# ~ v2
@classmethod
def uncancel(cls, id):
obj = Facturas.get(Facturas.id==id)
if obj.uuid is None:
msg = 'La factura no esta timbrada'
return {'ok': False, 'msg': msg}
@utils.run_in_thread
def _update_inventory(self, invoice, cancel=False):
if invoice.tipo_comprobante != 'I':
return
products = FacturasDetalle.get_by_invoice(invoice.id)
for p in products:
if p.producto.inventario:
if cancel:
p.producto.existencia += Decimal(p.cantidad)
else:
p.producto.existencia -= Decimal(p.cantidad)
p.producto.save()
return
@utils.run_in_thread
def _update_client_balance(self, invoice, cancel=False):
if invoice.tipo_comprobante == 'T':
return
if invoice.donativo and invoice.forma_pago == '12':
return
if invoice.cliente.rfc == RFC_PUBLICO:
return
importe = invoice.total_mn
if invoice.tipo_comprobante == 'E':
importe *= -1
if cancel:
importe *= -1
q = (Socios
.update(saldo_cliente=Socios.saldo_cliente + importe)
.where(Socios.id==invoice.cliente.id)
)
return bool(q.execute())
def _put_uncancel(self, args, user):
id = int(args['id'])
obj = Facturas.get(Facturas.id==id)
obj.estatus = 'Timbrada'
obj.error = ''
obj.cancelada = False
obj.fecha_cancelacion = None
obj.acuse = ''
self._update_client_balance(self, obj)
self._update_inventory(self, obj)
obj.save()
msg = 'Factura actualizada correctamente'
result = {'result': True, 'msg': msg, 'values': {'estatus': 'Timbrada'}}
return result
@classmethod
def put(cls, args, user):
return getattr(cls, f"_put_{args['opt']}")(cls, args, user)
class PreFacturas(BaseModel):
cliente = ForeignKeyField(Socios)
@ -9416,7 +9461,8 @@ def empresa_agregar(rfc, no_bd):
def empresa_borrar(rfc):
if _delete_emisor(rfc):
util.delete_db(rfc.lower())
# ~ util.delete_db(rfc.lower())
utils.db_delete(rfc.lower(), PATHS['DOCS'])
return True
@ -10195,7 +10241,8 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña,
sys.exit(0)
if opt['backup_dbs']:
util.backup_dbs()
# ~ util.backup_dbs()
utils.db_backup(DB_COMPANIES, PATHS['BK'], IS_MV, URL['SEAFILE'])
sys.exit(0)
if opt['exportar_documentos']:

View File

@ -0,0 +1,5 @@
from seafileapi.client import SeafileApiClient
def connect(server, username, password):
client = SeafileApiClient(server, username, password)
return client

View File

@ -0,0 +1,7 @@
class SeafileAdmin(object):
def lists_users(self, maxcount=100):
pass
def list_user_repos(self, username):
pass

View File

@ -0,0 +1,77 @@
import requests
from seafileapi.utils import urljoin
from seafileapi.exceptions import ClientHttpError
from seafileapi.repos import Repos
class SeafileApiClient(object):
"""Wraps seafile web api"""
def __init__(self, server, username=None, password=None, token=None):
"""Wraps various basic operations to interact with seahub http api.
"""
self.server = server
self.username = username
self.password = password
self._token = token
self.repos = Repos(self)
self.groups = Groups(self)
if token is None:
self._get_token()
def _get_token(self):
data = {
'username': self.username,
'password': self.password,
}
url = urljoin(self.server, '/api2/auth-token/')
res = requests.post(url, data=data)
if res.status_code != 200:
raise ClientHttpError(res.status_code, res.content)
token = res.json()['token']
assert len(token) == 40, 'The length of seahub api auth token should be 40'
self._token = token
def __str__(self):
return 'SeafileApiClient[server=%s, user=%s]' % (self.server, self.username)
__repr__ = __str__
def get(self, *args, **kwargs):
return self._send_request('GET', *args, **kwargs)
def post(self, *args, **kwargs):
return self._send_request('POST', *args, **kwargs)
def put(self, *args, **kwargs):
return self._send_request('PUT', *args, **kwargs)
def delete(self, *args, **kwargs):
return self._send_request('delete', *args, **kwargs)
def _send_request(self, method, url, *args, **kwargs):
if not url.startswith('http'):
url = urljoin(self.server, url)
headers = kwargs.get('headers', {})
headers.setdefault('Authorization', 'Token ' + self._token)
kwargs['headers'] = headers
expected = kwargs.pop('expected', 200)
if not hasattr(expected, '__iter__'):
expected = (expected, )
resp = requests.request(method, url, *args, **kwargs)
if resp.status_code not in expected:
msg = 'Expected %s, but get %s' % \
(' or '.join(map(str, expected)), resp.status_code)
raise ClientHttpError(resp.status_code, msg)
return resp
class Groups(object):
def __init__(self, client):
pass
def create_group(self, name):
pass

View File

@ -0,0 +1,25 @@
class ClientHttpError(Exception):
"""This exception is raised if the returned http response is not as
expected"""
def __init__(self, code, message):
super(ClientHttpError, self).__init__()
self.code = code
self.message = message
def __str__(self):
return 'ClientHttpError[%s: %s]' % (self.code, self.message)
class OperationError(Exception):
"""Expcetion to raise when an opeartion is failed"""
pass
class DoesNotExist(Exception):
"""Raised when not matching resource can be found."""
def __init__(self, msg):
super(DoesNotExist, self).__init__()
self.msg = msg
def __str__(self):
return 'DoesNotExist: %s' % self.msg

View File

@ -0,0 +1,250 @@
import io
import os
import posixpath
import re
from seafileapi.utils import querystr
ZERO_OBJ_ID = '0000000000000000000000000000000000000000'
class _SeafDirentBase(object):
"""Base class for :class:`SeafFile` and :class:`SeafDir`.
It provides implementation of their common operations.
"""
isdir = None
def __init__(self, repo, path, object_id, size=0):
"""
:param:`path` the full path of this entry within its repo, like
"/documents/example.md"
:param:`size` The size of a file. It should be zero for a dir.
"""
self.client = repo.client
self.repo = repo
self.path = path
self.id = object_id
self.size = size
@property
def name(self):
return posixpath.basename(self.path)
def list_revisions(self):
pass
def delete(self):
suffix = 'dir' if self.isdir else 'file'
url = '/api2/repos/%s/%s/' % (self.repo.id, suffix) + querystr(p=self.path)
resp = self.client.delete(url)
return resp
def rename(self, newname):
"""Change file/folder name to newname
"""
suffix = 'dir' if self.isdir else 'file'
url = '/api2/repos/%s/%s/' % (self.repo.id, suffix) + querystr(p=self.path, reloaddir='true')
postdata = {'operation': 'rename', 'newname': newname}
resp = self.client.post(url, data=postdata)
succeeded = resp.status_code == 200
if succeeded:
if self.isdir:
new_dirent = self.repo.get_dir(os.path.join(os.path.dirname(self.path), newname))
else:
new_dirent = self.repo.get_file(os.path.join(os.path.dirname(self.path), newname))
for key in list(self.__dict__.keys()):
self.__dict__[key] = new_dirent.__dict__[key]
return succeeded
def _copy_move_task(self, operation, dirent_type, dst_dir, dst_repo_id=None):
url = '/api/v2.1/copy-move-task/'
src_repo_id = self.repo.id
src_parent_dir = os.path.dirname(self.path)
src_dirent_name = os.path.basename(self.path)
dst_repo_id = dst_repo_id
dst_parent_dir = dst_dir
operation = operation
dirent_type = dirent_type
postdata = {'src_repo_id': src_repo_id, 'src_parent_dir': src_parent_dir,
'src_dirent_name': src_dirent_name, 'dst_repo_id': dst_repo_id,
'dst_parent_dir': dst_parent_dir, 'operation': operation,
'dirent_type': dirent_type}
return self.client.post(url, data=postdata)
def copyTo(self, dst_dir, dst_repo_id=None):
"""Copy file/folder to other directory (also to a different repo)
"""
if dst_repo_id is None:
dst_repo_id = self.repo.id
dirent_type = 'dir' if self.isdir else 'file'
resp = self._copy_move_task('copy', dirent_type, dst_dir, dst_repo_id)
return resp.status_code == 200
def moveTo(self, dst_dir, dst_repo_id=None):
"""Move file/folder to other directory (also to a different repo)
"""
if dst_repo_id is None:
dst_repo_id = self.repo.id
dirent_type = 'dir' if self.isdir else 'file'
resp = self._copy_move_task('move', dirent_type, dst_dir, dst_repo_id)
succeeded = resp.status_code == 200
if succeeded:
new_repo = self.client.repos.get_repo(dst_repo_id)
dst_path = os.path.join(dst_dir, os.path.basename(self.path))
if self.isdir:
new_dirent = new_repo.get_dir(dst_path)
else:
new_dirent = new_repo.get_file(dst_path)
for key in list(self.__dict__.keys()):
self.__dict__[key] = new_dirent.__dict__[key]
return succeeded
def get_share_link(self):
pass
class SeafDir(_SeafDirentBase):
isdir = True
def __init__(self, *args, **kwargs):
super(SeafDir, self).__init__(*args, **kwargs)
self.entries = None
self.entries = kwargs.pop('entries', None)
def ls(self, force_refresh=False):
"""List the entries in this dir.
Return a list of objects of class :class:`SeafFile` or :class:`SeafDir`.
"""
if self.entries is None or force_refresh:
self.load_entries()
return self.entries
def share_to_user(self, email, permission):
url = '/api2/repos/%s/dir/shared_items/' % self.repo.id + querystr(p=self.path)
putdata = {
'share_type': 'user',
'username': email,
'permission': permission
}
resp = self.client.put(url, data=putdata)
return resp.status_code == 200
def create_empty_file(self, name):
"""Create a new empty file in this dir.
Return a :class:`SeafFile` object of the newly created file.
"""
# TODO: file name validation
path = posixpath.join(self.path, name)
url = '/api2/repos/%s/file/' % self.repo.id + querystr(p=path, reloaddir='true')
postdata = {'operation': 'create'}
resp = self.client.post(url, data=postdata)
self.id = resp.headers['oid']
self.load_entries(resp.json())
return SeafFile(self.repo, path, ZERO_OBJ_ID, 0)
def mkdir(self, name):
"""Create a new sub folder right under this dir.
Return a :class:`SeafDir` object of the newly created sub folder.
"""
path = posixpath.join(self.path, name)
url = '/api2/repos/%s/dir/' % self.repo.id + querystr(p=path, reloaddir='true')
postdata = {'operation': 'mkdir'}
resp = self.client.post(url, data=postdata)
self.id = resp.headers['oid']
self.load_entries(resp.json())
return SeafDir(self.repo, path, ZERO_OBJ_ID)
def upload(self, fileobj, filename):
"""Upload a file to this folder.
:param:fileobj :class:`File` like object
:param:filename The name of the file
Return a :class:`SeafFile` object of the newly uploaded file.
"""
if isinstance(fileobj, str):
fileobj = io.BytesIO(fileobj)
upload_url = self._get_upload_link()
files = {
'file': (filename, fileobj),
'parent_dir': self.path,
}
self.client.post(upload_url, files=files)
return self.repo.get_file(posixpath.join(self.path, filename))
def upload_local_file(self, filepath, name=None):
"""Upload a file to this folder.
:param:filepath The path to the local file
:param:name The name of this new file. If None, the name of the local file would be used.
Return a :class:`SeafFile` object of the newly uploaded file.
"""
name = name or os.path.basename(filepath)
with open(filepath, 'r') as fp:
return self.upload(fp, name)
def _get_upload_link(self):
url = '/api2/repos/%s/upload-link/' % self.repo.id
resp = self.client.get(url)
return re.match(r'"(.*)"', resp.text).group(1)
def get_uploadable_sharelink(self):
"""Generate a uploadable shared link to this dir.
Return the url of this link.
"""
pass
def load_entries(self, dirents_json=None):
if dirents_json is None:
url = '/api2/repos/%s/dir/' % self.repo.id + querystr(p=self.path)
dirents_json = self.client.get(url).json()
self.entries = [self._load_dirent(entry_json) for entry_json in dirents_json]
def _load_dirent(self, dirent_json):
path = posixpath.join(self.path, dirent_json['name'])
if dirent_json['type'] == 'file':
return SeafFile(self.repo, path, dirent_json['id'], dirent_json['size'])
else:
return SeafDir(self.repo, path, dirent_json['id'], 0)
@property
def num_entries(self):
if self.entries is None:
self.load_entries()
return len(self.entries) if self.entries is not None else 0
def __str__(self):
return 'SeafDir[repo=%s,path=%s,entries=%s]' % \
(self.repo.id[:6], self.path, self.num_entries)
__repr__ = __str__
class SeafFile(_SeafDirentBase):
isdir = False
def update(self, fileobj):
"""Update the content of this file"""
pass
def __str__(self):
return 'SeafFile[repo=%s,path=%s,size=%s]' % \
(self.repo.id[:6], self.path, self.size)
def _get_download_link(self):
url = '/api2/repos/%s/file/' % self.repo.id + querystr(p=self.path)
resp = self.client.get(url)
return re.match(r'"(.*)"', resp.text).group(1)
def get_content(self):
"""Get the content of the file"""
url = self._get_download_link()
return self.client.get(url).content
__repr__ = __str__

View File

@ -0,0 +1,22 @@
class Group(object):
def __init__(self, client, group_id, group_name):
self.client = client
self.group_id = group_id
self.group_name = group_name
def list_memebers(self):
pass
def delete(self):
pass
def add_member(self, username):
pass
def remove_member(self, username):
pass
def list_group_repos(self):
pass

View File

@ -0,0 +1,99 @@
from urllib.parse import urlencode
from seafileapi.files import SeafDir, SeafFile
from seafileapi.utils import raise_does_not_exist
class Repo(object):
"""
A seafile library
"""
def __init__(self, client, repo_id, repo_name,
encrypted, owner, perm):
self.client = client
self.id = repo_id
self.name = repo_name
self.encrypted = encrypted
self.owner = owner
self.perm = perm
@classmethod
def from_json(cls, client, repo_json):
repo_id = repo_json['id']
repo_name = repo_json['name']
encrypted = repo_json['encrypted']
perm = repo_json['permission']
owner = repo_json['owner']
return cls(client, repo_id, repo_name, encrypted, owner, perm)
def is_readonly(self):
return 'w' not in self.perm
@raise_does_not_exist('The requested file does not exist')
def get_file(self, path):
"""Get the file object located in `path` in this repo.
Return a :class:`SeafFile` object
"""
assert path.startswith('/')
url = '/api2/repos/%s/file/detail/' % self.id
query = '?' + urlencode(dict(p=path))
file_json = self.client.get(url + query).json()
return SeafFile(self, path, file_json['id'], file_json['size'])
@raise_does_not_exist('The requested dir does not exist')
def get_dir(self, path):
"""Get the dir object located in `path` in this repo.
Return a :class:`SeafDir` object
"""
assert path.startswith('/')
url = '/api2/repos/%s/dir/' % self.id
query = '?' + urlencode(dict(p=path))
resp = self.client.get(url + query)
dir_id = resp.headers['oid']
dir_json = resp.json()
dir = SeafDir(self, path, dir_id)
dir.load_entries(dir_json)
return dir
def delete(self):
"""Remove this repo. Only the repo owner can do this"""
self.client.delete('/api2/repos/' + self.id)
def list_history(self):
"""List the history of this repo
Returns a list of :class:`RepoRevision` object.
"""
pass
## Operations only the repo owner can do:
def update(self, name=None):
"""Update the name of this repo. Only the repo owner can do
this.
"""
pass
def get_settings(self):
"""Get the settings of this repo. Returns a dict containing the following
keys:
`history_limit`: How many days of repo history to keep.
"""
pass
def restore(self, commit_id):
pass
class RepoRevision(object):
def __init__(self, client, repo, commit_id):
self.client = client
self.repo = repo
self.commit_id = commit_id
def restore(self):
"""Restore the repo to this revision"""
self.repo.revert(self.commit_id)

View File

@ -0,0 +1,26 @@
from seafileapi.repo import Repo
from seafileapi.utils import raise_does_not_exist
class Repos(object):
def __init__(self, client):
self.client = client
def create_repo(self, name, password=None):
data = {'name': name}
if password:
data['passwd'] = password
repo_json = self.client.post('/api2/repos/', data=data).json()
return self.get_repo(repo_json['repo_id'])
@raise_does_not_exist('The requested library does not exist')
def get_repo(self, repo_id):
"""Get the repo which has the id `repo_id`.
Raises :exc:`DoesNotExist` if no such repo exists.
"""
repo_json = self.client.get('/api2/repos/' + repo_id).json()
return Repo.from_json(self.client, repo_json)
def list_repos(self):
repos_json = self.client.get('/api2/repos/').json()
return [Repo.from_json(self.client, j) for j in repos_json]

View File

@ -0,0 +1,57 @@
import string
import random
from functools import wraps
from urllib.parse import urlencode
from seafileapi.exceptions import ClientHttpError, DoesNotExist
def randstring(length=0):
if length == 0:
length = random.randint(1, 30)
return ''.join(random.choice(string.lowercase) for i in range(length))
def urljoin(base, *args):
url = base
if url[-1] != '/':
url += '/'
for arg in args:
arg = arg.strip('/')
url += arg + '/'
if '?' in url:
url = url[:-1]
return url
def raise_does_not_exist(msg):
"""Decorator to turn a function that get a http 404 response to a
:exc:`DoesNotExist` exception."""
def decorator(func):
@wraps(func)
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
except ClientHttpError as e:
if e.code == 404:
raise DoesNotExist(msg)
else:
raise
return wrapped
return decorator
def to_utf8(obj):
if isinstance(obj, str):
return obj.encode('utf-8')
return obj
def querystr(**kwargs):
return '?' + urlencode(kwargs)
def utf8lize(obj):
if isinstance(obj, dict):
return {k: to_utf8(v) for k, v in obj.items()}
if isinstance(obj, list):
return [to_utf8(x) for x in ob]
if instance(obj, str):
return obj.encode('utf-8')
return obj

View File

@ -47,11 +47,12 @@ except ImportError:
DEBUG = DEBUG
VERSION = '1.31.2'
VERSION = '1.32.0'
EMAIL_SUPPORT = ('soporte@empresalibre.mx',)
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db'))
path_static = os.path.abspath(os.path.join(BASE_DIR, '..', 'static'))
path_docs = os.path.abspath(os.path.join(BASE_DIR, '..', 'docs'))
@ -196,6 +197,10 @@ API = 'https://api.empresalibre.net{}'
CURRENCY_MN = 'MXN'
# ~ v2
IS_MV = MV
DB_COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db'))
path_bk = os.path.join(path_docs, 'tmp')
EXT = {
'CSS': 'css',
'HTML': 'html',
@ -204,6 +209,7 @@ EXT = {
'JSON': 'json',
}
MXN = 'MXN'
PATHS = {
'STATIC': path_static,
'CSS': path_css,
@ -211,7 +217,9 @@ PATHS = {
'DOCS': path_docs,
'USER': path_user_template,
'LOGOS': path_user_logos,
'BK': path_bk,
}
VALUES_PDF = {
'CANCEL': {True: 'inline', False: 'none'},
'TYPE': {'I': 'Ingreso', 'E': 'Egreso', 'T': 'Traslado'},
@ -221,3 +229,7 @@ VALUES_PDF = {
'PPD': 'Pago en parcialidades o diferido',
},
}
URL = {
'SEAFILE': 'https://seafile.elmau.net',
}

View File

@ -508,7 +508,6 @@ function generar_anticipo_egreso(id){
function send_timbrar(id){
//~ webix.ajax().get('/values/timbrar', {id: id}, function(text, data){
webix.ajax().post('invoices', {opt: 'timbrar', id: id}, function(text, data){
var values = data.json()
if(values.ok){
@ -1322,7 +1321,6 @@ function grid_invoices_click(id, e, node){
function send_cancel(id){
//~ webix.ajax().get('/values/cancelinvoice', {id: id}, function(text, data){
webix.ajax().post('invoices', {opt: 'cancel', id: id}, function(text, data){
var values = data.json()
if(values.ok){
@ -1338,6 +1336,7 @@ function send_cancel(id){
})
}
function cmd_invoice_cancelar_click(){
if(gi.count() == 0){
return
@ -1435,6 +1434,37 @@ function filter_dates_change(range){
}
function invoice_uncancel(id){
var options = {opt: 'uncancel', id: id}
webix.ajax().put('/invoices', options, function(text, data){
var result = data.json()
if(result['result']){
gi.updateItem(id, result['values'])
msg_ok(result['msg'])
}else{
msg_error(result['msg'])
}
})
}
function ask_invoice_uncancel(id){
var msg = 'La factura seleccionada esta Vigente en el SAT, pero Cancelada en el sistema <BR>¿Deseas cambiar su estatus?'
webix.confirm({
title: 'Cambiar estatus de factura',
ok: 'Si',
cancel: 'No',
type: 'confirm-error',
text: msg,
callback:function(result){
if (result){
invoice_uncancel(id)
}
}
})
}
function cmd_invoice_sat_click(){
if(gi.count() == 0){
return
@ -1456,12 +1486,15 @@ function cmd_invoice_sat_click(){
return
}
webix.ajax().get('/values/statussat', {id: row.id}, function(text, data){
var values = data.json()
if(values == 'Vigente'){
msg_ok(values)
var options = {opt: 'statussat', id: row.id}
webix.ajax().get('/invoices', options, function(text, data){
var value = data.json()
if(value == 'Vigente'){
msg_ok(value)
}else if(value == 'uncancel'){
ask_invoice_uncancel(row.id)
}else{
msg_error(values)
msg_error(value)
}
})