Fix #355
This commit is contained in:
parent
cff2294601
commit
e1b7e525df
|
@ -1,4 +1,4 @@
|
||||||
falcon
|
falcon==1.4.1
|
||||||
falcon-multipart
|
falcon-multipart
|
||||||
Beaker
|
Beaker
|
||||||
Mako
|
Mako
|
||||||
|
|
|
@ -311,6 +311,12 @@ class AppInvoices(object):
|
||||||
req.context['result'] = self._db.invoice(values, session['userobj'])
|
req.context['result'] = self._db.invoice(values, session['userobj'])
|
||||||
resp.status = falcon.HTTP_200
|
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):
|
def on_delete(self, req, resp):
|
||||||
values = req.params
|
values = req.params
|
||||||
session = req.env['beaker.session']
|
session = req.env['beaker.session']
|
||||||
|
|
|
@ -19,9 +19,17 @@
|
||||||
import base64
|
import base64
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
import smtplib
|
import smtplib
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.base import MIMEBase
|
from email.mime.base import MIMEBase
|
||||||
|
@ -38,8 +46,21 @@ from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
from dateutil import parser
|
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
|
TIMEOUT = 10
|
||||||
|
PATH_INVOICES = 'facturas'
|
||||||
|
|
||||||
|
|
||||||
#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
|
#~ 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})
|
self._values.update({'divisas': d})
|
||||||
return
|
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):
|
def send_mail(data):
|
||||||
msg = ''
|
msg = ''
|
||||||
ok = True
|
ok = True
|
||||||
|
@ -293,3 +331,96 @@ def to_zip(files):
|
||||||
|
|
||||||
return zip_buffer.getvalue()
|
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
|
||||||
|
|
|
@ -124,8 +124,8 @@ class StorageEngine(object):
|
||||||
# ~ def _get_cancelinvoice(self, values):
|
# ~ def _get_cancelinvoice(self, values):
|
||||||
# ~ return main.Facturas.cancel(values['id'])
|
# ~ return main.Facturas.cancel(values['id'])
|
||||||
|
|
||||||
def _get_statussat(self, values):
|
# ~ def _get_statussat(self, values):
|
||||||
return main.Facturas.get_status_sat(values['id'])
|
# ~ return main.Facturas.get_status_sat(values['id'])
|
||||||
|
|
||||||
def _get_verifysat(self, values):
|
def _get_verifysat(self, values):
|
||||||
return main.Facturas.get_verify_sat(values['id'])
|
return main.Facturas.get_verify_sat(values['id'])
|
||||||
|
@ -349,6 +349,9 @@ class StorageEngine(object):
|
||||||
|
|
||||||
return main.Facturas.add(values, user)
|
return main.Facturas.add(values, user)
|
||||||
|
|
||||||
|
def invoice_put(self, values, user):
|
||||||
|
return main.Facturas.put(values, user)
|
||||||
|
|
||||||
def preinvoice(self, values):
|
def preinvoice(self, values):
|
||||||
id = int(values.pop('id', '0'))
|
id = int(values.pop('id', '0'))
|
||||||
#~ if id:
|
#~ if id:
|
||||||
|
|
|
@ -29,22 +29,23 @@ if __name__ == '__main__':
|
||||||
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
# ~ v2
|
|
||||||
from controllers import utils
|
|
||||||
|
|
||||||
# ~ v1
|
|
||||||
from controllers import util
|
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, \
|
INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \
|
||||||
CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \
|
CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \
|
||||||
DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \
|
DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \
|
||||||
DEFAULT_CFDIPAY, CURRENCY_MN
|
DEFAULT_CFDIPAY, CURRENCY_MN
|
||||||
|
|
||||||
|
# ~ v2
|
||||||
|
from controllers import utils
|
||||||
from settings import (
|
from settings import (
|
||||||
|
DB_COMPANIES,
|
||||||
EXT,
|
EXT,
|
||||||
|
IS_MV,
|
||||||
MXN,
|
MXN,
|
||||||
PATHS,
|
PATHS,
|
||||||
|
URL,
|
||||||
VALUES_PDF,
|
VALUES_PDF,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -75,7 +76,8 @@ def conectar(opt):
|
||||||
if not db_type in db:
|
if not db_type in db:
|
||||||
log.error('Tipo de base de datos no soportado')
|
log.error('Tipo de base de datos no soportado')
|
||||||
return False
|
return False
|
||||||
#~ print ('DB NAME', db_name)
|
|
||||||
|
opt['host'] = opt.get('host', 'localhost')
|
||||||
database = db[db_type](db_name, **opt)
|
database = db[db_type](db_name, **opt)
|
||||||
try:
|
try:
|
||||||
database_proxy.initialize(database)
|
database_proxy.initialize(database)
|
||||||
|
@ -4284,6 +4286,9 @@ class Facturas(BaseModel):
|
||||||
if values['opt'] == 'detalle':
|
if values['opt'] == 'detalle':
|
||||||
return FacturasDetalle.get_detalle(int(values['id']))
|
return FacturasDetalle.get_detalle(int(values['id']))
|
||||||
|
|
||||||
|
if values['opt'] == 'statussat':
|
||||||
|
return self.get_status_sat(int(values['id']))
|
||||||
|
|
||||||
cfdis = util.loads(values['cfdis'])
|
cfdis = util.loads(values['cfdis'])
|
||||||
|
|
||||||
if values['year'] == '-1':
|
if values['year'] == '-1':
|
||||||
|
@ -4907,6 +4912,12 @@ class Facturas(BaseModel):
|
||||||
if obj.estatus_sat != estatus_sat:
|
if obj.estatus_sat != estatus_sat:
|
||||||
obj.estatus_sat = estatus_sat
|
obj.estatus_sat = estatus_sat
|
||||||
obj.save()
|
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
|
return estatus_sat
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -4964,21 +4975,6 @@ class Facturas(BaseModel):
|
||||||
}
|
}
|
||||||
return
|
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
|
@classmethod
|
||||||
def timbrar(cls, values, user):
|
def timbrar(cls, values, user):
|
||||||
id = int(values['id'])
|
id = int(values['id'])
|
||||||
|
@ -5278,15 +5274,64 @@ class Facturas(BaseModel):
|
||||||
return {'ok': False}
|
return {'ok': False}
|
||||||
|
|
||||||
# ~ v2
|
# ~ v2
|
||||||
@classmethod
|
@utils.run_in_thread
|
||||||
def uncancel(cls, id):
|
def _update_inventory(self, invoice, cancel=False):
|
||||||
obj = Facturas.get(Facturas.id==id)
|
if invoice.tipo_comprobante != 'I':
|
||||||
if obj.uuid is None:
|
return
|
||||||
msg = 'La factura no esta timbrada'
|
|
||||||
return {'ok': False, 'msg': msg}
|
|
||||||
|
|
||||||
|
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
|
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):
|
class PreFacturas(BaseModel):
|
||||||
cliente = ForeignKeyField(Socios)
|
cliente = ForeignKeyField(Socios)
|
||||||
|
@ -9416,7 +9461,8 @@ def empresa_agregar(rfc, no_bd):
|
||||||
|
|
||||||
def empresa_borrar(rfc):
|
def empresa_borrar(rfc):
|
||||||
if _delete_emisor(rfc):
|
if _delete_emisor(rfc):
|
||||||
util.delete_db(rfc.lower())
|
# ~ util.delete_db(rfc.lower())
|
||||||
|
utils.db_delete(rfc.lower(), PATHS['DOCS'])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -10195,7 +10241,8 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña,
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if opt['backup_dbs']:
|
if opt['backup_dbs']:
|
||||||
util.backup_dbs()
|
# ~ util.backup_dbs()
|
||||||
|
utils.db_backup(DB_COMPANIES, PATHS['BK'], IS_MV, URL['SEAFILE'])
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if opt['exportar_documentos']:
|
if opt['exportar_documentos']:
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
from seafileapi.client import SeafileApiClient
|
||||||
|
|
||||||
|
def connect(server, username, password):
|
||||||
|
client = SeafileApiClient(server, username, password)
|
||||||
|
return client
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
class SeafileAdmin(object):
|
||||||
|
def lists_users(self, maxcount=100):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def list_user_repos(self, username):
|
||||||
|
pass
|
|
@ -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
|
|
@ -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
|
|
@ -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__
|
|
@ -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
|
|
@ -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)
|
|
@ -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]
|
|
@ -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
|
|
@ -47,11 +47,12 @@ except ImportError:
|
||||||
|
|
||||||
|
|
||||||
DEBUG = DEBUG
|
DEBUG = DEBUG
|
||||||
VERSION = '1.31.2'
|
VERSION = '1.32.0'
|
||||||
EMAIL_SUPPORT = ('soporte@empresalibre.mx',)
|
EMAIL_SUPPORT = ('soporte@empresalibre.mx',)
|
||||||
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
|
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
|
||||||
|
|
||||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
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_static = os.path.abspath(os.path.join(BASE_DIR, '..', 'static'))
|
||||||
path_docs = os.path.abspath(os.path.join(BASE_DIR, '..', 'docs'))
|
path_docs = os.path.abspath(os.path.join(BASE_DIR, '..', 'docs'))
|
||||||
|
@ -196,6 +197,10 @@ API = 'https://api.empresalibre.net{}'
|
||||||
CURRENCY_MN = 'MXN'
|
CURRENCY_MN = 'MXN'
|
||||||
|
|
||||||
# ~ v2
|
# ~ 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 = {
|
EXT = {
|
||||||
'CSS': 'css',
|
'CSS': 'css',
|
||||||
'HTML': 'html',
|
'HTML': 'html',
|
||||||
|
@ -204,6 +209,7 @@ EXT = {
|
||||||
'JSON': 'json',
|
'JSON': 'json',
|
||||||
}
|
}
|
||||||
MXN = 'MXN'
|
MXN = 'MXN'
|
||||||
|
|
||||||
PATHS = {
|
PATHS = {
|
||||||
'STATIC': path_static,
|
'STATIC': path_static,
|
||||||
'CSS': path_css,
|
'CSS': path_css,
|
||||||
|
@ -211,7 +217,9 @@ PATHS = {
|
||||||
'DOCS': path_docs,
|
'DOCS': path_docs,
|
||||||
'USER': path_user_template,
|
'USER': path_user_template,
|
||||||
'LOGOS': path_user_logos,
|
'LOGOS': path_user_logos,
|
||||||
|
'BK': path_bk,
|
||||||
}
|
}
|
||||||
|
|
||||||
VALUES_PDF = {
|
VALUES_PDF = {
|
||||||
'CANCEL': {True: 'inline', False: 'none'},
|
'CANCEL': {True: 'inline', False: 'none'},
|
||||||
'TYPE': {'I': 'Ingreso', 'E': 'Egreso', 'T': 'Traslado'},
|
'TYPE': {'I': 'Ingreso', 'E': 'Egreso', 'T': 'Traslado'},
|
||||||
|
@ -221,3 +229,7 @@ VALUES_PDF = {
|
||||||
'PPD': 'Pago en parcialidades o diferido',
|
'PPD': 'Pago en parcialidades o diferido',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
URL = {
|
||||||
|
'SEAFILE': 'https://seafile.elmau.net',
|
||||||
|
}
|
||||||
|
|
|
@ -508,7 +508,6 @@ function generar_anticipo_egreso(id){
|
||||||
|
|
||||||
|
|
||||||
function send_timbrar(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){
|
webix.ajax().post('invoices', {opt: 'timbrar', id: id}, function(text, data){
|
||||||
var values = data.json()
|
var values = data.json()
|
||||||
if(values.ok){
|
if(values.ok){
|
||||||
|
@ -1322,7 +1321,6 @@ function grid_invoices_click(id, e, node){
|
||||||
|
|
||||||
|
|
||||||
function send_cancel(id){
|
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){
|
webix.ajax().post('invoices', {opt: 'cancel', id: id}, function(text, data){
|
||||||
var values = data.json()
|
var values = data.json()
|
||||||
if(values.ok){
|
if(values.ok){
|
||||||
|
@ -1338,6 +1336,7 @@ function send_cancel(id){
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function cmd_invoice_cancelar_click(){
|
function cmd_invoice_cancelar_click(){
|
||||||
if(gi.count() == 0){
|
if(gi.count() == 0){
|
||||||
return
|
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(){
|
function cmd_invoice_sat_click(){
|
||||||
if(gi.count() == 0){
|
if(gi.count() == 0){
|
||||||
return
|
return
|
||||||
|
@ -1456,12 +1486,15 @@ function cmd_invoice_sat_click(){
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
webix.ajax().get('/values/statussat', {id: row.id}, function(text, data){
|
var options = {opt: 'statussat', id: row.id}
|
||||||
var values = data.json()
|
webix.ajax().get('/invoices', options, function(text, data){
|
||||||
if(values == 'Vigente'){
|
var value = data.json()
|
||||||
msg_ok(values)
|
if(value == 'Vigente'){
|
||||||
|
msg_ok(value)
|
||||||
|
}else if(value == 'uncancel'){
|
||||||
|
ask_invoice_uncancel(row.id)
|
||||||
}else{
|
}else{
|
||||||
msg_error(values)
|
msg_error(value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue