Merge branch 'develop'

Facturar y cancelar
This commit is contained in:
Mauricio Baeza 2017-10-30 19:02:19 -06:00
commit 60528d8be1
15 changed files with 690 additions and 134 deletions

View File

@ -8,7 +8,7 @@
Este proyecto está en continuo desarrollo, contratar un esquema de soporte,
nos ayuda a continuar su desarrollo. Ponte en contacto con nosotros para
contratar.
contratar: administracion@empresalibre.net
### Requerimientos:
@ -16,6 +16,9 @@ contratar.
* Servidor web, recomendado Nginx
* uwsgi
* python3
* xsltproc
* openssl
* xmlsec
Debería de funcionar con cualquier combinación servidor-wsgi que soporte
aplicaciones Python.

View File

@ -1,10 +1,4 @@
#!/usr/bin/env python
from peewee import SqliteDatabase
DEBUG = True
ID_SUPPORT = ''
DATABASE = None
if DEBUG:
DATABASE = SqliteDatabase('empresalibre.sqlite')

View File

@ -371,7 +371,7 @@ class Finkok(object):
if os.path.isfile(file_xml):
root = etree.parse(file_xml).getroot()
else:
root = etree.fromstring(file_xml)
root = etree.fromstring(file_xml.encode())
xml = etree.tostring(root)
@ -385,8 +385,12 @@ class Finkok(object):
'store_pending': True,
}
result = client.service.cancel_signature(**args)
return result
try:
result = client.service.cancel_signature(**args)
return result
except Fault as e:
self.error = str(e)
return ''
def get_acuse(self, rfc, uuids, type_acuse='C'):
for u in uuids:
@ -443,21 +447,41 @@ class Finkok(object):
return result
def add_token(self, rfc, email):
"""
"""Agrega un nuevo token al cliente para timbrado.
Se requiere cuenta de reseller para usar este método
Args:
rfc (str): El RFC del cliente, ya debe existir
email (str): El correo del cliente, funciona como USER al timbrar
Returns:
dict
'username': 'username',
'status': True or False
'name': 'name',
'success': True or False
'token': 'Token de timbrado',
'message': None
"""
auth = AUTH['RESELLER']
method = 'util'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': AUTH['USER'],
'password': AUTH['PASS'],
'username': auth['USER'],
'password': auth['PASS'],
'name': rfc,
'token_username': email,
'taxpayer_id': rfc,
'status': True,
}
result = client.service.add_token(**args)
try:
result = client.service.add_token(**args)
except Fault as e:
self.error = str(e)
return ''
return result
def get_date(self):
@ -477,17 +501,31 @@ class Finkok(object):
return result.datetime
def add_client(self, rfc, type_user=False):
"""
"""Agrega un nuevo cliente para timbrado.
Se requiere cuenta de reseller para usar este método
type_user: False == 'P' == Prepago or True == 'O' == On demand
Args:
rfc (str): El RFC del nuevo cliente
Kwargs:
type_user (bool): False == 'P' == Prepago or True == 'O' == On demand
Returns:
dict
'message':
'Account Created successfully'
'Account Already exists'
'success': True or False
"""
auth = AUTH['RESELLER']
tu = {False: 'P', True: 'O'}
method = 'client'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'reseller_username': AUTH['USER'],
'reseller_password': AUTH['PASS'],
'reseller_username': auth['USER'],
'reseller_password': auth['PASS'],
'taxpayer_id': rfc,
'type_user': tu[type_user],
'added': datetime.datetime.now().isoformat()[:19],
@ -505,13 +543,15 @@ class Finkok(object):
Se requiere cuenta de reseller para usar este método
status = 'A' or 'S'
"""
auth = AUTH['RESELLER']
sv = {False: 'S', True: 'A'}
method = 'client'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'reseller_username': AUTH['USER'],
'reseller_password': AUTH['PASS'],
'reseller_username': auth['USER'],
'reseller_password': auth['PASS'],
'taxpayer_id': rfc,
'status': sv[status],
}
@ -524,15 +564,35 @@ class Finkok(object):
return result
def get_client(self, rfc):
"""
"""Regresa el estatus del cliente
.
Se requiere cuenta de reseller para usar este método
Args:
rfc (str): El RFC del emisor
Returns:
dict
'message': None,
'users': {
'ResellerUser': [
{
'status': 'A',
'counter': 0,
'taxpayer_id': '',
'credit': 0
}
]
} or None si no existe
"""
auth = AUTH['RESELLER']
method = 'client'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'reseller_username': AUTH['USER'],
'reseller_password': AUTH['PASS'],
'reseller_username': auth['USER'],
'reseller_password': auth['PASS'],
'taxpayer_id': rfc,
}
@ -548,15 +608,30 @@ class Finkok(object):
return result
def assign_client(self, rfc, credit):
"""
"""Agregar credito a un emisor
Se requiere cuenta de reseller para usar este método
Args:
rfc (str): El RFC del emisor, debe existir
credit (int): Cantidad de folios a agregar
Returns:
dict
'success': True or False,
'credit': nuevo credito despues de agregar or None
'message':
'Success, added {credit} of credit to {RFC}'
'RFC no encontrado'
"""
auth = AUTH['RESELLER']
method = 'client'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': AUTH['USER'],
'password': AUTH['PASS'],
'username': auth['USER'],
'password': auth['PASS'],
'taxpayer_id': rfc,
'credit': credit,
}
@ -577,7 +652,7 @@ def _get_data_sat(path):
if os.path.isfile(path):
tree = etree.parse(path).getroot()
else:
tree = etree.fromstring(path)
tree = etree.fromstring(path.encode())
data = {}
emisor = escape(
@ -602,7 +677,7 @@ def _get_data_sat(path):
def get_status_sat(xml):
data = _get_data_sat(xml)
if not data:
return
return 'XML inválido'
URL = 'https://consultaqr.facturaelectronica.sat.gob.mx/ConsultaCFDIService.svc?wsdl'
client = Client(URL, transport=Transport(cache=SqliteCache()))

View File

@ -33,7 +33,8 @@ from dateutil import parser
from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice
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
#~ def _get_hash(password):
@ -52,7 +53,7 @@ def _get_md5(data):
return hashlib.md5(data.encode()).hexdigest()
def _save_temp(data, modo='wb'):
def save_temp(data, modo='wb'):
path = tempfile.mkstemp()[1]
with open(path, modo) as f:
f.write(data)
@ -276,21 +277,22 @@ def to_slug(string):
class Certificado(object):
def __init__(self, key, cer):
self._key = key
self._cer = cer
def __init__(self, paths):
self._path_key = paths['path_key']
self._path_cer = paths['path_cer']
self._modulus = ''
self._save_files()
#~ self._save_files()
self.error = ''
def _save_files(self):
try:
self._path_key = _save_temp(self._key)
self._path_cer = _save_temp(self._cer)
except:
self._path_key = ''
self._path_cer = ''
return
#~ def _save_files(self):
#~ try:
#~ self._path_key = _save_temp(bytes(self._key))
#~ self._path_cer = _save_temp(bytes(self._cer))
#~ except Exception as e:
#~ log.error(e)
#~ self._path_key = ''
#~ self._path_cer = ''
#~ return
def _kill(self, path):
try:
@ -341,8 +343,7 @@ class Certificado(object):
hasta = parser.parse(dates[1].split('=')[1])
self._modulus = _call(args.format(self._path_cer, 'modulus'))
data['cer'] = self._cer
data['cer_tmp'] = None
data['cer'] = read_file(self._path_cer)
data['cer_pem'] = cer_pem
data['cer_txt'] = cer_txt.replace('\n', '')
data['serie'] = serie
@ -365,7 +366,8 @@ class Certificado(object):
'pass:"{}" -out "{}"'
_call(args.format(tmp_cer, tmp_key, rfc,
hashlib.md5(rfc.encode()).hexdigest(), tmp_p12))
data = open(tmp_p12, 'rb').read()
#~ data = open(tmp_p12, 'rb').read()
data = read_file(tmp_p12)
self._kill(tmp_cer)
self._kill(tmp_key)
@ -396,18 +398,20 @@ class Certificado(object):
self._path_key, password, _get_md5(rfc))
key_enc = _call(args)
data['key'] = self._key
data['key_tmp'] = None
data['key'] = read_file(self._path_key)
data['key_enc'] = key_enc
data['p12'] = self._get_p12(password, rfc)
return data
def validate(self, password, rfc):
if not self._path_key or not self._path_cer:
self.error = 'Error al cargar el certificado'
self.error = 'Error en las rutas temporales del certificado'
return {}
data = self._get_info_cer(rfc)
if not data:
return {}
llave = self._get_info_key(password, data['rfc'])
if not llave:
return {}
@ -432,9 +436,9 @@ def make_xml(data, certificado):
data = {
'xsltproc': PATH_XSLTPROC,
'xslt': _join(PATH_XSLT, 'cadena.xslt'),
'xml': _save_temp(xml, 'w'),
'xml': save_temp(xml, 'w'),
'openssl': PATH_OPENSSL,
'key': _save_temp(certificado.key_enc, 'w'),
'key': save_temp(certificado.key_enc, 'w'),
'pass': _get_md5(certificado.rfc)
}
args = '"{xsltproc}" "{xslt}" "{xml}" | ' \
@ -473,6 +477,11 @@ def timbra_xml(xml, auth):
return result
def get_sat(xml):
from .pac import get_status_sat
return get_status_sat(xml)
class LIBO(object):
HOST = 'localhost'
PORT = '8100'
@ -1036,6 +1045,59 @@ def get_path_temp():
return tempfile.mkstemp()[1]
def get_date(value, next_day=False):
d = parser.parse(value)
if next_day:
return d + datetime.timedelta(days=1)
return d
def cancel_cfdi(uuid, pk12, rfc, auth):
from .pac import Finkok as PAC
template = read_file(TEMPLATE_CANCEL, 'r')
data = {
'rfc': rfc,
'fecha': datetime.datetime.now().isoformat()[:19],
'uuid': str(uuid).upper(),
}
template = template.format(**data)
data = {
'xmlsec': PATH_XMLSEC,
'pk12': save_temp(pk12),
'pass': _get_md5(rfc),
'template': save_temp(template, 'w'),
}
args = '"{xmlsec}" --sign --pkcs12 "{pk12}" --pwd {pass} ' \
'"{template}"'.format(**data)
xml_sign = _call(args)
if DEBUG:
auth = {}
else:
if not auth:
msg = 'Sin datos para cancelar'
result = {'ok': False, 'error': msg}
return result
msg = 'Factura cancelada correctamente'
data = {'ok': True, 'msg': msg, 'row': {'estatus': 'Cancelada'}}
pac = PAC(auth)
result = pac.cancel_signature(xml_sign)
if result:
codes = {None: '',
'Could not get UUID Text': 'UUID no encontrado'}
if not result['CodEstatus'] is None:
data['ok'] = False
data['msg'] = codes.get(result['CodEstatus'], result['CodEstatus'])
else:
data['ok'] = False
data['msg'] = pac.error
return data, result
class ImportFacturaLibre(object):
def __init__(self, path):

View File

@ -2,6 +2,7 @@
socket = 127.0.0.1:3033
uid = nginx
gid = nginx
#~ Establece una ruta accesible para nginx o el servidor web que uses
chdir = /srv/app/empresa-libre/app
wsgi-file = main.py
callable = app
@ -10,4 +11,5 @@ processes = 4
threads = 4
thunder-lock = true
#~ stats = 127.0.0.1:9191
#~ Establece una ruta accesible para nginx o el servidor web que uses
logger = file:/srv/log/empresalibre-uwsgi.log

View File

@ -45,7 +45,6 @@ api.add_route('/products', AppProducts(db))
api.add_route('/invoices', AppInvoices(db))
if DEBUG:
api.add_sink(static, '/static')

View File

@ -1,6 +1,5 @@
[uwsgi]
http = 127.0.0.1:8000
#~ http = 37.228.132.181:9000
wsgi-file = main.py
callable = app
master = true

View File

@ -20,8 +20,8 @@ class StorageEngine(object):
def add_config(self, values):
return main.Configuracion.add(values)
def add_cert(self, file_object):
return main.Certificado.add(file_object)
def add_cert(self, file_obj):
return main.Certificado.add(file_obj)
def validate_cert(self, values, session):
return main.Certificado.validate(values, session)
@ -32,6 +32,15 @@ class StorageEngine(object):
def send_email(self, values, session):
return main.Facturas.send(values['id'], session['rfc'])
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_filteryears(self, values):
return main.Facturas.filter_years()
def _get_cert(self, values):
return main.Certificado.get_data()

View File

@ -64,6 +64,9 @@ class Configuracion(BaseModel):
clave = TextField(unique=True)
valor = TextField(default='')
def __str__(self):
return '{} = {}'.format(self.clave, self.valor)
@classmethod
def get_(cls, keys):
if keys['fields'] == 'correo':
@ -74,7 +77,14 @@ class Configuracion(BaseModel):
.select()
.where(Configuracion.clave.in_(fields))
)
values = {r.clave: r.valor for r in data}
elif keys['fields'] == 'path_cer':
fields = ('path_key', 'path_cer')
data = (Configuracion
.select()
.where(Configuracion.clave.in_(fields))
)
values = {r.clave: r.valor for r in data}
return values
@classmethod
@ -289,10 +299,8 @@ class Emisor(BaseModel):
class Certificado(BaseModel):
key = BlobField(null=True)
key_tmp = BlobField(null=True)
key_enc = TextField(default='')
cer = BlobField(null=True)
cer_tmp = BlobField(null=True)
cer_pem = TextField(default='')
cer_txt = TextField(default='')
p12 = BlobField(null=True)
@ -316,28 +324,26 @@ class Certificado(BaseModel):
return row
def get_(cls):
if Certificado.select().count():
obj = Certificado.select()[0]
else:
obj = Certificado()
return obj
return Certificado.select()[0]
@classmethod
def add(cls, file_object):
obj = cls.get_(cls)
if file_object.filename.endswith('key'):
obj.key_tmp = file_object.file.read()
elif file_object.filename.endswith('cer'):
obj.cer_tmp = file_object.file.read()
obj.save()
def add(cls, file_obj):
if file_obj.filename.endswith('key'):
path_key = util.save_temp(file_obj.file.read())
Configuracion.add({'path_key': path_key})
elif file_obj.filename.endswith('cer'):
path_cer = util.save_temp(file_obj.file.read())
Configuracion.add({'path_cer': path_cer})
return {'status': 'server'}
@classmethod
def validate(cls, values, session):
row = {}
result = False
obj = cls.get_(cls)
cert = util.Certificado(obj.key_tmp, obj.cer_tmp)
paths = Configuracion.get_({'fields': 'path_cer'})
cert = util.Certificado(paths)
data = cert.validate(values['contra'], session['rfc'])
if data:
msg = 'Certificado guardado correctamente'
@ -352,9 +358,9 @@ class Certificado(BaseModel):
}
else:
msg = cert.error
obj.key_tmp = None
obj.cer_tmp = None
obj.save()
Configuracion.add({'path_key': ''})
Configuracion.add({'path_cer': ''})
return {'ok': result, 'msg': msg, 'data': row}
@ -666,8 +672,10 @@ class Socios(BaseModel):
es_proveedor = BooleanField(default=False)
cuenta_cliente = TextField(default='')
cuenta_proveedor = TextField(default='')
saldo_cliente = DecimalField(default=0.0, decimal_places=6, auto_round=True)
saldo_proveedor = DecimalField(default=0.0, decimal_places=6, auto_round=True)
saldo_cliente = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
saldo_proveedor = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
web = TextField(default='')
correo_facturas = TextField(default='')
forma_pago = ForeignKeyField(SATFormaPago, null=True)
@ -723,7 +731,7 @@ class Socios(BaseModel):
.where((Socios.id==id) & (Socios.es_cliente==True))
.dicts()
)
print (id, row)
#~ print (id, row)
if len(row):
return {'ok': True, 'row': row[0]}
return {'ok': False}
@ -801,12 +809,17 @@ class Productos(BaseModel):
clave_sat = TextField(default='')
descripcion = TextField(index=True)
unidad = ForeignKeyField(SATUnidades)
valor_unitario = DecimalField(default=0.0, decimal_places=6, auto_round=True)
ultimo_costo = DecimalField(default=0.0, decimal_places=6, auto_round=True)
descuento = DecimalField(default=0.0, decimal_places=6, auto_round=True)
valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
ultimo_costo = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
inventario = BooleanField(default=False)
existencia = DoubleField(default=0.0)
minimo = DoubleField(default=0.0)
existencia = DecimalField(default=0.0, max_digits=18, decimal_places=2,
auto_round=True)
minimo = DecimalField(default=0.0, max_digits=18, decimal_places=2,
auto_round=True)
codigo_barras = TextField(default='')
cuenta_predial = TextField(default='')
es_activo = BooleanField(default=True)
@ -818,7 +831,11 @@ class Productos(BaseModel):
@classmethod
def next_key(cls):
value = Productos.select(fn.Max(Productos.id)).scalar()
value = (Productos
.select(fn.Max(Productos.id))
.group_by(Productos.id)
.order_by(Productos.id)
.scalar())
if value is None:
value = 1
else:
@ -982,28 +999,35 @@ class Facturas(BaseModel):
fecha_timbrado = DateTimeField(null=True)
forma_pago = TextField(default='')
condiciones_pago = TextField(default='')
subtotal = DecimalField(default=0.0, decimal_places=6, auto_round=True)
descuento = DecimalField(default=0.0, decimal_places=6, auto_round=True)
subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
moneda = TextField(default='MXN')
tipo_cambio = DecimalField(default=1.0, decimal_places=6, auto_round=True)
total = DecimalField(default=0.0, decimal_places=6, auto_round=True)
total_mn = DecimalField(default=0.0, decimal_places=6, auto_round=True)
total = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
tipo_comprobante = TextField(default='I')
metodo_pago = TextField(default='PUE')
lugar_expedicion = TextField(default='')
confirmacion = TextField(default='')
uso_cfdi = TextField(default='')
total_retenciones = DecimalField(
decimal_places=6, auto_round=True, null=True)
max_digits=20, decimal_places=6, auto_round=True, null=True)
total_trasladados = DecimalField(
decimal_places=6, auto_round=True, null=True)
max_digits=20, decimal_places=6, auto_round=True, null=True)
xml = TextField(default='')
uuid = UUIDField(null=True)
estatus = TextField(default='Guardada')
estatus_sat = TextField(default='Vigente')
regimen_fiscal = TextField(default='')
notas = TextField(default='')
pagada = BooleanField(default=False)
cancelada = BooleanField(default=False)
fecha_cancelacion = DateTimeField(null=True)
acuse = TextField(default='')
donativo = BooleanField(default=False)
tipo_relacion = TextField(default='')
error = TextField(default='')
@ -1011,6 +1035,38 @@ class Facturas(BaseModel):
class Meta:
order_by = ('fecha',)
@classmethod
def cancel(cls, id):
msg = 'Factura cancelada correctamente'
auth = Emisor.get_auth()
certificado = Certificado.select()[0]
obj = Facturas.get(Facturas.id==id)
data, result = util.cancel_cfdi(
obj.uuid, certificado.p12, certificado.rfc, auth)
if data['ok']:
obj.estatus = 'Cancelada'
obj.error = ''
obj.cancelada = True
obj.fecha_cancelacion = result['Fecha']
obj.acuse = result['Acuse']
else:
obj.error = data['msg']
obj.save()
return data
@classmethod
def filter_years(cls):
data = [{'id': -1, 'value': 'Todos'}]
rows = (Facturas
.select(Facturas.fecha.year)
.group_by(Facturas.fecha.year)
.order_by(Facturas.fecha.year)
.scalar(as_tuple=True)
)
if not rows is None:
data += [{'id': int(row), 'value': int(row)} for row in rows]
return tuple(data)
@classmethod
def get_xml(cls, id):
obj = Facturas.get(Facturas.id==id)
@ -1171,10 +1227,27 @@ class Facturas(BaseModel):
@classmethod
def get_(cls, values):
if 'start' in values:
filters = Facturas.fecha.between(
util.get_date(values['start']),
util.get_date(values['end'], True)
)
else:
if values['year'] == '-1':
fy = (Facturas.fecha.year > 0)
else:
fy = (Facturas.fecha.year == int(values['year']))
if values['month'] == '-1':
fm = (Facturas.fecha.month > 0)
else:
fm = (Facturas.fecha.month == int(values['month']))
filters = (fy & fm)
rows = tuple(Facturas
.select(Facturas.id, Facturas.serie, Facturas.folio, Facturas.uuid,
Facturas.fecha, Facturas.tipo_comprobante, Facturas.estatus,
Facturas.total_mn, Socios.nombre.alias('cliente'))
.where(filters)
.join(Socios)
.switch(Facturas).dicts()
)
@ -1190,6 +1263,8 @@ class Facturas(BaseModel):
q.execute()
q = FacturasImpuestos.delete().where(FacturasImpuestos.factura==obj)
q.execute()
q = FacturasRelacionadas.delete().where(FacturasRelacionadas.factura==obj)
q.execute()
return bool(obj.delete_instance())
def _get_folio(self, serie):
@ -1198,6 +1273,8 @@ class Facturas(BaseModel):
inicio = (Facturas
.select(fn.Max(Facturas.folio))
.where(Facturas.serie==serie)
.group_by(Facturas.folio)
.order_by(Facturas.folio)
.scalar())
if inicio is None:
inicio = inicio_serie
@ -1437,6 +1514,13 @@ class Facturas(BaseModel):
}
return util.make_xml(data, certificado)
@classmethod
def get_status_sat(cls, id):
obj = Facturas.get(Facturas.id == id)
obj.estatus_sat = util.get_sat(obj.xml)
obj.save()
return obj.estatus_sat
@classmethod
def timbrar(cls, id):
obj = Facturas.get(Facturas.id == id)
@ -1446,7 +1530,7 @@ class Facturas(BaseModel):
auth = Emisor.get_auth()
error = False
#~ error = False
msg = 'Factura timbrada correctamente'
result = util.timbra_xml(obj.xml, auth)
if result['ok']:
@ -1455,10 +1539,10 @@ class Facturas(BaseModel):
obj.fecha_timbrado = result['fecha']
obj.estatus = 'Timbrada'
obj.error = ''
obj.save()
#~ obj.save()
row = {'uuid': obj.uuid, 'estatus': 'Timbrada'}
else:
error = True
#~ error = True
msg = result['error']
obj.estatus = 'Error'
obj.error = msg
@ -1467,25 +1551,69 @@ class Facturas(BaseModel):
return {'ok': result['ok'], 'msg': msg, 'row': row}
class PreFacturas(BaseModel):
cliente = ForeignKeyField(Socios)
serie = TextField(default='PRE')
folio = IntegerField(default=0)
fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S'])
forma_pago = TextField(default='')
condiciones_pago = TextField(default='')
subtotal = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
descuento = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
moneda = TextField(default='MXN')
tipo_cambio = DecimalField(default=1.0, decimal_places=6, auto_round=True)
total = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
total_mn = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
tipo_comprobante = TextField(default='I')
metodo_pago = TextField(default='PUE')
lugar_expedicion = TextField(default='')
uso_cfdi = TextField(default='')
total_retenciones = DecimalField(
max_digits=20, decimal_places=6, auto_round=True, null=True)
total_trasladados = DecimalField(
max_digits=20, decimal_places=6, auto_round=True, null=True)
estatus = TextField(default='Generada')
regimen_fiscal = TextField(default='')
notas = TextField(default='')
donativo = BooleanField(default=False)
tipo_relacion = TextField(default='')
class Meta:
order_by = ('fecha',)
class FacturasRelacionadas(BaseModel):
factura = ForeignKeyField(Facturas, related_name='original')
factura_origen = ForeignKeyField(Facturas, related_name='relacion')
class Meta:
order_by = ('factura',)
indexes = (
(('factura', 'factura_origen'), True),
)
class PreFacturasRelacionadas(BaseModel):
factura = ForeignKeyField(PreFacturas, related_name='original')
factura_origen = ForeignKeyField(PreFacturas, related_name='relacion')
class Meta:
order_by = ('factura',)
class FacturasDetalle(BaseModel):
factura = ForeignKeyField(Facturas)
producto = ForeignKeyField(Productos, null=True)
cantidad = DecimalField(default=0.0, decimal_places=6, auto_round=True)
valor_unitario = DecimalField(default=0.0, decimal_places=6, auto_round=True)
descuento = DecimalField(default=0.0, decimal_places=6, auto_round=True)
precio_final = DecimalField(default=0.0, decimal_places=6, auto_round=True)
importe = DecimalField(default=0.0, decimal_places=6, auto_round=True)
cantidad = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
precio_final = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
importe = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
descripcion = TextField(default='')
unidad = TextField(default='')
clave = TextField(default='')
@ -1504,11 +1632,54 @@ class FacturasDetalle(BaseModel):
order_by = ('factura',)
class PreFacturasDetalle(BaseModel):
factura = ForeignKeyField(PreFacturas)
producto = ForeignKeyField(Productos, null=True)
cantidad = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
valor_unitario = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
descuento = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
precio_final = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
importe = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
aduana = TextField(default='')
pedimento = TextField(default='')
fecha_pedimento = DateField(null=True)
alumno = TextField(default='')
curp = TextField(default='')
nivel = TextField(default='')
autorizacion = TextField(default='')
cuenta_predial = TextField(default='')
class Meta:
order_by = ('factura',)
class FacturasImpuestos(BaseModel):
factura = ForeignKeyField(Facturas)
impuesto = ForeignKeyField(SATImpuestos)
base = DecimalField(default=0.0, decimal_places=6, auto_round=True)
importe = DecimalField(default=0.0, decimal_places=6, auto_round=True)
base = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
importe = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
class Meta:
order_by = ('factura',)
indexes = (
(('factura', 'impuesto'), True),
)
class PreFacturasImpuestos(BaseModel):
factura = ForeignKeyField(PreFacturas)
impuesto = ForeignKeyField(SATImpuestos)
base = DecimalField(default=0.0, max_digits=20, decimal_places=6,
auto_round=True)
importe = DecimalField(default=0.0, max_digits=18, decimal_places=6,
auto_round=True)
class Meta:
order_by = ('factura',)
@ -1596,12 +1767,14 @@ def test_correo(values):
return util.send_mail(data)
def _init_values():
def _init_values(rfc):
data = (
{'clave': 'version', 'valor': VERSION},
{'clave': 'rfc_publico', 'valor': 'XAXX010101000'},
{'clave': 'rfc_extranjero', 'valor': 'XEXX010101000'},
{'clave': 'decimales', 'valor': '2'},
{'clave': 'path_key', 'valor': ''},
{'clave': 'path_cer', 'valor': ''},
)
for row in data:
try:
@ -1609,6 +1782,10 @@ def _init_values():
Configuracion.create(**row)
except IntegrityError:
pass
if not Certificado.select().count():
Certificado.create(rfc=rfc)
log.info('Valores iniciales insertados...')
return
@ -1617,6 +1794,8 @@ def _crear_tablas(rfc):
tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion,
Emisor, Facturas, FacturasDetalle, FacturasImpuestos, Folios,
FacturasRelacionadas, Productos,
PreFacturas, PreFacturasDetalle, PreFacturasImpuestos,
PreFacturasRelacionadas,
SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes,
SATTipoRelacion, SATUnidades, SATUsoCfdi,
Socios, Tags, Usuarios,
@ -1639,7 +1818,7 @@ def _crear_tablas(rfc):
msg = 'El usuario ya existe'
log.error(msg)
_init_values()
_init_values(rfc)
_importar_valores('', rfc)
return True

View File

@ -11,7 +11,7 @@ from conf import DEBUG
DEBUG = DEBUG
VERSION = '0.1.0'
VERSION = '0.2.0'
EMAIL_SUPPORT = ('soporte@empresalibre.net',)
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
@ -25,6 +25,8 @@ DB_SAT = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'sat.db'))
IV = 'valores_iniciales.json'
INIT_VALUES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', IV))
CT = 'cancel_template.xml'
TEMPLATE_CANCEL = os.path.abspath(os.path.join(PATH_TEMPLATES, CT))
PATH_XSLT = os.path.abspath(os.path.join(BASE_DIR, '..', 'xslt'))
PATH_BIN = os.path.abspath(os.path.join(BASE_DIR, '..', 'bin'))
@ -50,6 +52,7 @@ if DEBUG:
level=LOG_LEVEL,
format_string=format_string).push_application()
else:
#~ Establece una ruta con acceso para nginx o el servidor web que uses
LOG_PATH = '/srv/log/empresalibre.log'
RotatingFileHandler(
LOG_PATH,
@ -69,9 +72,11 @@ log = Logger(LOG_NAME)
PATH_XSLTPROC = 'xsltproc'
PATH_OPENSSL = 'openssl'
PATH_XMLSEC = 'xmlsec1'
if 'win' in sys.platform:
PATH_XSLTPROC = os.path.join(PATH_BIN, 'xsltproc.exe')
PATH_OPENSSL = os.path.join(PATH_BIN, 'openssl.exe')
PATH_XMLSEC = os.path.join(PATH_BIN, 'xmlsec.exe')
PRE = {

View File

@ -27,9 +27,7 @@ function get_series(){
function get_forma_pago(){
webix.ajax().get('/values/formapago', {key: true}, function(text, data){
var values = data.json()
//~ pre = values[0]
$$('lst_forma_pago').getList().parse(values)
//~ $$('lst_forma_pago').setValue(pre.id)
})
}
@ -697,7 +695,19 @@ function grid_invoices_click(id, e, node){
function send_cancel(id){
show(id)
webix.ajax().get('/values/cancelinvoice', {id: id}, function(text, data){
var values = data.json()
if(values.ok){
msg_sucess(values.msg)
gi.updateItem(id, values.row)
}else{
webix.alert({
title: 'Error al Cancelar',
text: values.msg,
type: 'alert-error'
})
}
})
}
function cmd_invoice_cancelar_click(){
@ -717,6 +727,11 @@ function cmd_invoice_cancelar_click(){
return
}
if(row.estatus == 'Cancelada'){
msg_error('La factura ya esta cancelada')
return
}
msg = '¿Estás seguro de enviar a cancelar esta factura?<BR><BR> \
ESTA ACCIÓN NO SE PUEDE DESHACER'
webix.confirm({
@ -732,3 +747,87 @@ function cmd_invoice_cancelar_click(){
}
})
}
function get_invoices(rango){
if(rango == undefined){
var fy = $$('filter_year')
var fm = $$('filter_month')
var y = fy.getValue()
var m = fm.getValue()
rango = {'year': y, 'month': m}
}
var grid = $$('grid_invoices')
webix.ajax().get('/invoices', rango, {
error: function(text, data, xhr) {
webix.message({type: 'error', text: 'Error al consultar'})
},
success: function(text, data, xhr) {
var values = data.json();
grid.clearAll();
if (values.ok){
grid.parse(values.rows, 'json');
};
}
});
}
function filter_year_change(nv, ov){
get_invoices()
}
function filter_month_change(nv, ov){
get_invoices()
}
function filter_dates_change(range){
if(range.start != null && range.end != null){
get_invoices(range)
}
}
function cmd_invoice_sat_click(){
if(gi.count() == 0){
return
}
var row = gi.getSelectedItem()
if (row == undefined){
msg_error('Selecciona una factura')
return
}
if(!row.uuid){
msg_error('La factura no esta timbrada, solo es posible consultar \
el estatus en el SAT de facturas timbradas')
return
}
webix.ajax().get('/values/statussat', {id: row.id}, function(text, data){
var values = data.json()
show(values)
})
}
function cmd_prefactura_click(){
var form = this.getFormView()
if(!form.validate()) {
msg_error('Valores inválidos')
return
}
var values = form.getValues()
if(!validate_invoice(values)){
return
}
show('PreFactura')
}

View File

@ -44,8 +44,13 @@ var controllers = {
$$('grid_details').attachEvent('onBeforeEditStart', grid_details_before_edit_start)
$$('grid_details').attachEvent('onBeforeEditStop', grid_details_before_edit_stop)
$$('cmd_invoice_timbrar').attachEvent('onItemClick', cmd_invoice_timbrar_click)
$$('cmd_invoice_sat').attachEvent('onItemClick', cmd_invoice_sat_click)
$$('cmd_invoice_cancelar').attachEvent('onItemClick', cmd_invoice_cancelar_click)
$$('grid_invoices').attachEvent('onItemClick', grid_invoices_click)
$$('filter_year').attachEvent('onChange', filter_year_change)
$$('filter_month').attachEvent('onChange', filter_month_change)
$$('filter_dates').attachEvent('onChange', filter_dates_change)
$$('cmd_prefactura').attachEvent('onItemClick', cmd_prefactura_click)
}
}
@ -92,23 +97,6 @@ function get_products(){
}
function get_invoices(){
var grid = $$('grid_invoices')
webix.ajax().get('/invoices', {}, {
error: function(text, data, xhr) {
webix.message({type: 'error', text: 'Error al consultar'})
},
success: function(text, data, xhr) {
var values = data.json();
grid.clearAll();
if (values.ok){
grid.parse(values.rows, 'json');
};
}
});
}
function menu_user_click(id, e, node){
if (id == 1){
window.location = '/logout';
@ -117,6 +105,26 @@ function menu_user_click(id, e, node){
}
function current_dates(){
var fy = $$('filter_year')
var fm = $$('filter_month')
var d = new Date()
fy.blockEvent()
fm.blockEvent()
fm.setValue(d.getMonth() + 1)
webix.ajax().sync().get('/values/filteryears', function(text, data){
var values = data.json()
fy.getList().parse(values)
fy.setValue(d.getFullYear())
})
fy.unblockEvent()
fm.unblockEvent()
}
function multi_change(prevID, nextID){
//~ webix.message(nextID)
if(nextID == 'app_partners'){
@ -138,6 +146,7 @@ function multi_change(prevID, nextID){
if(nextID == 'app_invoices'){
active = $$('multi_invoices').getActiveId()
if(active == 'invoices_home'){
current_dates()
get_invoices()
}
gi = $$('grid_invoices')

View File

@ -14,12 +14,41 @@ var toolbar_invoices = [
var toolbar_invoices_util = [
{view: 'button', id: 'cmd_invoice_timbrar', label: 'Timbrar',
type: 'iconButton', autowidth: true, icon: 'ticket'},
{view: 'button', id: 'cmd_invoice_sat', label: 'SAT',
type: 'iconButton', autowidth: true, icon: 'check-circle'},
{},
{view: 'button', id: 'cmd_invoice_cancelar', label: 'Cancelar',
type: 'iconButton', autowidth: true, icon: 'ban'},
]
var months = [
{id: -1, value: 'Todos'},
{id: 1, value: 'Enero'},
{id: 2, value: 'Febrero'},
{id: 3, value: 'Marzo'},
{id: 4, value: 'Abril'},
{id: 5, value: 'Mayo'},
{id: 6, value: 'Junio'},
{id: 7, value: 'Julio'},
{id: 8, value: 'Agosto'},
{id: 9, value: 'Septiembre'},
{id: 10, value: 'Octubre'},
{id: 11, value: 'Noviembre'},
{id: 12, value: 'Diciembre'},
]
var toolbar_invoices_filter = [
{view: 'richselect', id: 'filter_year', label: 'Año', labelAlign: 'right',
labelWidth: 50, width: 150, options: []},
{view: 'richselect', id: 'filter_month', label: 'Mes', labelAlign: 'right',
labelWidth: 50, width: 200, options: months},
{view: 'daterangepicker', id: 'filter_dates', label: 'Fechas',
labelAlign: 'right', width: 300},
]
function get_icon(tipo){
var node = "<img src='/static/img/file-" + tipo + ".png' height='20' width='17' style='margin: 5px 0px'/>"
return node
@ -34,7 +63,8 @@ var grid_invoices_cols = [
sort:"int", css: "cell_right"},
{id: "uuid", header: ["UUID", {content: "textFilter"}], adjust: "data",
sort:"string", hidden:true},
{id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort:"string"},
{id: "fecha", header: ["Fecha y Hora"],
adjust: "data", sort: "date"},
{id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}],
adjust: 'header', sort: 'string'},
{id: "estatus", header: ["Estatus", {content: "selectFilter"}],
@ -228,6 +258,7 @@ var body_regimen_fiscal = {
var controls_generate = [
{minHeight: 15, maxHeight: 15},
{cols: [ {rows:[
{view: 'fieldset', label: 'Buscar Cliente', body: {rows: [
{cols: [
@ -272,26 +303,86 @@ var controls_generate = [
{view: 'label', label: 'Detalle', height: 30, align: 'left'},
grid_details,
{minHeight: 15, maxHeight: 15},
{cols: [{}, grid_totals]}
{cols: [{}, grid_totals]},
{minHeight: 15, maxHeight: 15},
{margin: 20, cols: [{},
{view: "button", id: "cmd_timbrar", label: "Timbrar",
type: "form", autowidth: true, align:"center"},
{view: "button", id: 'cmd_prefactura', label: "PreFactura",
type: "form", autowidth: true, align:"center"},
{}]
}
]
var toolbar_preinvoices = [
{view: 'button', id: 'cmd_facturar_preinvoice', label: 'Facturar',
type: 'iconButton', autowidth: true, icon: 'pencil'},
{},
{view: "button", id: "cmd_delete_preinvoice", label: "Eliminar",
type: "iconButton", autowidth: true, icon: "minus"},
]
var toolbar_prefilter = [
{view: 'richselect', id: 'prefilter_year', label: 'Año', labelAlign: 'right',
labelWidth: 50, width: 150, options: []},
{view: 'richselect', id: 'prefilter_month', label: 'Mes', labelAlign: 'right',
labelWidth: 50, width: 200, options: months},
]
var grid_preinvoices_cols = [
{id: "id", header:"ID", hidden:true},
{id: "folio", header: ["Folio", {content: "numberFilter"}], adjust: "data",
sort:"int", css: "cell_right"},
{id: "fecha", header: ["Fecha y Hora"],
adjust: "data", sort: "date"},
{id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}],
adjust: 'header', sort: 'string'},
{id: 'total_mn', header: ['Total M.N.'], width: 150,
sort: 'int', format: webix.i18n.priceFormat, css: 'right'},
{id: "cliente", header: ["Razón Social", {content: "selectFilter"}],
fillspace:true, sort:"string"},
{id: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')},
{id: 'email', header: '', adjust: 'data', template: get_icon('email')}
]
var grid_preinvoices = {
view: 'datatable',
id: 'grid_preinvoices',
select: 'row',
adjust: true,
footer: true,
resizeColumn: true,
headermenu: true,
columns: grid_preinvoices_cols,
}
var controls_prefactura = [
{view: 'toolbar', elements: toolbar_preinvoices},
{view: 'toolbar', elements: toolbar_prefilter},
grid_preinvoices,
]
var controls_invoices = [
{
view: "tabview",
tabbar: {options: ["Generar"]}, animate: true,
view: 'tabview',
tabbar: {options: ['Generar', 'PreFacturas']}, animate: true,
cells: [
{id: "Generar", rows: controls_generate},
{id: 'Generar', rows: controls_generate},
{id: 'PreFacturas', rows: controls_prefactura},
]
},
{rows: [
{template:"", type: "section" },
{margin: 10, cols: [{},
{view: "button", id: "cmd_timbrar", label: "Timbrar",
type: "form", autowidth: true, align:"center"},
{view: 'button', id: 'cmd_close_invoice', label: 'Cancelar',
type: 'danger', autowidth: true, align: 'center'},
{}]
type: 'danger', autowidth: true, align: 'center'}
]
},
]}
]
@ -316,6 +407,7 @@ var multi_invoices = {
{id: 'invoices_home', rows:[
{view: 'toolbar', elements: toolbar_invoices},
{view: 'toolbar', elements: toolbar_invoices_util},
{view: 'toolbar', elements: toolbar_invoices_filter},
grid_invoices,
]},
{id: 'invoices_new', rows:[form_invoice, {}]}

View File

@ -62,7 +62,8 @@ var ui_main = {
{view: 'label', label: 'Empresa Libre'},
{},
menu_user,
{view: 'button', type: 'icon', width: 45, css: 'app_button', icon: 'bell-o', badge: 1}
{view: 'button', type: 'icon', width: 45, css: 'app_button',
icon: 'bell-o', badge: 0}
]
},
{

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Cancelacion RfcEmisor="{rfc}" Fecha="{fecha}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://cancelacfd.sat.gob.mx">
<Folios>
<UUID>{uuid}</UUID>
</Folios>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue />
</Reference>
</SignedInfo>
<SignatureValue />
<KeyInfo>
<X509Data>
<X509SubjectName />
<X509IssuerSerial />
<X509Certificate />
</X509Data>
<KeyValue />
</KeyInfo>
</Signature>
</Cancelacion>