diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3aecb8d..f3e42af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,32 @@
+v 1.40.0 [05-ene-2021]
+----------------------
+ - Error: Al parsear XML en Python 3.9+
+ - Mejora: Agregar versión de Empresa Libre a plantilla.
+ - Mejora: Sellado en memoria
+ - Mejora: Se agrega un segundo PAC y se refactoriza el timbrado.
+
+* **IMPORTANTE**
+
+Es necesario seguir una serie de pasos **obligatorios** para migrar a esta
+versión, **no continues hasta seguir paso a paso** estas instrucciones.
+**Antes** de comenzar ten a la mano tus certificados de sello para timbrar, es
+necesario subirlos de nuevo. **NO actualices si no tienes tus certificados**
+con su respectiva contraseña, te quedarás sin poder timbrar.
+
+1. Entra a la parte administrativa y toma de tus credenciales de timbrado en el
+menú "Emisor" ficha "Otros Datos", usuario y token de timbrado.
+1. Agregar nuevo requerimiento `pip install xmlsec`
+1. Actualizar `git pull origin master`
+1. Entrar a `source/app/controllers/pacs` y copiar `conf.py.example` a `conf.py`
+1. Reiniciar el servicio: `sudo systemctl restart empresalibre`
+1. Sube de nuevo tus certificados en el menú "Emisor" ficha "Certificado".
+1. Ve al menú "Opciones", ficha "Otros".
+1. Selecciona tu PAC, si tu usuario es un correo electrónico, invariablemente
+debes seleccionar Finkok.
+1. Establece las credenciales del punto 1.
+1. Guarda los datos.
+
+
v 1.39.1 [17-sep-2020]
----------------------
- Error: Esquema para complemento IEDU
diff --git a/README.md b/README.md
index 826f193..e9fde91 100644
--- a/README.md
+++ b/README.md
@@ -10,16 +10,17 @@ Este proyecto está en continuo desarrollo, contratar un esquema de soporte,
nos ayuda a continuar su desarrollo. Ponte en contacto con nosotros para
contratar: administracion ARROBA empresalibre.net
-#### Ahora también puede aportar con Bitcoin Cash (BCH):
+#### Ahora también puede aportar con criptomonedas:
-`pq763fj7kxxf2wtf360lfsy5ydw84yz72q76hanhxq`
+BCH: `qztd3l00xle5tffdqvh2snvadkuau2ml0uqm4n875d`
+BTC: `3FhiXcXmAesmQzrNEngjHFnvaJRhU1AGWV`
### Requerimientos:
* Servidor web, recomendado Nginx
* uwsgi
-* python3.6+
+* python3.7+
* xsltproc
* openssl
* xmlsec
diff --git a/VERSION b/VERSION
index 0c11aad..32b7211 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.39.1
+1.40.0
diff --git a/requirements.txt b/requirements.txt
index d33642b..389a78d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,3 +13,10 @@ pypng
reportlab
psycopg2-binary
cryptography
+xmlsec
+
+# escpos
+# pyusb
+# pyserial
+# qrcode
+
diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py
index 0351477..fc5087e 100644
--- a/source/app/controllers/cfdi_xml.py
+++ b/source/app/controllers/cfdi_xml.py
@@ -139,8 +139,9 @@ class CFDI(object):
return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))
- def add_sello(self, sello):
+ def add_sello(self, sello, cert_txt):
self._cfdi.attrib['Sello'] = sello
+ self._cfdi.attrib['Certificado'] = cert_txt
return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))
def _to_pretty_xml(self, source):
diff --git a/source/app/controllers/conf.py.example b/source/app/controllers/conf.py.example
deleted file mode 100644
index 2a015b5..0000000
--- a/source/app/controllers/conf.py.example
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env python3
-
-
-DEBUG = False
-
-#~ Ecodex
-ID_INTEGRADOR = ''
-
-#~ Finkok
-FINKOK= {
- 'USER': '',
- 'PASS': '',
-}
diff --git a/source/app/controllers/configpac.py b/source/app/controllers/configpac.py
deleted file mode 100644
index 814fefa..0000000
--- a/source/app/controllers/configpac.py
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env python3
-
-
-from .conf import DEBUG, ID_INTEGRADOR, FINKOK
-
-DEBUG = DEBUG
-TIMEOUT = 10
-
-#~ PACs que han proporcionado un entorno de pruebas libre y abierto
-#~ ecodex, finkok
-PAC = 'finkok'
-
-
-def ecodex(debug):
- NEW_SERVER = True
- auth = {'ID': ID_INTEGRADOR}
- if debug:
- #~ No cambies este ID de pruebas
- auth = {'ID': '2b3a8764-d586-4543-9b7e-82834443f219'}
-
- base_url = 'https://servicios.ecodex.com.mx:4043/Servicio{}.svc?wsdl'
- if NEW_SERVER:
- base_url = 'https://serviciosnominas.ecodex.com.mx:4043/Servicio{}.svc?wsdl'
- base_api = 'https://api.ecodex.com.mx/{}'
- if debug:
- base_url = 'https://wsdev.ecodex.com.mx:2045/Servicio{}.svc?wsdl'
- base_api = 'https://pruebasapi.ecodex.com.mx/{}'
- url = {
- 'seguridad': base_url.format('Seguridad'),
- 'clients': base_url.format('Clientes'),
- 'timbra': base_url.format('Timbrado'),
- 'token': base_api.format('token?version=2'),
- 'docs': base_api.format('api/documentos'),
- 'hash': base_api.format('api/Documentos/{}'),
- 'codes': {
- 'HASH': 'DUPLICIDAD EN HASH',
- }
- }
- return auth, url
-
-
-#~ IMPORTANTE: Si quieres hacer pruebas, con tu propio correo de usuario y
-#~ contraseña, ponte en contacto con Finkok para que te asignen tus datos de
-#~ acceso, consulta su documentación para ver las diferentes opciones de acceso.
-#~ Si solo estas haciendo pruebas de timbrado y ancelación, con estos datos debería
-#~ ser suficiente.
-def finkok(debug):
- USER = FINKOK['USER']
- PASS = FINKOK['PASS']
- TOKEN = ''
- auth = {
- 'DEBUG': debug,
- 'USER': '',
- 'PASS': TOKEN or PASS,
- 'RESELLER': {'USER': USER, 'PASS': PASS}
- }
- if debug:
- USER = 'pruebas-finkok@correolibre.net'
- PASS = ''
- TOKEN = '5c9a88da105bff9a8c430cb713f6d35269f51674bdc5963c1501b7316366'
- auth = {
- 'DEBUG': debug,
- 'USER': USER,
- 'PASS': TOKEN or PASS,
- 'RESELLER': {
- 'USER': '',
- 'PASS': ''
- }
- }
-
- base_url = 'https://facturacion.finkok.com/servicios/soap/{}.wsdl'
- if debug:
- base_url = 'http://demo-facturacion.finkok.com/servicios/soap/{}.wsdl'
- url = {
- 'timbra': base_url.format('stamp'),
- 'quick_stamp': False,
- 'cancel': base_url.format('cancel'),
- 'client': base_url.format('registration'),
- 'util': base_url.format('utilities'),
- 'codes': {
- '200': 'Comprobante timbrado satisfactoriamente',
- '307': 'Comprobante timbrado previamente',
- '205': 'No Encontrado',
- }
- }
- return auth, url
-
-
-AUTH, URL = globals()[PAC](DEBUG)
-
diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py
index 3efcfc2..81441f1 100644
--- a/source/app/controllers/main.py
+++ b/source/app/controllers/main.py
@@ -632,3 +632,19 @@ class AppSociosCuentasBanco(object):
req.context['result'] = self._db.partners_accounts_bank(values)
resp.status = falcon.HTTP_200
+
+class AppCert(object):
+
+ def __init__(self, db):
+ self._db = db
+
+ def on_get(self, req, resp):
+ values = req.params
+ req.context['result'] = self._db.cert_get(values)
+ resp.status = falcon.HTTP_200
+
+ def on_post(self, req, resp):
+ values = req.params
+ req.context['result'] = self._db.cert_post(values)
+ resp.status = falcon.HTTP_200
+
diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py
deleted file mode 100644
index 9242773..0000000
--- a/source/app/controllers/pac.py
+++ /dev/null
@@ -1,755 +0,0 @@
-#!/usr/bin/env python3
-
-#~ import re
-#~ from xml.etree import ElementTree as ET
-#~ from requests import Request, Session, exceptions
-import datetime
-import hashlib
-import os
-import requests
-import time
-from lxml import etree
-from xml.dom.minidom import parseString
-from xml.sax.saxutils import escape, unescape
-from uuid import UUID
-
-from logbook import Logger
-from zeep import Client
-from zeep.plugins import HistoryPlugin
-from zeep.cache import SqliteCache
-from zeep.transports import Transport
-from zeep.exceptions import Fault, TransportError
-from requests.exceptions import ConnectionError
-
-
-if __name__ == '__main__':
- from configpac import DEBUG, TIMEOUT, AUTH, URL
-else:
- from .configpac import DEBUG, TIMEOUT, AUTH, URL
-
-
-log = Logger('PAC')
-#~ node = client.create_message(client.service, SERVICE, **args)
-#~ print(etree.tostring(node, pretty_print=True).decode())
-
-
-class Ecodex(object):
-
- def __init__(self, auth, url):
- self.auth = auth
- self.url = url
- self.codes = self.url['codes']
- self.error = ''
- self.message = ''
- self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT)
- self._plugins = None
- self._history = None
- if DEBUG:
- self._history = HistoryPlugin()
- self._plugins = [self._history]
-
- def _get_token(self, rfc):
- client = Client(self.url['seguridad'],
- transport=self._transport, plugins=self._plugins)
- try:
- result = client.service.ObtenerToken(rfc, self._get_epoch())
- except Fault as e:
- self.error = str(e)
- log.error(self.error)
- return ''
-
- s = '{}|{}'.format(self.auth['ID'], result.Token)
- return hashlib.sha1(s.encode()).hexdigest()
-
- def _get_token_rest(self, rfc):
- data = {
- 'rfc': rfc,
- 'grant_type': 'authorization_token',
- }
- headers = {'Content-type': 'application/x-www-form-urlencoded'}
- result = requests.post(URL['token'], data=data, headers=headers)
- data = result.json()
- s = '{}|{}'.format(AUTH['ID'], data['service_token'])
- return hashlib.sha1(s.encode()).hexdigest(), data['access_token']
-
- def _validate_xml(self, xml):
- NS_CFDI = {'cfdi': 'http://www.sat.gob.mx/cfd/3'}
- if os.path.isfile(xml):
- tree = etree.parse(xml).getroot()
- else:
- tree = etree.fromstring(xml.encode())
-
- fecha = tree.get('Fecha')
- rfc = tree.xpath('string(//cfdi:Emisor/@Rfc)', namespaces=NS_CFDI)
- data = {
- 'ComprobanteXML': etree.tostring(tree).decode(),
- 'RFC': rfc,
- 'Token': self._get_token(rfc),
- 'TransaccionID': self._get_epoch(fecha),
- }
- return data
-
- def _get_by_hash(self, sh, rfc):
- token, access_token = self._get_token_rest(rfc)
- url = URL['hash'].format(sh)
- headers = {
- 'Authorization': 'Bearer {}'.format(access_token),
- 'X-Auth-Token': token,
- }
- result = requests.get(url, headers=headers)
- if result.status_code == 200:
- print (result.json())
- return
-
- def timbra_xml(self, xml):
- data = self._validate_xml(xml)
- client = Client(self.url['timbra'],
- transport=self._transport, plugins=self._plugins)
- try:
- result = client.service.TimbraXML(**data)
- except Fault as e:
- error = str(e)
- if self.codes['HASH'] in error:
- sh = error.split(' ')[3]
- return self._get_by_hash(sh[:40], data['RFC'])
- self.error = error
- return ''
-
- tree = parseString(result.ComprobanteXML.DatosXML)
- xml = tree.toprettyxml(encoding='utf-8').decode('utf-8')
- return xml
-
- def _get_epoch(self, date=None):
- if isinstance(date, str):
- f = '%Y-%m-%dT%H:%M:%S'
- e = int(time.mktime(time.strptime(date, f)))
- else:
- date = datetime.datetime.now()
- e = int(time.mktime(date.timetuple()))
- return e
-
- def estatus_cuenta(self, rfc):
- #~ Codigos:
- #~ 100 = Cuenta encontrada
- #~ 101 = RFC no dado de alta en el sistema ECODEX
- token = self._get_token(rfc)
- if not token:
- return {}
-
- data = {
- 'RFC': rfc,
- 'Token': token,
- 'TransaccionID': self._get_epoch()
- }
- client = Client(URL['clients'],
- transport=self._transport, plugins=self._plugins)
- try:
- result = client.service.EstatusCuenta(**data)
- except Fault as e:
- log.error(str(e))
- return
- #~ print (result)
- return result.Estatus
-
-
-class Finkok(object):
-
- def __init__(self, auth={}):
- self.codes = URL['codes']
- self.error = ''
- self.message = ''
- self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT)
- self._plugins = None
- self._history = None
- self.uuid = ''
- self.fecha = None
- if DEBUG:
- self._history = HistoryPlugin()
- self._plugins = [self._history]
- self._auth = AUTH
- else:
- self._auth = auth
-
- def _debug(self):
- if not DEBUG:
- return
- print('SEND', self._history.last_sent)
- print('RESULT', self._history.last_received)
- return
-
- def _check_result(self, method, result):
- # ~ print ('CODE', result.CodEstatus)
- # ~ print ('INCIDENCIAS', result.Incidencias)
- self.message = ''
- MSG = {
- 'OK': 'Comprobante timbrado satisfactoriamente',
- '307': 'Comprobante timbrado previamente',
- }
- status = result.CodEstatus
- if status is None and result.Incidencias:
- for i in result.Incidencias['Incidencia']:
- self.error += 'Error: {}\n{}\n{}'.format(
- i['CodigoError'], i['MensajeIncidencia'], i['ExtraInfo'])
- return ''
-
- if method == 'timbra' and status in (MSG['OK'], MSG['307']):
- #~ print ('UUID', result.UUID)
- #~ print ('FECHA', result.Fecha)
- if status == MSG['307']:
- self.message = MSG['307']
- tree = parseString(result.xml)
- response = tree.toprettyxml(encoding='utf-8').decode('utf-8')
- self.uuid = result.UUID
- self.fecha = result.Fecha
-
- return response
-
- def _load_file(self, path):
- try:
- with open(path, 'rb') as f:
- data = f.read()
- except Exception as e:
- self.error = str(e)
- return
- return data
-
- def _validate_xml(self, file_xml):
- if os.path.isfile(file_xml):
- try:
- with open(file_xml, 'rb') as f:
- xml = f.read()
- except Exception as e:
- self.error = str(e)
- return False, ''
- else:
- xml = file_xml.encode('utf-8')
- return True, xml
-
- def _validate_uuid(self, uuid):
- try:
- UUID(uuid)
- return True
- except ValueError:
- self.error = 'UUID no válido: {}'.format(uuid)
- return False
-
- def timbra_xml(self, file_xml):
- self.error = ''
-
- if not DEBUG and not self._auth:
- self.error = 'Sin datos para timbrar'
- return
-
- method = 'timbra'
- ok, xml = self._validate_xml(file_xml)
- if not ok:
- return ''
- client = Client(
- URL[method], transport=self._transport, plugins=self._plugins)
-
- args = {
- 'username': self._auth['USER'],
- 'password': self._auth['PASS'],
- 'xml': xml,
- }
- if URL['quick_stamp']:
- try:
- result = client.service.quick_stamp(**args)
- except Fault as e:
- self.error = str(e)
- return
- else:
- try:
- result = client.service.stamp(**args)
- except Fault as e:
- self.error = str(e)
- return
- except TransportError as e:
- if '413' in str(e):
- self.error = '413
Documento muy grande para timbrar'
- else:
- self.error = str(e)
- return
- except ConnectionError as e:
- msg = '502 - Error de conexión'
- self.error = msg
- return
-
- return self._check_result(method, result)
-
- def _get_xml(self, uuid):
- if not self._validate_uuid(uuid):
- return ''
-
- method = 'util'
- client = Client(
- URL[method], transport=self._transport, plugins=self._plugins)
-
- args = {
- 'username': self._auth['USER'],
- 'password': self._auth['PASS'],
- 'uuid': uuid,
- 'taxpayer_id': self.rfc,
- 'invoice_type': 'I',
- }
- try:
- result = client.service.get_xml(**args)
- except Fault as e:
- self.error = str(e)
- return ''
- except TransportError as e:
- self.error = str(e)
- return ''
-
- if result.error:
- self.error = result.error
- return ''
-
- tree = parseString(result.xml)
- xml = tree.toprettyxml(encoding='utf-8').decode('utf-8')
- return xml
-
- def recupera_xml(self, file_xml='', uuid=''):
- self.error = ''
- if uuid:
- return self._get_xml(uuid)
-
- method = 'timbra'
- ok, xml = self._validate_xml(file_xml)
- if not ok:
- return ''
- client = Client(
- URL[method], transport=self._transport, plugins=self._plugins)
- try:
- result = client.service.stamped(
- xml, self._auth['user'], self._auth['pass'])
- except Fault as e:
- self.error = str(e)
- return ''
-
- return self._check_result(method, result)
-
- def estatus_xml(self, uuid):
- method = 'timbra'
- if not self._validate_uuid(uuid):
- return ''
- client = Client(
- URL[method], transport=self._transport, plugins=self._plugins)
- try:
- result = client.service.query_pending(
- self._auth['USER'], self._auth['PASS'], uuid)
- return result.status
- except Fault as e:
- self.error = str(e)
- return ''
-
- def cancel_xml(self, rfc, uuid, cer, key):
- # ~ for u in uuids:
- # ~ if not self._validate_uuid(u):
- # ~ return ''
-
- method = 'cancel'
- client = Client(
- URL[method], transport=self._transport, plugins=self._plugins)
- uuid_type = client.get_type('ns1:UUIDS')
- sa = client.get_type('ns0:stringArray')
-
- args = {
- 'UUIDS': uuid_type(uuids=sa(string=uuid)),
- 'username': self._auth['USER'],
- 'password': self._auth['PASS'],
- 'taxpayer_id': rfc,
- 'cer': cer,
- 'key': key,
- 'store_pending': False,
- }
- try:
- result = client.service.cancel(**args)
- except Fault as e:
- self.error = str(e)
- return ''
-
- if result.CodEstatus and self.codes['205'] in result.CodEstatus:
- self.error = result.CodEstatus
- return ''
-
- return result
-
- def cancel_signature(self, file_xml):
- method = 'cancel'
- if os.path.isfile(file_xml):
- root = etree.parse(file_xml).getroot()
- else:
- root = etree.fromstring(file_xml.encode())
-
- xml = etree.tostring(root)
-
- client = Client(
- URL[method], transport=self._transport, plugins=self._plugins)
-
- args = {
- 'username': self._auth['USER'],
- 'password': self._auth['PASS'],
- 'xml': xml,
- 'store_pending': False,
- }
-
- 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:
- if not self._validate_uuid(u):
- return ''
-
- method = 'cancel'
- client = Client(
- URL[method], transport=self._transport, plugins=self._plugins)
-
- args = {
- 'username': self._auth['USER'],
- 'password': self._auth['PASS'],
- 'taxpayer_id': rfc,
- 'uuid': '',
- 'type': type_acuse,
- }
- try:
- result = []
- for u in uuids:
- args['uuid'] = u
- r = client.service.get_receipt(**args)
- result.append(r)
- except Fault as e:
- self.error = str(e)
- return ''
-
- return result
-
- def estatus_cancel(self, uuids):
- for u in uuids:
- if not self._validate_uuid(u):
- return ''
-
- method = 'cancel'
- client = Client(
- URL[method], transport=self._transport, plugins=self._plugins)
-
- args = {
- 'username': self._auth['USER'],
- 'password': self._auth['PASS'],
- 'uuid': '',
- }
- try:
- result = []
- for u in uuids:
- args['uuid'] = u
- r = client.service.query_pending_cancellation(**args)
- result.append(r)
- except Fault as e:
- self.error = str(e)
- return ''
-
- 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'],
- 'name': rfc,
- 'token_username': email,
- 'taxpayer_id': rfc,
- 'status': True,
- }
- try:
- result = client.service.add_token(**args)
- except Fault as e:
- self.error = str(e)
- return ''
-
- return result
-
- def get_date(self):
- method = 'util'
- client = Client(
- URL[method], transport=self._transport, plugins=self._plugins)
- try:
- result = client.service.datetime(AUTH['USER'], AUTH['PASS'])
- except Fault as e:
- self.error = str(e)
- return ''
-
- if result.error:
- self.error = result.error
- return
-
- 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
-
- 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'],
- 'taxpayer_id': rfc,
- 'type_user': tu[type_user],
- 'added': datetime.datetime.now().isoformat()[:19],
- }
- try:
- result = client.service.add(**args)
- except Fault as e:
- self.error = str(e)
- return ''
-
- return result
-
- def edit_client(self, rfc, status=True):
- """
- 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'],
- 'taxpayer_id': rfc,
- 'status': sv[status],
- }
- try:
- result = client.service.edit(**args)
- except Fault as e:
- self.error = str(e)
- return ''
-
- 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'],
- 'taxpayer_id': rfc,
- }
-
- try:
- result = client.service.get(**args)
- except Fault as e:
- self.error = str(e)
- return ''
- except TransportError as e:
- self.error = str(e)
- return ''
-
- 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'],
- 'taxpayer_id': rfc,
- 'credit': credit,
- }
- try:
- result = client.service.assign(**args)
- except Fault as e:
- self.error = str(e)
- return ''
-
- return result
-
- def client_get_timbres(self, rfc):
- method = 'client'
- client = Client(
- URL[method], transport=self._transport, plugins=self._plugins)
- args = {
- 'reseller_username': self._auth['USER'],
- 'reseller_password': self._auth['PASS'],
- 'taxpayer_id': rfc,
- }
-
- try:
- self.result = client.service.get(**args)
- except Fault as e:
- self.error = str(e)
- return 0
- except TransportError as e:
- self.error = str(e)
- return 0
- except ConnectionError:
- self.error = 'Verifica la conexión a internet'
- return 0
-
- success = bool(self.result.users)
- if not success:
- self.error = self.result.message or 'RFC no existe'
- return 0
-
- return self.result.users.ResellerUser[0].credit
-
-
-def _get_data_sat(path):
- BF = 'string(//*[local-name()="{}"]/@{})'
- NS_CFDI = {'cfdi': 'http://www.sat.gob.mx/cfd/3'}
-
- try:
- if os.path.isfile(path):
- tree = etree.parse(path).getroot()
- else:
- tree = etree.fromstring(path.encode())
-
- data = {}
- emisor = escape(
- tree.xpath('string(//cfdi:Emisor/@rfc)', namespaces=NS_CFDI) or
- tree.xpath('string(//cfdi:Emisor/@Rfc)', namespaces=NS_CFDI)
- )
- receptor = escape(
- tree.xpath('string(//cfdi:Receptor/@rfc)', namespaces=NS_CFDI) or
- tree.xpath('string(//cfdi:Receptor/@Rfc)', namespaces=NS_CFDI)
- )
- data['total'] = tree.get('total') or tree.get('Total')
- data['emisor'] = emisor
- data['receptor'] = receptor
- data['uuid'] = tree.xpath(BF.format('TimbreFiscalDigital', 'UUID'))
- except Exception as e:
- print (e)
- return {}
-
- return '?re={emisor}&rr={receptor}&tt={total}&id={uuid}'.format(**data)
-
-
-def get_status_sat(xml):
- data = _get_data_sat(xml)
- if not data:
- return 'XML inválido'
-
- data = """
-
-
-
-
-
- {}
-
-
-
- """.format(data)
- headers = {
- 'SOAPAction': '"http://tempuri.org/IConsultaCFDIService/Consulta"',
- 'Content-type': 'text/xml; charset="UTF-8"'
- }
- URL = 'https://consultaqr.facturaelectronica.sat.gob.mx/consultacfdiservice.svc'
-
- try:
- result = requests.post(URL, data=data, headers=headers)
- tree = etree.fromstring(result.text)
- node = tree.xpath("//*[local-name() = 'Estado']")[0]
- except Exception as e:
- return 'Error: {}'.format(str(e))
-
- return node.text
-
-
-def main():
- return
-
-
-if __name__ == '__main__':
- main()
diff --git a/source/app/controllers/pacs/__init__.py b/source/app/controllers/pacs/__init__.py
new file mode 100644
index 0000000..05445b6
--- /dev/null
+++ b/source/app/controllers/pacs/__init__.py
@@ -0,0 +1,4 @@
+#!/usr/bin/env python
+
+from .comerciodigital import PACComercioDigital
+from .finkok import PACFinkok
diff --git a/source/app/controllers/pacs/cfdi_cert.py b/source/app/controllers/pacs/cfdi_cert.py
new file mode 100644
index 0000000..a19fe7e
--- /dev/null
+++ b/source/app/controllers/pacs/cfdi_cert.py
@@ -0,0 +1,261 @@
+#!/usr/bin/env python3
+
+import argparse
+import base64
+import datetime
+import getpass
+from pathlib import Path
+
+import xmlsec
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography import x509
+from cryptography.x509.oid import NameOID
+from cryptography.x509.oid import ExtensionOID
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric import padding
+
+from .conf import TOKEN
+
+
+class SATCertificate(object):
+
+ def __init__(self, cer=b'', key=b'', password=''):
+ self._error = ''
+ self._init_values()
+ self._get_data_cer(cer)
+ self._get_data_key(key, password)
+
+ def _init_values(self):
+ self._rfc = ''
+ self._serial_number = ''
+ self._not_before = None
+ self._not_after = None
+ self._is_fiel = False
+ self._are_couple = False
+ self._is_valid_time = False
+ self._cer = b''
+ self._cer_pem = ''
+ self._cer_txt = ''
+ self._key_enc = b''
+ self._p12 = b''
+ self._cer_modulus = 0
+ self._key_modulus = 0
+ return
+
+ def __str__(self):
+ msg = '\tRFC: {}\n'.format(self.rfc)
+ msg += '\tNo de Serie: {}\n'.format(self.serial_number)
+ msg += '\tVálido desde: {}\n'.format(self.not_before)
+ msg += '\tVálido hasta: {}\n'.format(self.not_after)
+ msg += '\tEs vigente: {}\n'.format(self.is_valid_time)
+ msg += '\tSon pareja: {}\n'.format(self.are_couple)
+ msg += '\tEs FIEL: {}\n'.format(self.is_fiel)
+ return msg
+
+ def __bool__(self):
+ return self.is_valid
+
+ def _get_hash(self):
+ digest = hashes.Hash(hashes.SHA512(), default_backend())
+ digest.update(self._rfc.encode())
+ digest.update(self._serial_number.encode())
+ digest.update(TOKEN.encode())
+ return digest.finalize()
+
+ def _get_data_cer(self, cer):
+ self._cer = cer
+ obj = x509.load_der_x509_certificate(cer, default_backend())
+ self._rfc = obj.subject.get_attributes_for_oid(
+ NameOID.X500_UNIQUE_IDENTIFIER)[0].value.split(' ')[0]
+ self._serial_number = '{0:x}'.format(obj.serial_number)[1::2]
+ self._not_before = obj.not_valid_before
+ self._not_after = obj.not_valid_after
+ now = datetime.datetime.utcnow()
+ self._is_valid_time = (now > self.not_before) and (now < self.not_after)
+ if not self._is_valid_time:
+ msg = 'El certificado no es vigente'
+ self._error = msg
+
+ self._is_fiel = obj.extensions.get_extension_for_oid(
+ ExtensionOID.KEY_USAGE).value.key_agreement
+
+ self._cer_pem = obj.public_bytes(serialization.Encoding.PEM).decode()
+ self._cer_txt = ''.join(self._cer_pem.split('\n')[1:-2])
+ self._cer_modulus = obj.public_key().public_numbers().n
+ return
+
+ def _get_data_key(self, key, password):
+ self._key_enc = key
+ if not key or not password:
+ return
+
+ try:
+ obj = serialization.load_der_private_key(
+ key, password.encode(), default_backend())
+ except ValueError:
+ msg = 'La contraseña es incorrecta'
+ self._error = msg
+ return
+
+ p = self._get_hash()
+ self._key_enc = obj.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.BestAvailableEncryption(p)
+ )
+
+ self._key_modulus = obj.public_key().public_numbers().n
+ self._are_couple = self._cer_modulus == self._key_modulus
+ if not self._are_couple:
+ msg = 'El CER y el KEY no son pareja'
+ self._error = msg
+ return
+
+ def _get_key(self, password):
+ if not password:
+ password = self._get_hash()
+ private_key = serialization.load_pem_private_key(
+ self._key_enc, password=password, backend=default_backend())
+ return private_key
+
+ def _get_key_pem(self):
+ obj = self._get_key('')
+ key_pem = obj.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.NoEncryption()
+ )
+ return key_pem
+
+ # Not work
+ def _get_p12(self):
+ obj = serialization.pkcs12.serialize_key_and_certificates('test',
+ self.key_pem, self.cer_pem, None,
+ encryption_algorithm=serialization.NoEncryption()
+ )
+ return obj
+
+ def sign(self, data, password=''):
+ private_key = self._get_key(password)
+ firma = private_key.sign(data, padding.PKCS1v15(), hashes.SHA256())
+ return base64.b64encode(firma).decode()
+
+ def sign_xml(self, tree):
+ node = xmlsec.tree.find_node(tree, xmlsec.constants.NodeSignature)
+ ctx = xmlsec.SignatureContext()
+ key = xmlsec.Key.from_memory(self.key_pem, xmlsec.constants.KeyDataFormatPem)
+ ctx.key = key
+ ctx.sign(node)
+ node = xmlsec.tree.find_node(tree, 'X509Certificate')
+ node.text = self.cer_txt
+ return tree
+
+ @property
+ def rfc(self):
+ return self._rfc
+
+ @property
+ def serial_number(self):
+ return self._serial_number
+
+ @property
+ def not_before(self):
+ return self._not_before
+
+ @property
+ def not_after(self):
+ return self._not_after
+
+ @property
+ def is_fiel(self):
+ return self._is_fiel
+
+ @property
+ def are_couple(self):
+ return self._are_couple
+
+ @property
+ def is_valid(self):
+ return not bool(self.error)
+
+ @property
+ def is_valid_time(self):
+ return self._is_valid_time
+
+ @property
+ def cer(self):
+ return self._cer
+
+ @property
+ def cer_pem(self):
+ return self._cer_pem.encode()
+
+ @property
+ def cer_txt(self):
+ return self._cer_txt
+
+ @property
+ def key_pem(self):
+ return self._get_key_pem()
+
+ @property
+ def key_enc(self):
+ return self._key_enc
+
+ @property
+ def p12(self):
+ return self._get_p12()
+
+ @property
+ def error(self):
+ return self._error
+
+
+def main(args):
+ # ~ contra = getpass.getpass('Introduce la contraseña del archivo KEY: ')
+ contra = '12345678a'
+ if not contra.strip():
+ msg = 'La contraseña es requerida'
+ print(msg)
+ return
+
+ path_cer = Path(args.cer)
+ path_key = Path(args.key)
+
+ if not path_cer.is_file():
+ msg = 'El archivo CER es necesario'
+ print(msg)
+ return
+
+ if not path_key.is_file():
+ msg = 'El archivo KEY es necesario'
+ print(msg)
+ return
+
+ cer = path_cer.read_bytes()
+ key = path_key.read_bytes()
+ cert = SATCertificate(cer, key, contra)
+
+ if cert.error:
+ print(cert.error)
+ else:
+ print(cert)
+ return
+
+
+def _process_command_line_arguments():
+ parser = argparse.ArgumentParser(description='CFDI Certificados')
+
+ help = 'Archivo CER'
+ parser.add_argument('-c', '--cer', help=help, default='')
+ help = 'Archivo KEY'
+ parser.add_argument('-k', '--key', help=help, default='')
+
+ args = parser.parse_args()
+ return args
+
+
+if __name__ == '__main__':
+ args = _process_command_line_arguments()
+ main(args)
diff --git a/source/app/controllers/comercio/__init__.py b/source/app/controllers/pacs/comerciodigital/__init__.py
similarity index 100%
rename from source/app/controllers/comercio/__init__.py
rename to source/app/controllers/pacs/comerciodigital/__init__.py
diff --git a/source/app/controllers/comercio/comercio.py b/source/app/controllers/pacs/comerciodigital/comercio.py
similarity index 83%
rename from source/app/controllers/comercio/comercio.py
rename to source/app/controllers/pacs/comerciodigital/comercio.py
index 8836156..365b47e 100644
--- a/source/app/controllers/comercio/comercio.py
+++ b/source/app/controllers/pacs/comerciodigital/comercio.py
@@ -23,6 +23,8 @@ import lxml.etree as ET
import requests
from requests.exceptions import ConnectionError
+from .conf import DEBUG, AUTH
+
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
LOG_DATE = '%d/%m/%Y %H:%M:%S'
@@ -31,16 +33,10 @@ 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('requests').setLevel(logging.ERROR)
-try:
- from .conf import DEBUG, AUTH
-except ImportError:
- DEBUG = False
- log.debug('Need make conf.py')
-
-
TIMEOUT = 10
@@ -51,6 +47,7 @@ class PACComercioDigital(object):
'timbra': ws.format('ws', 'timbre/timbrarV5.aspx'),
'cancel': ws.format('cancela', 'cancela3/cancelarUuid'),
'cancelxml': ws.format('cancela', 'cancela3/cancelarXml'),
+ 'status': ws.format('cancela', 'arws/consultaEstatus'),
'client': api.format('x3/altaEmpresa'),
'saldo': api.format('x3/saldo'),
'timbres': api.format('x3/altaTimbres'),
@@ -59,6 +56,7 @@ class PACComercioDigital(object):
'000': '000 Exitoso',
'004': '004 RFC {} ya esta dado de alta con Estatus=A',
'704': '704 Usuario Invalido',
+ '702': '702 Error rfc/empresa invalido',
}
NS_CFDI = {
'cfdi': 'http://www.sat.gob.mx/cfd/3',
@@ -67,10 +65,12 @@ class PACComercioDigital(object):
if DEBUG:
ws = 'https://pruebas.comercio-digital.mx/{}'
+ ws6 = 'https://pruebas6.comercio-digital.mx/arws/{}'
URL = {
'timbra': ws.format('timbre/timbrarV5.aspx'),
'cancel': ws.format('cancela3/cancelarUuid'),
'cancelxml': ws.format('cancela3/cancelarXml'),
+ 'status': ws6.format('consultaEstatus'),
'client': api.format('x3/altaEmpresa'),
'saldo': api.format('x3/saldo'),
'timbres': api.format('x3/altaTimbres'),
@@ -78,8 +78,8 @@ class PACComercioDigital(object):
def __init__(self):
self.error = ''
- self.cfdi_uuid = ''
- self.date_stamped = ''
+ # ~ self.cfdi_uuid = ''
+ # ~ self.date_stamped = ''
def _error(self, msg):
self.error = str(msg)
@@ -133,21 +133,26 @@ class PACComercioDigital(object):
xml = result.content
tree = ET.fromstring(xml)
- self.cfdi_uuid = tree.xpath(
+ cfdi_uuid = tree.xpath(
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
namespaces=self.NS_CFDI)
- self.date_stamped = tree.xpath(
+ date_stamped = tree.xpath(
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@FechaTimbrado)',
namespaces=self.NS_CFDI)
- return xml.decode()
+ data = {
+ 'xml': xml.decode(),
+ 'uuid': cfdi_uuid,
+ 'date': date_stamped,
+ }
+ return data
def _get_data_cancel(self, cfdi, info, auth):
NS_CFDI = {
'cfdi': 'http://www.sat.gob.mx/cfd/3',
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
}
- tree = ET.fromstring(cfdi)
+ tree = ET.fromstring(cfdi.encode())
tipo = tree.xpath(
'string(//cfdi:Comprobante/@TipoDeComprobante)',
namespaces=NS_CFDI)
@@ -197,15 +202,15 @@ class PACComercioDigital(object):
self._error(result.headers['errmsg'])
return ''
- return result.content
+ return result.text
def _get_headers_cancel_xml(self, cfdi, info, auth):
NS_CFDI = {
'cfdi': 'http://www.sat.gob.mx/cfd/3',
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
}
- tree = ET.fromstring(cfdi)
- tipo = tree.xpath(
+ tree = ET.fromstring(cfdi.encode())
+ tipocfdi = tree.xpath(
'string(//cfdi:Comprobante/@TipoDeComprobante)',
namespaces=NS_CFDI)
total = tree.xpath(
@@ -220,15 +225,16 @@ class PACComercioDigital(object):
'pwdws': auth['pass'],
'rfcr': rfc_receptor,
'total': total,
- 'tipocfdi': tipo,
+ 'tipocfdi': tipocfdi,
}
headers.update(info)
return headers
- def cancel_xml(self, cfdi, xml, info, auth={}):
- if not auth:
+ def cancel_xml(self, xml, auth={}, cfdi='', info={'tipo': 'cfdi3.3'}):
+ if DEBUG or not auth:
auth = AUTH
+
url = self.URL['cancelxml']
headers = self._get_headers_cancel_xml(cfdi, info, auth)
result = self._post(url, xml, headers)
@@ -243,7 +249,39 @@ class PACComercioDigital(object):
self._error(result.headers['errmsg'])
return ''
- return result.content
+ tree = ET.fromstring(result.text)
+ date_cancel = tree.xpath('string(//Acuse/@Fecha)')[:19]
+
+ data = {
+ 'acuse': result.text,
+ 'date': date_cancel,
+ }
+ return data
+
+ def status(self, data, auth={}):
+ if not auth:
+ auth = AUTH
+ url = self.URL['status']
+
+ data = (
+ f"USER={auth['user']}",
+ f"PWDW={auth['pass']}",
+ f"RFCR={data['rfc_receptor']}",
+ f"RFCE={data['rfc_emisor']}",
+ f"TOTAL={data['total']}",
+ f"UUID={data['uuid']}",
+ )
+ data = '\n'.join(data)
+ result = self._post(url, data)
+
+ if result is None:
+ return ''
+
+ if result.status_code != 200:
+ self._error(result.status_code)
+ return self.error
+
+ return result.text
def _get_data_client(self, auth, values):
data = [f"usr_ws={auth['user']}", f"pwd_ws={auth['pass']}"]
@@ -299,6 +337,7 @@ class PACComercioDigital(object):
'Host': host,
'Connection' : 'Keep-Alive',
}
+ data = {'usr': data['rfc'], 'pwd': data['password']}
try:
result = requests.get(url, params=data, headers=headers, timeout=TIMEOUT)
except ConnectionError as e:
@@ -312,6 +351,10 @@ class PACComercioDigital(object):
self._error(result.text)
return ''
+ if result.text == self.CODES['702']:
+ self._error(result.text)
+ return ''
+
return result.text
def client_add_timbres(self, data, auth={}):
diff --git a/source/app/controllers/comercio/conf.py.example b/source/app/controllers/pacs/comerciodigital/conf.py.example
similarity index 77%
rename from source/app/controllers/comercio/conf.py.example
rename to source/app/controllers/pacs/comerciodigital/conf.py.example
index de81efb..6006207 100644
--- a/source/app/controllers/comercio/conf.py.example
+++ b/source/app/controllers/pacs/comerciodigital/conf.py.example
@@ -17,14 +17,11 @@
# ~ along with this program. If not, see .
-# ~ Siempre consulta la documentación de Finkok
-# ~ AUTH = Puedes usar credenciales genericas para timbrar, o exclusivas para
-# ~ cada emisor
-# ~ RESELLER = Algunos procesos como agregar emisores, solo pueden ser usadas
-# ~ con una cuenta de reseller
+# ~ Siempre consulta la documentación de PAC
+# ~ AUTH = Las credenciales de timbrado proporcionadas por el PAC
+# ~ NO cambies las credenciales de prueba
-
-DEBUG = False
+DEBUG = True
AUTH = {
diff --git a/source/app/controllers/pacs/conf.py.example b/source/app/controllers/pacs/conf.py.example
new file mode 100644
index 0000000..deb384c
--- /dev/null
+++ b/source/app/controllers/pacs/conf.py.example
@@ -0,0 +1,6 @@
+#!/usr/bin/env python3
+
+
+DEBUG = False
+
+TOKEN = ''
diff --git a/source/app/controllers/pacs/finkok/__init__.py b/source/app/controllers/pacs/finkok/__init__.py
new file mode 100644
index 0000000..1b61fc3
--- /dev/null
+++ b/source/app/controllers/pacs/finkok/__init__.py
@@ -0,0 +1,3 @@
+#!/usr/bin/env python3
+
+from .finkok import PACFinkok
diff --git a/source/app/controllers/pacs/finkok/conf.py.example b/source/app/controllers/pacs/finkok/conf.py.example
new file mode 100644
index 0000000..af7b74c
--- /dev/null
+++ b/source/app/controllers/pacs/finkok/conf.py.example
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# ~
+# ~ PAC
+# ~ Copyright (C) 2018-2019 Mauricio Baeza Servin - public [AT] elmau [DOT] net
+# ~
+# ~ This program is free software: you can redistribute it and/or modify
+# ~ it under the terms of the GNU General Public License as published by
+# ~ the Free Software Foundation, either version 3 of the License, or
+# ~ (at your option) any later version.
+# ~
+# ~ This program is distributed in the hope that it will be useful,
+# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# ~ GNU General Public License for more details.
+# ~
+# ~ You should have received a copy of the GNU General Public License
+# ~ along with this program. If not, see .
+
+
+# ~ Siempre consulta la documentación de PAC
+# ~ AUTH = Las credenciales de timbrado proporcionadas por el PAC
+# ~ NO cambies las credenciales de prueba
+
+
+DEBUG = True
+
+
+AUTH = {
+ 'user': '',
+ 'pass': '',
+ 'RESELLER': {
+ 'user': '',
+ 'pass': ''
+ }
+}
+
+
+if DEBUG:
+ AUTH = {
+ 'user': 'pruebas-finkok@correolibre.net',
+ 'pass': '5c9a88da105bff9a8c430cb713f6d35269f51674bdc5963c1501b7316366',
+ 'RESELLER': {
+ 'user': '',
+ 'pass': ''
+ }
+ }
diff --git a/source/app/controllers/pacs/finkok/finkok.py b/source/app/controllers/pacs/finkok/finkok.py
new file mode 100644
index 0000000..e9bfa6f
--- /dev/null
+++ b/source/app/controllers/pacs/finkok/finkok.py
@@ -0,0 +1,559 @@
+#!/usr/bin/env python
+# ~
+# ~ PAC
+# ~ Copyright (C) 2018-2019 Mauricio Baeza Servin - public [AT] elmau [DOT] net
+# ~
+# ~ This program is free software: you can redistribute it and/or modify
+# ~ it under the terms of the GNU General Public License as published by
+# ~ the Free Software Foundation, either version 3 of the License, or
+# ~ (at your option) any later version.
+# ~
+# ~ This program is distributed in the hope that it will be useful,
+# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# ~ GNU General Public License for more details.
+# ~
+# ~ You should have received a copy of the GNU General Public License
+# ~ along with this program. If not, see .
+
+import base64
+import datetime
+import logging
+import os
+import re
+from io import BytesIO
+from xml.sax.saxutils import unescape
+
+import lxml.etree as ET
+from zeep import Client
+from zeep.plugins import Plugin
+from zeep.cache import SqliteCache
+from zeep.transports import Transport
+from zeep.exceptions import Fault, TransportError
+from requests.exceptions import ConnectionError
+
+from .conf import DEBUG, AUTH
+
+
+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('requests').setLevel(logging.ERROR)
+logging.getLogger('zeep').setLevel(logging.ERROR)
+
+
+TIMEOUT = 10
+DEBUG_SOAP = False
+
+
+class DebugPlugin(Plugin):
+
+ def _to_string(self, envelope, name):
+ if DEBUG_SOAP:
+ data = ET.tostring(envelope, pretty_print=True, encoding='utf-8').decode()
+ path = f'/tmp/soap_{name}.xml'
+ with open(path, 'w') as f:
+ f.write(data)
+ return
+
+ def egress(self, envelope, http_headers, operation, binding_options):
+ self._to_string(envelope, 'request')
+ return envelope, http_headers
+
+ def ingress(self, envelope, http_headers, operation):
+ self._to_string(envelope, 'response')
+ return envelope, http_headers
+
+
+class PACFinkok(object):
+ WS = 'https://facturacion.finkok.com/servicios/soap/{}.wsdl'
+ if DEBUG:
+ WS = 'http://demo-facturacion.finkok.com/servicios/soap/{}.wsdl'
+ URL = {
+ 'quick_stamp': False,
+ 'timbra': WS.format('stamp'),
+ 'cancel': WS.format('cancel'),
+ 'client': WS.format('registration'),
+ 'util': WS.format('utilities'),
+ }
+ CODE = {
+ '200': 'Comprobante timbrado satisfactoriamente',
+ '205': 'No Encontrado',
+ '307': 'Comprobante timbrado previamente',
+ '702': 'No se encontro el RFC del emisor',
+ 'IP': 'Invalid Passphrase',
+ 'IPMSG': 'Frase de paso inválida',
+ 'NE': 'No Encontrado',
+ }
+
+ def __init__(self):
+ self._error = ''
+ self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT)
+ self._plugins = [DebugPlugin()]
+
+ @property
+ def error(self):
+ return self._error
+
+ def _validate_result(self, result):
+ if hasattr(result, 'CodEstatus'):
+ ce = result.CodEstatus
+ if ce is None:
+ return result
+
+ if ce == self.CODE['IP']:
+ self._error = self.CODE['IPMSG']
+ return {}
+
+ if self.CODE['NE'] in ce:
+ self._error = 'UUID ' + self.CODE['NE']
+ return {}
+
+ if ce == 'UUID Not Found':
+ self._error = 'UUID ' + self.CODE['NE']
+ return {}
+
+ if self.CODE['200'] != ce:
+ log.error('CodEstatus', type(ce), ce)
+ return result
+
+ if hasattr(result, 'Incidencias'):
+ fault = result.Incidencias.Incidencia[0]
+ cod_error = fault.CodigoError.encode('utf-8')
+ msg_error = fault.MensajeIncidencia.encode('utf-8')
+ error = 'Error: {}\n{}'.format(cod_error, msg_error)
+ self._error = self.CODE.get(cod_error, error)
+ return {}
+
+ return result
+
+ def _get_result(self, client, method, args):
+ self._error = ''
+ try:
+ result = getattr(client.service, method)(**args)
+ except Fault as e:
+ self._error = str(e)
+ return {}
+ except TransportError as e:
+ if '413' in str(e):
+ self._error = '413
Documento muy grande para timbrar'
+ else:
+ self._error = str(e)
+ return {}
+ except ConnectionError as e:
+ msg = '502 - Error de conexión'
+ self._error = msg
+ return {}
+
+ return self._validate_result(result)
+
+ def _to_string(self, data):
+ root = ET.parse(BytesIO(data.encode('utf-8'))).getroot()
+ xml = ET.tostring(root,
+ pretty_print=True, xml_declaration=True, encoding='utf-8')
+ return xml.decode('utf-8')
+
+ def stamp(self, cfdi, auth={}):
+ if DEBUG or not auth:
+ auth = AUTH
+
+ method = 'timbra'
+ client = Client(self.URL[method],
+ transport=self._transport, plugins=self._plugins)
+ args = {
+ 'username': auth['user'],
+ 'password': auth['pass'],
+ 'xml': cfdi.encode('utf-8'),
+ }
+ result = self._get_result(client, 'stamp', args)
+ if self.error:
+ log.error(self.error)
+ return ''
+
+ data = {
+ 'xml': self._to_string(result.xml),
+ 'uuid': result.UUID,
+ 'date': result.Fecha,
+ }
+ return data
+
+ def _get_data_cancel(self, cfdi):
+ NS_CFDI = {
+ 'cfdi': 'http://www.sat.gob.mx/cfd/3',
+ 'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
+ }
+ tree = ET.fromstring(cfdi.encode())
+ rfc_emisor = tree.xpath(
+ 'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)',
+ namespaces=NS_CFDI)
+ cfdi_uuid = tree.xpath(
+ 'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
+ namespaces=NS_CFDI)
+ return rfc_emisor, cfdi_uuid
+
+ def cancel(self, cfdi, info, auth={}):
+ if DEBUG or not auth:
+ auth = AUTH
+
+ rfc_emisor, cfdi_uuid = self._get_data_cancel(cfdi)
+ method = 'cancel'
+ client = Client(self.URL[method],
+ transport=self._transport, plugins=self._plugins)
+ uuid_type = client.get_type('ns1:UUIDS')
+ sa = client.get_type('ns0:stringArray')
+
+ args = {
+ 'UUIDS': uuid_type(uuids=sa(string=cfdi_uuid)),
+ 'username': auth['user'],
+ 'password': auth['pass'],
+ 'taxpayer_id': rfc_emisor,
+ 'cer': info['cer'],
+ 'key': info['key'],
+ 'store_pending': False,
+ }
+
+ result = self._get_result(client, 'cancel', args)
+ if self.error:
+ log.error(self.error)
+ return ''
+
+ folio = result['Folios']['Folio'][0]
+ status = folio['EstatusUUID']
+ if status != '201':
+ log.debug(f'Cancel status: {status} - {cfdi_uuid}')
+
+ data = {
+ 'acuse': result['Acuse'],
+ 'date': result['Fecha'],
+ }
+ return data
+
+ def cancel_xml(self, xml, auth={}, cfdi=''):
+ if DEBUG or not auth:
+ auth = AUTH
+
+ method = 'cancel'
+ client = Client(self.URL[method],
+ transport=self._transport, plugins=self._plugins)
+ client.set_ns_prefix('can', 'http://facturacion.finkok.com/cancel')
+ # ~ xml = f'\n{xml}'
+ # ~ xml = f'\n{xml}'
+ args = {
+ 'xml': xml.encode(),
+ 'username': auth['user'],
+ 'password': auth['pass'],
+ 'store_pending': False,
+ }
+ result = self._get_result(client, 'cancel_signature', args)
+ if self.error:
+ log.error(self.error)
+ return ''
+
+ folio = result['Folios']['Folio'][0]
+ status = folio['EstatusUUID']
+
+ if status == '708':
+ self._error = 'Error 708 del SAT, intenta más tarde.'
+ log.error(self.error)
+ return ''
+
+ if status != '201':
+ log.debug(f'Cancel status: {status} -')
+
+ data = {
+ 'acuse': result['Acuse'],
+ 'date': result['Fecha'],
+ }
+ return data
+
+ def client_add(self, rfc, type_user=False):
+ """Agrega un nuevo cliente para timbrado.
+ Se requiere cuenta de reseller para usar este método
+
+ Args:
+ rfc (str): El RFC del nuevo cliente
+
+ Kwargs:
+ type_user (bool):
+ False == 'P' == Prepago
+ True == 'O' == On demand
+
+ Returns:
+ True or False
+
+ origin PAC
+ 'message':
+ 'Account Created successfully'
+ 'Account Already exists'
+ 'success': True or False
+ """
+ auth = AUTH['RESELLER']
+ tu = {True: 'O', False: 'P'}
+
+ method = 'client'
+ client = Client(
+ self.URL[method], transport=self._transport, plugins=self._plugins)
+
+ args = {
+ 'reseller_username': auth['user'],
+ 'reseller_password': auth['pass'],
+ 'taxpayer_id': rfc,
+ 'type_user': tu[type_user],
+ 'added': datetime.datetime.now().isoformat()[:19],
+ }
+
+ result = self._get_result(client, 'add', args)
+ if self.error:
+ return False
+
+ if not result.success:
+ self.error = result.message
+ return False
+
+ # ~ PAC success debería ser False
+ msg = 'Account Already exists'
+ if result.message == msg:
+ self.error = msg
+ return True
+
+ return result.success
+
+ def client_get_token(self, rfc, email):
+ """Genera 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:
+ token (str): Es la contraseña para timbrar
+
+ origin PAC
+ 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(
+ self.URL[method], transport=self._transport, plugins=self._plugins)
+ args = {
+ 'username': auth['user'],
+ 'password': auth['pass'],
+ 'name': rfc,
+ 'token_username': email,
+ 'taxpayer_id': rfc,
+ 'status': True,
+ }
+
+ result = self._get_result(client, 'add_token', args)
+ if self.error:
+ log.error(self.error)
+ return ''
+
+ if not result.success:
+ self.error = result.message
+ log.error(self.error)
+ return ''
+
+ return result.token
+
+ def client_add_timbres(self, rfc, credit):
+ """Agregar credito a un emisor
+
+ Se requiere cuenta de reseller
+
+ 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(
+ self.URL[method], transport=self._transport, plugins=self._plugins)
+ args = {
+ 'username': auth['user'],
+ 'password': auth['pass'],
+ 'taxpayer_id': rfc,
+ 'credit': credit,
+ }
+
+ result = self._get_result(client, 'assign', args)
+ if self.error:
+ log.error(error)
+ return ''
+
+ if not result.success:
+ self.error = result.message
+ return 0
+
+ return result.credit
+
+ def client_balance(self, auth={}, rfc=''):
+ """Regresa los timbres restantes del cliente
+ Se pueden usar las credenciales de relleser o las credenciales del emisor
+
+ Args:
+ rfc (str): El RFC del emisor
+
+ Kwargs:
+ auth (dict): Credenciales del emisor
+
+ Returns:
+ int Cantidad de timbres restantes
+ """
+
+ if not auth:
+ auth = AUTH['RESELLER']
+
+ method = 'client'
+ client = Client(self.URL[method],
+ transport=self._transport, plugins=self._plugins)
+ args = {
+ 'reseller_username': auth['user'],
+ 'reseller_password': auth['pass'],
+ 'taxpayer_id': rfc,
+ }
+
+ result = self._get_result(client, 'get', args)
+ if self.error:
+ log.error(self.error)
+ return ''
+
+ success = bool(result.users)
+ if not success:
+ self.error = result.message or 'RFC no existe'
+ return 0
+
+ return result.users.ResellerUser[0].credit
+
+ def client_set_status(self, rfc, status):
+ """Edita el estatus (Activo o Suspendido) de un cliente
+ Se requiere cuenta de reseller para usar este método
+
+ Args:
+ rfc (str): El RFC del cliente
+
+ Kwargs:
+ status (bool):
+ True == 'A' == Activo
+ False == 'S' == Suspendido
+
+ Returns:
+ dict
+ 'message':
+ 'Account Created successfully'
+ 'Account Already exists'
+ 'success': True or False
+ """
+ auth = AUTH['RESELLER']
+ ts = {True: 'A', False: 'S'}
+ method = 'client'
+ client = Client(self.URL[method],
+ transport=self._transport, plugins=self._plugins)
+
+ args = {
+ 'reseller_username': auth['user'],
+ 'reseller_password': auth['pass'],
+ 'taxpayer_id': rfc,
+ 'status': ts[status],
+ }
+ result = self._get_result(client, 'edit', args)
+
+ if self.error:
+ return False
+
+ if not result.success:
+ self.error = result.message
+ return False
+
+ return True
+
+ def client_switch(self, rfc, type_user):
+ """Edita el tipo de timbrado (OnDemand o Prepago) de un cliente
+ Se requiere cuenta de reseller para usar este método
+
+ Args:
+ rfc (str): El RFC del cliente
+
+ Kwargs:
+ status (bool):
+ True == 'O' == OnDemand
+ False == 'P' == Prepago
+
+ Returns:
+ dict
+ 'message':
+ 'Account Created successfully'
+ 'Account Already exists'
+ 'success': True or False
+ """
+ auth = AUTH['RESELLER']
+ tu = {True: 'O', False: 'P'}
+ method = 'client'
+ client = Client(self.URL[method],
+ transport=self._transport, plugins=self._plugins)
+
+ args = {
+ 'username': auth['user'],
+ 'password': auth['pass'],
+ 'taxpayer_id': rfc,
+ 'type_user': tu[type_user],
+ }
+ result = self._get_result(client, 'switch', args)
+
+ if self.error:
+ return False
+
+ if not result.success:
+ self.error = result.message
+ return False
+
+ return True
+
+ def client_report_folios(self, rfc, date_from, date_to, invoice_type='I'):
+ """Obtiene un reporte del total de facturas timbradas
+ """
+ auth = AUTH['RESELLER']
+
+ args = {
+ 'username': auth['user'],
+ 'password': auth['pass'],
+ 'taxpayer_id': rfc,
+ 'date_from': date_from,
+ 'date_to': date_to,
+ 'invoice_type': invoice_type,
+ }
+
+ method = 'util'
+ client = Client(self.URL[method],
+ transport=self._transport, plugins=self._plugins)
+
+ result = self._get_result(client, 'report_total', args)
+
+ if result.result is None:
+ # ~ PAC - Debería regresar RFC inexistente o sin registros
+ self.error = 'RFC no existe o no tiene registros'
+ return 0
+
+ total = result.result.ReportTotal[0].total
+
+ return total
diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py
index aade77a..af95d32 100644
--- a/source/app/controllers/util.py
+++ b/source/app/controllers/util.py
@@ -68,11 +68,13 @@ from settings import DEBUG, MV, log, template_lookup, COMPANIES, DB_SAT, \
PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PATH_MEDIA, PRE, \
PATH_XMLSEC, TEMPLATE_CANCEL, DEFAULT_SAT_PRODUCTO, DECIMALES, DIR_FACTURAS
-from settings import SEAFILE_SERVER, USAR_TOKEN, API, DECIMALES_TAX
-from .configpac import AUTH
+from settings import USAR_TOKEN, API, DECIMALES_TAX
+# ~ from .configpac import AUTH
# ~ v2
+from .pacs.cfdi_cert import SATCertificate
+
from settings import (
EXT,
MXN,
@@ -395,190 +397,34 @@ def to_slug(string):
return value.replace(' ', '_')
-class Certificado(object):
+# ~ def make_xml(data, certificado):
+ # ~ from .cfdi_xml import CFDI
- def __init__(self, paths):
- self._path_key = paths['path_key']
- self._path_cer = paths['path_cer']
- self._modulus = ''
- self.error = ''
+ # ~ cert = SATCertificate(certificado.cer, certificado.key_enc.encode())
+ # ~ if DEBUG:
+ # ~ data['emisor']['Rfc'] = certificado.rfc
+ # ~ data['emisor']['RegimenFiscal'] = '603'
- def _kill(self, path):
- try:
- os.remove(path)
- except:
- pass
- return
+ # ~ cfdi = CFDI()
+ # ~ xml = cfdi.get_xml(data)
- def _get_info_cer(self, session_rfc):
- data = {}
- args = 'openssl x509 -inform DER -in {}'
- try:
- cer_pem = _call(args.format(self._path_cer))
- except Exception as e:
- self.error = 'No se pudo convertir el CER en PEM'
- return data
+ # ~ data = {
+ # ~ 'xsltproc': PATH_XSLTPROC,
+ # ~ 'xslt': _join(PATH_XSLT, 'cadena.xslt'),
+ # ~ 'xml': save_temp(xml, 'w'),
+ # ~ 'openssl': PATH_OPENSSL,
+ # ~ 'key': save_temp(certificado.key_enc, 'w'),
+ # ~ 'pass': token,
+ # ~ }
+ # ~ args = '"{xsltproc}" "{xslt}" "{xml}" | ' \
+ # ~ '"{openssl}" dgst -sha256 -sign "{key}" -passin pass:"{pass}" | ' \
+ # ~ '"{openssl}" enc -base64 -A'.format(**data)
+ # ~ sello = _call(args)
- args = 'openssl enc -base64 -in {}'
- try:
- cer_txt = _call(args.format(self._path_cer))
- except Exception as e:
- self.error = 'No se pudo convertir el CER en TXT'
- return data
+ # ~ _kill(data['xml'])
+ # ~ _kill(data['key'])
- args = 'openssl x509 -inform DER -in {} -noout -{}'
- try:
- result = _call(args.format(self._path_cer, 'purpose')).split('\n')[3]
- except Exception as e:
- self.error = 'No se puede saber si es FIEL'
- return data
-
- if result == 'SSL server : No':
- self.error = 'El certificado es FIEL'
- return data
-
- result = _call(args.format(self._path_cer, 'serial'))
- serie = result.split('=')[1].split('\n')[0][1::2]
- result = _call(args.format(self._path_cer, 'subject'))
- #~ Verificar si es por la version de OpenSSL
- t1 = 'x500UniqueIdentifier = '
- t2 = 'x500UniqueIdentifier='
- if t1 in result:
- rfc = result.split(t1)[1][:13].strip()
- elif t2 in result:
- rfc = result.split(t2)[1][:13].strip()
- else:
- self.error = 'No se pudo obtener el RFC del certificado'
- print ('\n', result)
- return data
-
- if not DEBUG:
- if not rfc == session_rfc:
- self.error = 'El RFC del certificado no corresponde.'
- return data
-
- dates = _call(args.format(self._path_cer, 'dates')).split('\n')
- desde = parser.parse(dates[0].split('=')[1])
- hasta = parser.parse(dates[1].split('=')[1])
- self._modulus = _call(args.format(self._path_cer, 'modulus'))
-
- data['cer'] = read_file(self._path_cer)
- data['cer_pem'] = cer_pem
- data['cer_txt'] = cer_txt.replace('\n', '')
- data['serie'] = serie
- data['rfc'] = rfc
- data['desde'] = desde.replace(tzinfo=None)
- data['hasta'] = hasta.replace(tzinfo=None)
- return data
-
- def _get_p12(self, password, rfc, token):
- tmp_cer = tempfile.mkstemp()[1]
- tmp_key = tempfile.mkstemp()[1]
- tmp_p12 = tempfile.mkstemp()[1]
-
- args = 'openssl x509 -inform DER -in "{}" -out "{}"'
- _call(args.format(self._path_cer, tmp_cer))
- args = 'openssl pkcs8 -inform DER -in "{}" -passin pass:"{}" -out "{}"'
- _call(args.format(self._path_key, password, tmp_key))
-
- args = 'openssl pkcs12 -export -in "{}" -inkey "{}" -name "{}" ' \
- '-passout pass:"{}" -out "{}"'
- _call(args.format(tmp_cer, tmp_key, rfc, token, tmp_p12))
- data = read_file(tmp_p12)
-
- self._kill(tmp_cer)
- self._kill(tmp_key)
- self._kill(tmp_p12)
-
- return data
-
- def _get_info_key(self, password, rfc, token):
- data = {}
-
- args = 'openssl pkcs8 -inform DER -in "{}" -passin pass:"{}"'
- try:
- result = _call(args.format(self._path_key, password))
- except Exception as e:
- self.error = 'Contraseña incorrecta'
- return data
-
- args = 'openssl pkcs8 -inform DER -in "{}" -passin pass:"{}" | ' \
- 'openssl rsa -noout -modulus'
- mod_key = _call(args.format(self._path_key, password))
-
- if self._modulus != mod_key:
- self.error = 'Los archivos no son pareja'
- return data
-
- args = "openssl pkcs8 -inform DER -in '{}' -passin pass:'{}' | " \
- "openssl rsa -des3 -passout pass:'{}'".format(
- self._path_key, password, token)
- key_enc = _call(args)
-
- data['key'] = read_file(self._path_key)
- data['key_enc'] = key_enc
- data['p12'] = self._get_p12(password, rfc, token)
- return data
-
- def validate(self, password, rfc, auth):
- token = _get_md5(rfc)
- if USAR_TOKEN:
- token = auth['PASS']
- if AUTH['DEBUG']:
- token = AUTH['PASS']
-
- if not self._path_key or not self._path_cer:
- 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, rfc, token)
- if not llave:
- return {}
-
- data.update(llave)
-
- self._kill(self._path_key)
- self._kill(self._path_cer)
- return data
-
-
-def make_xml(data, certificado, auth):
- from .cfdi_xml import CFDI
-
- token = _get_md5(certificado.rfc)
- if USAR_TOKEN:
- token = auth['PASS']
- if AUTH['DEBUG']:
- token = AUTH['PASS']
-
- if DEBUG:
- data['emisor']['Rfc'] = certificado.rfc
- data['emisor']['RegimenFiscal'] = '603'
-
- cfdi = CFDI()
- xml = cfdi.get_xml(data)
-
- data = {
- 'xsltproc': PATH_XSLTPROC,
- 'xslt': _join(PATH_XSLT, 'cadena.xslt'),
- 'xml': save_temp(xml, 'w'),
- 'openssl': PATH_OPENSSL,
- 'key': save_temp(certificado.key_enc, 'w'),
- 'pass': token,
- }
- args = '"{xsltproc}" "{xslt}" "{xml}" | ' \
- '"{openssl}" dgst -sha256 -sign "{key}" -passin pass:"{pass}" | ' \
- '"{openssl}" enc -base64 -A'.format(**data)
- sello = _call(args)
-
- _kill(data['xml'])
- _kill(data['key'])
-
- return cfdi.add_sello(sello)
+ # ~ return cfdi.add_sello(sello)
def timbra_xml(xml, auth):
@@ -1339,9 +1185,15 @@ class LIBO(object):
self._leyendas(data.get('leyendas', ''))
self._cancelado(data['cancelada'])
+ self._others_values(data)
self._clean()
return
+ def _others_values(self, data):
+ version = data['version']
+ self._set_cell('{version}', version)
+ return
+
def pdf(self, path, data, ods=False):
options = {'AsTemplate': True, 'Hidden': True}
log.debug('Abrir plantilla...')
@@ -1818,7 +1670,7 @@ def _get_relacionados(doc, version):
if node is None:
return ''
- uuids = ['UUID: {}'.format(n.attrib['UUID']) for n in node.getchildren()]
+ uuids = ['UUID: {}'.format(n.attrib['UUID']) for n in list(node)]
return '\n'.join(uuids)
@@ -1933,7 +1785,8 @@ def _conceptos(doc, version, options):
data = []
conceptos = doc.find('{}Conceptos'.format(PRE[version]))
- for c in conceptos.getchildren():
+ # ~ for c in conceptos.getchildren():
+ for c in list(conceptos):
values = CaseInsensitiveDict(c.attrib.copy())
if is_nomina:
values['noidentificacion'] = values['ClaveProdServ']
@@ -2002,7 +1855,8 @@ def _totales(doc, cfdi, version):
node = imp.find('{}Traslados'.format(PRE[version]))
if node is not None:
- for n in node.getchildren():
+ # ~ for n in node.getchildren():
+ for n in list(node):
tmp = CaseInsensitiveDict(n.attrib.copy())
if version == '3.3':
tasa = round(float(tmp['tasaocuota']), DECIMALES)
@@ -2013,7 +1867,8 @@ def _totales(doc, cfdi, version):
node = imp.find('{}Retenciones'.format(PRE[version]))
if node is not None:
- for n in node.getchildren():
+ # ~ for n in node.getchildren():
+ for n in list(node):
tmp = CaseInsensitiveDict(n.attrib.copy())
if version == '3.3':
title = 'Retención {} {}'.format(
@@ -2119,20 +1974,20 @@ def _nomina(doc, data, values, version_cfdi):
if not node is None:
data['comprobante'].update(CaseInsensitiveDict(node.attrib.copy()))
info['percepciones'] = []
- for p in node.getchildren():
+ for p in list(node):
info['percepciones'].append(CaseInsensitiveDict(p.attrib.copy()))
node = node_nomina.find('{}Deducciones'.format(PRE['NOMINA'][version]))
if not node is None:
data['comprobante'].update(CaseInsensitiveDict(node.attrib.copy()))
info['deducciones'] = []
- for d in node.getchildren():
+ for d in list(node):
info['deducciones'].append(CaseInsensitiveDict(d.attrib.copy()))
node = node_nomina.find('{}OtrosPagos'.format(PRE['NOMINA'][version]))
if not node is None:
info['otrospagos'] = []
- for o in node.getchildren():
+ for o in list(node):
info['otrospagos'].append(CaseInsensitiveDict(o.attrib.copy()))
n = o.find('{}SubsidioAlEmpleo'.format(PRE['NOMINA'][version]))
if not n is None:
@@ -2141,7 +1996,7 @@ def _nomina(doc, data, values, version_cfdi):
node = node_nomina.find('{}Incapacidades'.format(PRE['NOMINA'][version]))
if not node is None:
info['incapacidades'] = []
- for i in node.getchildren():
+ for i in list(node):
info['incapacidades'].append(CaseInsensitiveDict(i.attrib.copy()))
return info
@@ -2196,6 +2051,8 @@ def get_data_from_xml(invoice, values):
if data['pagos']:
data['pays'] = _cfdipays(doc, data, version)
data['pakings'] = values.get('pakings', [])
+ # ~ data['version'] = values['version']
+ data['version'] = version
return data
@@ -2692,12 +2549,12 @@ def local_copy(files):
log.error(msg)
return
- args = 'df -P {} | tail -1 | cut -d" " -f 1'.format(path_bk)
- try:
- result = _call(args)
+ # ~ args = 'df -P {} | tail -1 | cut -d" " -f 1'.format(path_bk)
+ # ~ try:
+ # ~ result = _call(args)
# ~ log.info(result)
- except:
- pass
+ # ~ except:
+ # ~ pass
# ~ if result != 'empresalibre\n':
# ~ log.info(result)
# ~ msg = 'Asegurate de que exista la carpeta para sincronizar'
@@ -2742,20 +2599,20 @@ def sync_files(files, auth={}):
return
-def sync_cfdi(auth, files):
+def sync_cfdi(files):
local_copy(files)
if DEBUG:
return
- if not auth['REPO'] or not SEAFILE_SERVER:
- return
+ # ~ if not auth['REPO'] or not SEAFILE_SERVER:
+ # ~ return
- seafile = SeaFileAPI(SEAFILE_SERVER['URL'], auth['USER'], auth['PASS'])
- if seafile.is_connect:
- for f in files:
- seafile.update_file(
- f, auth['REPO'], 'Facturas/{}/'.format(f[2]), auth['PASS'])
+ # ~ seafile = SeaFileAPI(SEAFILE_SERVER['URL'], auth['USER'], auth['PASS'])
+ # ~ if seafile.is_connect:
+ # ~ for f in files:
+ # ~ seafile.update_file(
+ # ~ f, auth['REPO'], 'Facturas/{}/'.format(f[2]), auth['PASS'])
return
@@ -3158,7 +3015,7 @@ class ImportCFDI(object):
def _conceptos(self):
data = []
conceptos = self._doc.find('{}Conceptos'.format(self._pre))
- for c in conceptos.getchildren():
+ for c in list(conceptos):
values = CaseInsensitiveDict(c.attrib.copy())
data.append(values)
return data
diff --git a/source/app/controllers/utils.py b/source/app/controllers/utils.py
index cceda98..232ac37 100644
--- a/source/app/controllers/utils.py
+++ b/source/app/controllers/utils.py
@@ -48,11 +48,13 @@ from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from dateutil import parser
-import seafileapi
+from .cfdi_xml import CFDI
-from settings import DEBUG, DB_COMPANIES, PATHS
-from .comercio import PACComercioDigital
-# ~ from .finkok import PACFinkok
+from settings import DEBUG, DB_COMPANIES, PATHS, TEMPLATE_CANCEL, RFCS
+
+from .pacs.cfdi_cert import SATCertificate
+from .pacs import PACComercioDigital
+from .pacs import PACFinkok
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
@@ -74,9 +76,14 @@ if DEBUG:
PSQL = 'psql -h localhost -U postgres'
PACS = {
- # ~ '': PACFinkok,
+ 'finkok': PACFinkok,
'comercio': PACComercioDigital,
}
+NS_CFDI = {
+ 'cfdi': 'http://www.sat.gob.mx/cfd/3',
+ 'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
+}
+
#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
class CaseInsensitiveDict(collections.MutableMapping):
@@ -491,29 +498,6 @@ def _backup_db(rfc, is_mv, url_seafile):
shutil.copy(path, 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
@@ -583,26 +567,24 @@ def get_pass():
return True, password
-def xml_stamp(xml, auth, name):
+def xml_stamp(xml, auth):
if not DEBUG and not auth:
msg = 'Sin datos para timbrar'
result = {'ok': False, 'error': msg}
return result
result = {'ok': True, 'error': ''}
- auth = {'user': auth['USER'], 'pass': auth['PASS']}
- pac = PACS[name]()
- xml_stamped = pac.stamp(xml, auth)
+ pac = PACS[auth['pac']]()
+ response = pac.stamp(xml, auth)
- if not xml_stamped:
+ if not response:
result['ok'] = False
result['error'] = pac.error
return result
- result['xml'] = xml_stamped
- result['uuid'] = pac.cfdi_uuid
- result['fecha'] = pac.date_stamped
+ result.update(response)
+
return result
@@ -630,14 +612,89 @@ def xml_cancel(xml, auth, cert, name):
return data, result
-def get_client_balance(auth, name):
+def get_client_balance(auth):
if DEBUG:
return '-d'
- pac = PACS[name]()
- auth = {'usr': auth['USER'], 'pwd': auth['PASS']}
+ pac = PACS[auth['pac']]()
balance = pac.client_balance(auth)
if pac.error:
- balance = '-e'
+ balance = 'p/e'
return balance
+
+
+def get_cert(args):
+ cer = base64.b64decode(args['cer'].split(',')[1])
+ key = base64.b64decode(args['key'].split(',')[1])
+ cert = SATCertificate(cer, key, args['contra'])
+ return cert
+
+
+def make_xml(data, certificado):
+ cert = SATCertificate(certificado.cer, certificado.key_enc.encode())
+ if DEBUG:
+ data['emisor']['Rfc'] = certificado.rfc
+ data['emisor']['RegimenFiscal'] = '603'
+
+ cfdi = CFDI()
+ xml = ET.parse(BytesIO(cfdi.get_xml(data).encode()))
+
+ path_xslt = _join(PATHS['xslt'], 'cadena.xslt')
+ xslt = open(path_xslt, 'rb')
+ transfor = ET.XSLT(ET.parse(xslt))
+ cadena = str(transfor(xml)).encode()
+ stamp = cert.sign(cadena)
+ xslt.close()
+
+ return cfdi.add_sello(stamp, cert.cer_txt)
+
+
+def get_pac_by_rfc(cfdi):
+ tree = ET.fromstring(cfdi.encode())
+ path = 'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@RfcProvCertif)'
+ rfc_pac = tree.xpath(path, namespaces=NS_CFDI)
+ return RFCS[rfc_pac]
+
+
+def _cancel_finkok(invoice, auth, certificado):
+ cert = SATCertificate(certificado.cer, certificado.key_enc.encode())
+ pac = PACS[auth['pac']]()
+ info = {'cer': cert.cer_pem, 'key': cert.key_pem}
+
+ result = pac.cancel(invoice.xml, info, auth)
+ if pac.error:
+ data = {'ok': False, 'msg': pac.error, 'row': {}}
+ return data
+
+ msg = 'Factura cancelada correctamente'
+ data = {'ok': True, 'msg': msg, 'row': {'estatus': 'Cancelada'},
+ 'date': result['date'], 'acuse': result['acuse']}
+ return data
+
+
+def cancel_xml_sign(invoice, auth, certificado):
+ if auth['pac'] == 'finkok':
+ return _cancel_finkok(invoice, auth, certificado)
+
+ cert = SATCertificate(certificado.cer, certificado.key_enc.encode())
+ pac = PACS[auth['pac']]()
+ data = {
+ 'rfc': certificado.rfc,
+ 'fecha': now().isoformat()[:19],
+ 'uuid': str(invoice.uuid).upper(),
+ }
+ template = TEMPLATE_CANCEL.format(**data)
+ tree = ET.fromstring(template.encode())
+ tree = cert.sign_xml(tree)
+ sign_xml = ET.tostring(tree).decode()
+
+ result = pac.cancel_xml(sign_xml, auth, invoice.xml)
+ if pac.error:
+ data = {'ok': False, 'msg': pac.error, 'row': {}}
+ return data
+
+ msg = 'Factura cancelada correctamente'
+ data = {'ok': True, 'msg': msg, 'row': {'estatus': 'Cancelada'},
+ 'date': result['date'], 'acuse': result['acuse']}
+ return data
diff --git a/source/app/main.py b/source/app/main.py
index c03b858..cb28630 100644
--- a/source/app/main.py
+++ b/source/app/main.py
@@ -17,7 +17,7 @@ from controllers.main import (AppEmpresas,
AppDocumentos, AppFiles, AppPreInvoices, AppCuentasBanco,
AppMovimientosBanco, AppTickets, AppStudents, AppEmployees, AppNomina,
AppInvoicePay, AppCfdiPay, AppSATBancos, AppSociosCuentasBanco,
- AppSATFormaPago, AppSATLeyendaFiscales
+ AppSATFormaPago, AppSATLeyendaFiscales, AppCert
)
@@ -62,6 +62,7 @@ api.add_route('/satbancos', AppSATBancos(db))
api.add_route('/satformapago', AppSATFormaPago(db))
api.add_route('/socioscb', AppSociosCuentasBanco(db))
api.add_route('/leyendasfiscales', AppSATLeyendaFiscales(db))
+api.add_route('/cert', AppCert(db))
session_options = {
diff --git a/source/app/models/db.py b/source/app/models/db.py
index 7d50926..2219a2d 100644
--- a/source/app/models/db.py
+++ b/source/app/models/db.py
@@ -471,6 +471,13 @@ class StorageEngine(object):
def sat_leyendas_fiscales_delete(self, values):
return main.SATLeyendasFiscales.remove(values)
+ # ~ v2
+ def cert_get(self, values):
+ return main.Certificado.get_data(values)
+
+ def cert_post(self, values):
+ return main.Certificado.post(values)
+
# Companies only in MV
def _get_empresas(self, values):
return main.companies_get()
diff --git a/source/app/models/main.py b/source/app/models/main.py
index 464633b..1679bea 100644
--- a/source/app/models/main.py
+++ b/source/app/models/main.py
@@ -49,6 +49,7 @@ from settings import (
PATHS,
URL,
VALUES_PDF,
+ VERSION,
RFCS,
)
@@ -138,21 +139,26 @@ def validar_timbrar():
msg = 'Es necesario configurar un certificado de sellos'
try:
- obj = Certificado.select()[0]
- except IndexError:
+ obj = Certificado.get(Certificado.es_fiel==False)
+ except Exception as e:
return {'ok': False, 'msg': msg}
if not obj.serie:
return {'ok': False, 'msg': msg}
- dias = obj.hasta - util.now()
- if dias.days < 0:
+ diff = obj.hasta - utils.now()
+ if diff.days < 0:
msg = 'El certificado ha vencido, es necesario cargar uno nuevo'
return {'ok': False, 'msg': msg}
+ auth = Configuracion.get_({'fields': 'pac_auth'})
+ if not auth:
+ msg = 'Es necesario configurar los datos de timbrado del PAC'
+ return {'ok': False, 'msg': msg}
+
msg = ''
- if dias.days < 15:
- msg = 'El certificado vence en: {} días.'.format(dias.days)
+ if diff.days < 15:
+ msg = 'El certificado vence en: {} días.'.format(diff.days)
return {'ok': True, 'msg': msg}
@@ -218,6 +224,8 @@ def import_invoice():
def get_doc(type_doc, id, rfc):
types = {
'xml': 'application/xml',
+ 'xmlpago': 'application/xml',
+ 'nomxml': 'application/xml',
'ods': 'application/octet-stream',
'zip': 'application/octet-stream',
'nomlog': 'application/txt',
@@ -380,11 +388,11 @@ class Configuracion(BaseModel):
.select(Configuracion.valor)
.where(Configuracion.clave == key)
)
- if data:
- return util.get_bool(data[0].valor)
+ if data and data[0].valor == '1':
+ return True
return False
- def _get_partners(self):
+ def _get_partners(self, args={}):
fields = (
'chk_config_change_balance_partner',
)
@@ -396,7 +404,7 @@ class Configuracion(BaseModel):
return values
- def _get_admin_products(self):
+ def _get_admin_products(self, args={}):
fields = (
'chk_config_cuenta_predial',
'chk_config_codigo_barras',
@@ -411,7 +419,7 @@ class Configuracion(BaseModel):
values = {r.clave: util.get_bool(r.valor) for r in data}
return values
- def _get_main_products(self):
+ def _get_main_products(self, args={}):
fields = (
'chk_config_cuenta_predial',
'chk_config_codigo_barras',
@@ -428,7 +436,7 @@ class Configuracion(BaseModel):
values['default_unidad'] = SATUnidades.get_default()
return values
- def _get_complements(self):
+ def _get_complements(self, args={}):
fields = (
'chk_config_ine',
'chk_config_edu',
@@ -456,7 +464,7 @@ class Configuracion(BaseModel):
return values
- def _get_folios(self):
+ def _get_folios(self, args={}):
fields = (
'chk_folio_custom',
)
@@ -467,7 +475,7 @@ class Configuracion(BaseModel):
values = {r.clave: util.get_bool(r.valor) for r in data}
return values
- def _get_correo(self):
+ def _get_correo(self, args={}):
fields = ('correo_servidor', 'correo_puerto', 'correo_ssl',
'correo_usuario', 'correo_copia', 'correo_asunto',
'correo_mensaje', 'correo_directo', 'correo_confirmacion')
@@ -478,7 +486,7 @@ class Configuracion(BaseModel):
values = {r.clave: r.valor for r in data}
return values
- def _get_admin_config_users(self):
+ def _get_admin_config_users(self, args={}):
fields = (
'chk_users_notify_access',
)
@@ -500,6 +508,43 @@ class Configuracion(BaseModel):
value = data[0].valor
return value
+ def _get_pac(cls, pac):
+ user_field = f'user_timbrado_{pac}'
+ token_field = f'token_timbrado_{pac}'
+ fields = (user_field, token_field)
+ data = (Configuracion
+ .select()
+ .where(Configuracion.clave.in_(fields))
+ )
+ data = {r.clave: r.valor for r in data}
+ values = {
+ 'user_timbrado': data.get(user_field, ''),
+ 'token_timbrado': data.get(token_field, ''),
+ }
+ return values
+
+ def _get_pac_auth(cls, args={}):
+ pac = cls.get_('lst_pac').lower()
+ user = cls.get_(f'user_timbrado_{pac}')
+ token = cls.get_(f'token_timbrado_{pac}')
+ data = {}
+ if pac and user and token:
+ data['pac'] = pac
+ data['user'] = user
+ data['pass'] = token
+ return data
+
+ def _get_auth_by_pac(cls, args):
+ pac = args['pac']
+ user = cls.get_(f'user_timbrado_{pac}')
+ token = cls.get_(f'token_timbrado_{pac}')
+ data = {}
+ if pac and user and token:
+ data['pac'] = pac
+ data['user'] = user
+ data['pass'] = token
+ return data
+
@classmethod
def get_(cls, keys):
if isinstance(keys, str):
@@ -518,10 +563,15 @@ class Configuracion(BaseModel):
'folios',
'correo',
'admin_config_users',
+ 'pac_auth',
+ 'auth_by_pac',
)
opt = keys['fields']
if opt in options:
- return getattr(cls, '_get_{}'.format(opt))(cls)
+ return getattr(cls, f'_get_{opt}')(cls, keys)
+
+ if opt == 'pac':
+ return cls._get_pac(cls, keys['pac'])
if keys['fields'] == 'configtemplates':
try:
@@ -571,7 +621,7 @@ class Configuracion(BaseModel):
)
values = {r.clave: util.get_bool(r.valor) for r in data}
fields = (
- ('lst_pac', 'default'),
+ ('lst_pac', 'comercio'),
)
for k, d in fields:
values[k] = Configuracion.get_value(k, d)
@@ -616,6 +666,24 @@ class Configuracion(BaseModel):
values = {r.clave: r.valor for r in data}
return values
+ def _save_pac(cls, values):
+ pac = values['lst_pac']
+ user = values['user_timbrado']
+ token = values['token_timbrado']
+
+ data = {
+ 'lst_pac': pac,
+ f'user_timbrado_{pac}': user,
+ f'token_timbrado_{pac}': token,
+ }
+
+ for k, v in data.items():
+ obj, _ = Configuracion.get_or_create(clave=k)
+ obj.valor = v
+ obj.save()
+
+ return {'ok': True}
+
@classmethod
def add(cls, values):
opt = values.pop('opt', '')
@@ -1003,8 +1071,8 @@ class Emisor(BaseModel):
'ong_autorizacion': obj.autorizacion,
'ong_fecha': obj.fecha_autorizacion,
'ong_fecha_dof': obj.fecha_dof,
- 'correo_timbrado': obj.correo_timbrado,
- 'token_timbrado': obj.token_timbrado,
+ # ~ 'correo_timbrado': obj.correo_timbrado,
+ # ~ 'token_timbrado': obj.token_timbrado,
'token_soporte': obj.token_soporte,
'emisor_registro_patronal': obj.registro_patronal,
'regimenes': [row.id for row in obj.regimenes]
@@ -1030,15 +1098,13 @@ class Emisor(BaseModel):
@classmethod
def get_timbres(cls):
- auth = cls.get_auth()
- if not auth:
- return 'c/e'
+ try:
+ obj = Emisor.select()[0]
+ except IndexError:
+ return 's/e'
- pac = Configuracion.get_('lst_pac').lower()
- if pac:
- result = utils.get_client_balance(auth, pac)
- else:
- result = util.get_timbres(auth)
+ auth = Configuracion.get_({'fields': 'pac_auth'})
+ result = utils.get_client_balance(auth)
return result
@classmethod
@@ -1116,62 +1182,57 @@ class Certificado(BaseModel):
return self.serie
@classmethod
- def get_cert(cls, is_fiel=False):
- return Certificado.get(Certificado.es_fiel==is_fiel)
-
- @classmethod
- def get_data(cls):
- obj = cls.get_(cls)
- row = {
+ def _get_cert(cls, args):
+ obj = Certificado.get(Certificado.es_fiel==False)
+ data = {
'cert_rfc': obj.rfc,
'cert_serie': obj.serie,
'cert_desde': obj.desde,
'cert_hasta': obj.hasta,
}
- return row
-
- def get_(cls):
- return Certificado.select()[0]
+ return data
@classmethod
- 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'}
+ def get_data(cls, values):
+ opt = values['opt']
+ return getattr(cls, f'_get_{opt}')(values)
@classmethod
- def validate(cls, values, session):
- row = {}
- result = False
+ def _validate_cert(cls, args):
+ msg = 'Certificado guardado correctamente'
+ result = {'ok': True, 'msg': msg, 'data': {}}
+ cert = utils.get_cert(args)
+ if not cert.is_valid:
+ result['ok'] = False
+ result['msg'] = cert.error
+ return result
- obj = cls.get_(cls)
- paths = Configuracion.get_({'fields': 'path_cer'})
- cert = util.Certificado(paths)
- auth = Emisor.get_auth()
- data = cert.validate(values['contra'], session['rfc'], auth)
- if data:
- msg = 'Certificado guardado correctamente'
- q = Certificado.update(**data).where(Certificado.id==obj.id)
- if q.execute():
- result = True
- row = {
- 'cert_rfc': data['rfc'],
- 'cert_serie': data['serie'],
- 'cert_desde': data['desde'],
- 'cert_hasta': data['hasta'],
- }
- else:
- msg = cert.error
+ obj = Certificado.get(Certificado.es_fiel==False)
+ if obj.rfc != cert.rfc:
+ result['ok'] = False
+ result['msg'] = 'El RFC del certificado no corresponde.'
+ return result
- Configuracion.add({'path_key': ''})
- Configuracion.add({'path_cer': ''})
+ obj.key_enc = cert.key_enc
+ obj.cer = cert.cer
+ obj.serie = cert.serial_number
+ obj.desde = cert.not_before
+ obj.hasta = cert.not_after
+ obj.save()
- return {'ok': result, 'msg': msg, 'data': row}
+ data = {
+ 'cert_rfc': obj.rfc,
+ 'cert_serie': obj.serie,
+ 'cert_desde': obj.desde,
+ 'cert_hasta': obj.hasta,
+ }
+ result['data'] = data
+ return result
+ @classmethod
+ def post(cls, values):
+ opt = values['opt']
+ return getattr(cls, f'_{opt}')(values)
class Folios(BaseModel):
@@ -3798,11 +3859,38 @@ class Facturas(BaseModel):
obj.fecha_cancelacion = util.now()
obj.save()
msg = 'Factura cancelada correctamente'
- return {'ok': True, 'msg': msg, 'row': {'estatus': 'Cancelada'}}
+ return {'ok': True, 'msg': msg, 'row': {'estatus': obj.estatus}}
+
+ return cls._cancel_xml_sign(obj)
+
+ @classmethod
+ def _cancel_xml_sign(cls, invoice):
+ if invoice.version != '3.3':
+ msg = 'Solo es posible cancelar CFDI 3.3'
+ return {'ok': False, 'msg': msg}
+
+ pac = utils.get_pac_by_rfc(invoice.xml)
+ auth = Configuracion.get_({'fields': 'auth_by_pac', 'pac': pac})
+
+ certificado = Certificado.get(Certificado.es_fiel==False)
+ result = utils.cancel_xml_sign(invoice, auth, certificado)
+
+ if result['ok']:
+ invoice.estatus = 'Cancelada'
+ invoice.error = ''
+ invoice.cancelada = True
+ invoice.fecha_cancelacion = result['date']
+ invoice.acuse = result['acuse'] or ''
+ cls._actualizar_saldo_cliente(cls, invoice, True)
+ cls._update_inventory(cls, invoice, True)
+ cls._uncancel_tickets(cls, invoice)
+ else:
+ invoice.error = result['msg']
+ invoice.save()
+
+ data = {'ok': result['ok'], 'msg': result['msg'], 'row': result['row']}
+ return data
- if CANCEL_SIGNATURE:
- return cls._cancel_signature(cls, id)
- return cls._cancel_xml(cls, id)
def _cancel_xml(self, id):
msg = 'Factura cancelada correctamente'
@@ -3843,24 +3931,24 @@ class Facturas(BaseModel):
query.execute()
return
- def _cancel_signature(self, id):
- msg = 'Factura cancelada correctamente'
- auth = Emisor.get_auth()
- certificado = Certificado.select()[0]
- obj = Facturas.get(Facturas.id==id)
- data, result = util.cancel_signature(
- 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']
- self._actualizar_saldo_cliente(self, obj, True)
- else:
- obj.error = data['msg']
- obj.save()
- return data
+ # ~ def _cancel_signature(self, id):
+ # ~ msg = 'Factura cancelada correctamente'
+ # ~ auth = Emisor.get_auth()
+ # ~ certificado = Certificado.select()[0]
+ # ~ obj = Facturas.get(Facturas.id==id)
+ # ~ data, result = util.cancel_signature(
+ # ~ 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']
+ # ~ self._actualizar_saldo_cliente(self, obj, True)
+ # ~ else:
+ # ~ obj.error = data['msg']
+ # ~ obj.save()
+ # ~ return data
def _get_filters(self, values):
if 'start' in values and 'end' in values:
@@ -3965,7 +4053,7 @@ class Facturas(BaseModel):
def _get_not_in_xml(self, invoice, emisor):
pdf_from = Configuracion.get_('make_pdf_from') or '1'
- values = {}
+ values = {'version': VERSION}
values['notas'] = invoice.notas
values['fechadof'] = str(emisor.fecha_dof)
@@ -4213,28 +4301,27 @@ class Facturas(BaseModel):
return Facturas.send(id, rfc)
@util.run_in_thread
- def _sync(self, id, auth):
- return Facturas.sync(id, auth)
+ def _sync(self, id, rfc):
+ return Facturas.sync(id, rfc)
@util.run_in_thread
def _sync_pdf(self, pdf, name_pdf, target):
- auth = Emisor.get_auth()
+ # ~ auth = Emisor.get_auth()
files = (
(pdf, name_pdf, target),
)
- util.sync_cfdi(auth, files)
+ util.sync_cfdi(files)
return
@util.run_in_thread
def _sync_xml(self, obj):
emisor = Emisor.select()[0]
- auth = Emisor.get_auth()
name_xml = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc)
target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
files = (
(obj.xml, name_xml, target),
)
- util.sync_cfdi(auth, files)
+ util.sync_cfdi(files)
return
@util.run_in_thread
@@ -4313,21 +4400,21 @@ class Facturas(BaseModel):
return {'ok': True, 'msg': msg}
@classmethod
- def sync(cls, id, auth):
+ def sync(cls, id, rfc):
obj = Facturas.get(Facturas.id==id)
if obj.uuid is None:
msg = 'La factura no esta timbrada'
return
- emisor = Emisor.select()[0]
- pdf, name_pdf = cls.get_pdf(id, auth['RFC'], False)
+ # ~ emisor = Emisor.select()[0]
+ pdf, name_pdf = cls.get_pdf(id, rfc, False)
name_xml = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc)
- target = emisor.rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
+ target = rfc + '/' + str(obj.fecha)[:7].replace('-', '/')
files = (
(obj.xml, name_xml, target),
(pdf, name_pdf, target),
)
- util.sync_cfdi(auth, files)
+ util.sync_cfdi(files)
return
def _get_filter_folios(self, values):
@@ -4804,7 +4891,7 @@ class Facturas(BaseModel):
FacturasComplementos.create(**data)
return
- def _make_xml(self, invoice, auth):
+ def _make_xml(self, invoice):
tax_decimals = Configuracion.get_bool('chk_config_tax_decimals')
decimales_precios = Configuracion.get_bool('chk_config_decimales_precios')
invoice_by_ticket = Configuracion.get_bool('chk_config_invoice_by_ticket')
@@ -4815,7 +4902,7 @@ class Facturas(BaseModel):
frm_vu = FORMAT_PRECIO
tmp = 0
emisor = Emisor.select()[0]
- certificado = Certificado.select()[0]
+ certificado = Certificado.get(Certificado.es_fiel==False)
is_edu = False
comprobante = {}
@@ -4845,7 +4932,7 @@ class Facturas(BaseModel):
comprobante['Fecha'] = invoice.fecha.isoformat()[:19]
comprobante['FormaPago'] = invoice.forma_pago
comprobante['NoCertificado'] = certificado.serie
- comprobante['Certificado'] = certificado.cer_txt
+ # ~ comprobante['Certificado'] = certificado.cer_txt
comprobante['SubTotal'] = FORMAT.format(invoice.subtotal)
comprobante['Moneda'] = invoice.moneda
comprobante['TipoCambio'] = '1'
@@ -5081,7 +5168,8 @@ class Facturas(BaseModel):
'complementos': complementos,
}
- return util.make_xml(data, certificado, auth)
+ # ~ return util.make_xml(data, certificado)
+ return utils.make_xml(data, certificado)
@classmethod
def get_status_sat(cls, id):
@@ -5158,38 +5246,35 @@ class Facturas(BaseModel):
id = int(values['id'])
update = util.loads(values.get('update', 'true'))
- auth = Emisor.get_auth()
+ rfc = Emisor.select()[0].rfc
obj = Facturas.get(Facturas.id == id)
- obj.xml = cls._make_xml(cls, obj, auth)
+ obj.xml = cls._make_xml(cls, obj)
obj.estatus = 'Generada'
obj.save()
enviar_correo = util.get_bool(Configuracion.get_('correo_directo'))
- pac = Configuracion.get_('lst_pac').lower()
+ auth = Configuracion.get_({'fields': 'pac_auth'})
anticipo = False
msg = 'Factura timbrada correctamente'
- if pac:
- result = utils.xml_stamp(obj.xml, auth, pac)
- else:
- result = util.timbra_xml(obj.xml, auth)
+ result = utils.xml_stamp(obj.xml, auth)
if result['ok']:
obj.xml = result['xml']
obj.uuid = result['uuid']
- obj.fecha_timbrado = result['fecha']
+ obj.fecha_timbrado = result['date']
obj.estatus = 'Timbrada'
obj.error = ''
obj.save()
row = {'uuid': obj.uuid, 'estatus': 'Timbrada'}
if enviar_correo:
- cls._send(cls, id, auth['RFC'])
+ cls._send(cls, id, rfc)
if obj.tipo_comprobante == 'I' and obj.tipo_relacion == '07':
anticipo = True
cls._actualizar_saldo_cliente(cls, obj)
if update:
cls._update_inventory(cls, obj)
- cls._sync(cls, id, auth)
+ cls._sync(cls, id, rfc)
m = 'T {}'.format(obj.id)
_save_log(user.usuario, m, 'F')
@@ -5736,7 +5821,7 @@ class PreFacturas(BaseModel):
files = (
(doc, name, target),
)
- util.sync_cfdi({'REPO': False}, files)
+ util.sync_cfdi(files)
return
@classmethod
@@ -6455,21 +6540,19 @@ class CfdiPagos(BaseModel):
data = {'ok': False, 'msg': msg}
return data
- auth = Emisor.get_auth()
- cert = Certificado.get_cert()
-
- data, result = util.cancel_xml(auth, last.uuid, cert)
- if data['ok']:
+ auth = Configuracion.get_({'fields': 'pac_auth'})
+ certificado = Certificado.get(Certificado.es_fiel==False)
+ result = utils.cancel_xml_sign(last, auth, certificado)
+ if result['ok']:
last.estatus = 'Cancelada'
last.error = ''
last.cancelada = True
last.fecha_cancelacion = result['Fecha']
- msg = 'Factura cancelada correctamente'
else:
- last.error = msg = data['msg']
+ last.error = result['msg']
last.save()
- return {'ok': data['ok'], 'msg': msg, 'id': last.id}
+ return {'ok': result['ok'], 'msg': result['msg'], 'id': last.id}
def _get_folio(self, serie):
folio = int(Configuracion.get_('txt_config_cfdipay_folio') or '0')
@@ -6597,9 +6680,9 @@ class CfdiPagos(BaseModel):
return related
- def _generate_xml(self, invoice, auth):
+ def _generate_xml(self, invoice):
emisor = Emisor.select()[0]
- cert = Certificado.get_cert()
+ certificado = Certificado.get(Certificado.es_fiel==False)
used_data_bank = Configuracion.get_bool('chk_cfg_pays_data_bank')
cfdi = {}
@@ -6607,8 +6690,8 @@ class CfdiPagos(BaseModel):
cfdi['Serie'] = invoice.serie
cfdi['Folio'] = str(invoice.folio)
cfdi['Fecha'] = invoice.fecha.isoformat()[:19]
- cfdi['NoCertificado'] = cert.serie
- cfdi['Certificado'] = cert.cer_txt
+ cfdi['NoCertificado'] = certificado.serie
+ # ~ cfdi['Certificado'] = cert.cer_txt
cfdi['SubTotal'] = '0'
cfdi['Moneda'] = DEFAULT_CFDIPAY['CURRENCY']
cfdi['Total'] = '0'
@@ -6688,27 +6771,27 @@ class CfdiPagos(BaseModel):
'edu': False,
'complementos': complementos,
}
- return util.make_xml(data, cert, auth)
+ return utils.make_xml(data, certificado)
def _stamp(self, values):
id_mov = int(values['id_mov'])
+ send_email = Configuracion.get_bool('correo_directo')
+ auth = Configuracion.get_({'fields': 'pac_auth'})
- send_email = util.get_bool(Configuracion.get_('correo_directo'))
- auth = Emisor.get_auth()
filters = (
(CfdiPagos.movimiento==id_mov) &
(CfdiPagos.uuid.is_null(True))
)
obj = CfdiPagos.get(filters)
- obj.xml = self._generate_xml(self, obj, auth)
+ obj.xml = self._generate_xml(self, obj)
obj.estatus = 'Generada'
obj.save()
msg = 'Factura timbrada correctamente'
- result = util.timbra_xml(obj.xml, auth)
+ result = utils.xml_stamp(obj.xml, auth)
if result['ok']:
obj.xml = result['xml']
obj.uuid = result['uuid']
- obj.fecha_timbrado = result['fecha']
+ obj.fecha_timbrado = result['date']
obj.estatus = 'Timbrada'
obj.error = ''
row = {'uuid': obj.uuid, 'estatus': 'Timbrada'}
@@ -6761,7 +6844,6 @@ class CfdiPagos(BaseModel):
(obj.xml, name, target),
)
cls._sync_files(cls, files)
-
return obj.xml, name
def _get_not_in_xml(self, invoice, emisor):
@@ -7959,29 +8041,26 @@ class CfdiNomina(BaseModel):
def _cancel(self, values, user):
id = int(values['id'])
- msg = 'Recibo cancelado correctamente'
- auth = Emisor.get_auth()
- certificado = Certificado.select()[0]
obj = CfdiNomina.get(CfdiNomina.id==id)
-
if obj.uuid is None:
msg = 'Solo se pueden cancelar recibos timbrados'
return {'ok': False, 'msg': msg}
- data, result = util.cancel_xml(auth, obj.uuid, certificado)
+ auth = Configuracion.get_({'fields': 'pac_auth'})
+ certificado = Certificado.get(Certificado.es_fiel==False)
+ result = utils.cancel_xml_sign(obj, auth, certificado)
- if data['ok']:
- data['msg'] = 'Recibo cancelado correctamente'
- data['row']['estatus'] = 'Cancelado'
- obj.estatus = data['row']['estatus']
+ if result['ok']:
+ obj.estatus = 'Cancelado'
obj.error = ''
obj.cancelada = True
obj.fecha_cancelacion = result['Fecha']
obj.acuse = result['Acuse'] or ''
else:
- obj.error = data['msg']
+ obj.error = result['msg']
obj.save()
- return data
+
+ return result
def _send_mail(self, values, user):
id = int(values['id'])
@@ -8514,10 +8593,10 @@ class CfdiNomina(BaseModel):
return
- def _make_xml(self, cfdi, auth):
+ def _make_xml(self, cfdi):
emisor = Emisor.select()[0]
empleado = cfdi.empleado
- certificado = Certificado.select()[0]
+ certificado = Certificado.get(Certificado.es_fiel==False)
totals = CfdiNominaTotales.select().where(CfdiNominaTotales.cfdi==cfdi)[0]
comprobante = {}
@@ -8620,8 +8699,8 @@ class CfdiNomina(BaseModel):
ant = 'P{}D'.format(days)
nomina_receptor['Antigüedad'] = ant
- if empleado.puesto:
- if empleado.puesto.departamento:
+ if empleado.puesto.nombre:
+ if empleado.puesto.departamento.nombre:
nomina_receptor['Departamento'] = empleado.puesto.departamento.nombre
nomina_receptor['Puesto'] = empleado.puesto.nombre
@@ -8761,21 +8840,24 @@ class CfdiNomina(BaseModel):
'impuestos': {},
'donativo': {},
}
- return util.make_xml(data, certificado, auth)
+ # ~ return util.make_xml(data, certificado, auth)
+ return utils.make_xml(data, certificado)
def _stamp_id(self, id):
- auth = Emisor.get_auth()
+ # ~ auth = Emisor.get_auth()
+ auth = Configuracion.get_({'fields': 'pac_auth'})
obj = CfdiNomina.get(CfdiNomina.id==id)
- obj.xml = self._make_xml(self, obj, auth)
+ obj.xml = self._make_xml(self, obj)
obj.estatus = 'Generado'
obj.save()
- result = util.timbra_xml(obj.xml, auth)
+ # ~ result = util.timbra_xml(obj.xml, auth)
+ result = utils.xml_stamp(obj.xml, auth)
# ~ print (result)
if result['ok']:
obj.xml = result['xml']
obj.uuid = result['uuid']
- obj.fecha_timbrado = result['fecha']
+ obj.fecha_timbrado = result['date']
obj.estatus = 'Timbrado'
obj.error = ''
obj.save()
@@ -8786,7 +8868,6 @@ class CfdiNomina(BaseModel):
obj.error = msg
obj.save()
-
return result['ok'], obj.error
def _stamp(self):
@@ -9736,388 +9817,387 @@ def _importar_valores(archivo='', rfc=''):
return
-def _importar_socios(rows):
- log.info('\tImportando Clientes...')
- totals = len(rows)
- for i, row in enumerate(rows):
- msg = '\tGuardando cliente {} de {}'.format(i+1, totals)
- log.info(msg)
- try:
- with database_proxy.atomic() as txn:
- Socios.create(**row)
- except IntegrityError:
- msg = '\tSocio existente: {}'.format(row['nombre'])
- log.info(msg)
- log.info('\tClientes importados...')
- return
+# ~ def _importar_socios(rows):
+ # ~ log.info('\tImportando Clientes...')
+ # ~ totals = len(rows)
+ # ~ for i, row in enumerate(rows):
+ # ~ msg = '\tGuardando cliente {} de {}'.format(i+1, totals)
+ # ~ log.info(msg)
+ # ~ try:
+ # ~ with database_proxy.atomic() as txn:
+ # ~ Socios.create(**row)
+ # ~ except IntegrityError:
+ # ~ msg = '\tSocio existente: {}'.format(row['nombre'])
+ # ~ log.info(msg)
+ # ~ log.info('\tClientes importados...')
+ # ~ return
-def _existe_factura(row):
- filtro = (Facturas.uuid==row['uuid'])
- if row['uuid'] is None:
- filtro = (
- (Facturas.serie==row['serie']) &
- (Facturas.folio==row['folio'])
- )
- return Facturas.select().where(filtro).exists()
+# ~ def _existe_factura(row):
+ # ~ filtro = (Facturas.uuid==row['uuid'])
+ # ~ if row['uuid'] is None:
+ # ~ filtro = (
+ # ~ (Facturas.serie==row['serie']) &
+ # ~ (Facturas.folio==row['folio'])
+ # ~ )
+ # ~ return Facturas.select().where(filtro).exists()
-def _importar_facturas(rows):
- log.info('\tImportando Facturas...')
- totals = len(rows)
- for i, row in enumerate(rows):
- msg = '\tGuardando factura {} de {}'.format(i+1, totals)
- log.info(msg)
+# ~ def _importar_facturas(rows):
+ # ~ log.info('\tImportando Facturas...')
+ # ~ totals = len(rows)
+ # ~ for i, row in enumerate(rows):
+ # ~ msg = '\tGuardando factura {} de {}'.format(i+1, totals)
+ # ~ log.info(msg)
- try:
- detalles = row.pop('detalles')
- impuestos = row.pop('impuestos')
- cliente = row.pop('cliente')
- row['cliente'] = Socios.get(**cliente)
- with database_proxy.atomic() as txn:
- if _existe_factura(row):
- msg = '\tFactura existente: {}{}'.format(
- row['serie'], row['folio'])
- log.info(msg)
- continue
- obj = Facturas.create(**row)
- for detalle in detalles:
- detalle['factura'] = obj
- FacturasDetalle.create(**detalle)
- for impuesto in impuestos:
- imp = SATImpuestos.get(**impuesto['filtro'])
- new = {
- 'factura': obj,
- 'impuesto': imp,
- 'importe': impuesto['importe'],
- }
- try:
- with database_proxy.atomic() as txn:
- FacturasImpuestos.create(**new)
- except IntegrityError as e:
- pass
+ # ~ try:
+ # ~ detalles = row.pop('detalles')
+ # ~ impuestos = row.pop('impuestos')
+ # ~ cliente = row.pop('cliente')
+ # ~ row['cliente'] = Socios.get(**cliente)
+ # ~ with database_proxy.atomic() as txn:
+ # ~ if _existe_factura(row):
+ # ~ msg = '\tFactura existente: {}{}'.format(
+ # ~ row['serie'], row['folio'])
+ # ~ log.info(msg)
+ # ~ continue
+ # ~ obj = Facturas.create(**row)
+ # ~ for detalle in detalles:
+ # ~ detalle['factura'] = obj
+ # ~ FacturasDetalle.create(**detalle)
+ # ~ for impuesto in impuestos:
+ # ~ imp = SATImpuestos.get(**impuesto['filtro'])
+ # ~ new = {
+ # ~ 'factura': obj,
+ # ~ 'impuesto': imp,
+ # ~ 'importe': impuesto['importe'],
+ # ~ }
+ # ~ try:
+ # ~ with database_proxy.atomic() as txn:
+ # ~ FacturasImpuestos.create(**new)
+ # ~ except IntegrityError as e:
+ # ~ pass
- except IntegrityError as e:
- print (e)
- msg = '\tFactura: id: {}'.format(row['serie'] + str(row['folio']))
- log.error(msg)
- break
-
- log.info('\tFacturas importadas...')
- return
-
-
-def _importar_categorias(rows):
- log.info('\tImportando Categorías...')
- for row in rows:
- with database_proxy.atomic() as txn:
- try:
- Categorias.create(**row)
- except IntegrityError:
- msg = '\tCategoria: ({}) {}'.format(row['padre'], row['categoria'])
- log.error(msg)
-
- log.info('\tCategorías importadas...')
- return
-
-
-def _get_id_unidad(unidad):
- try:
- if 'pieza' in unidad.lower():
- unidad = 'pieza'
- if 'metros' in unidad.lower():
- unidad = 'metro'
- if 'tramo' in unidad.lower():
- unidad = 'paquete'
- if 'juego' in unidad.lower():
- unidad = 'par'
- if 'bolsa' in unidad.lower():
- unidad = 'globo'
- if unidad.lower() == 'no aplica':
- unidad = 'servicio'
-
- obj = SATUnidades.get(SATUnidades.name.contains(unidad))
- except SATUnidades.DoesNotExist:
- msg = '\tNo se encontró la unidad: {}'.format(unidad)
- # ~ log.error(msg)
- return unidad
-
- return str(obj.id)
-
-
-def _get_impuestos(impuestos):
- lines = '|'
- for impuesto in impuestos:
- if impuesto['tasa'] == '-2/3':
- tasa = str(round(2/3, 6))
- else:
- if impuesto['tasa'] == 'EXENTO':
- tasa = '0.00'
- else:
- tasa = str(round(float(impuesto['tasa']) / 100.0, 6))
-
- info = (
- IMPUESTOS.get(impuesto['nombre']),
- impuesto['nombre'],
- impuesto['tipo'][0],
- tasa,
- )
- lines += '|'.join(info) + '|'
- return lines
-
-
-def _generar_archivo_productos(archivo):
- rfc = input('Introduce el RFC: ').strip().upper()
- if not rfc:
- msg = 'El RFC es requerido'
- log.error(msg)
- return
-
- args = util.get_con(rfc)
- if not args:
- return
-
- conectar(args)
-
- log.info('Importando datos...')
- app = util.ImportFacturaLibre(archivo, rfc)
- if not app.is_connect:
- log.error('\t{}'.format(app._error))
- return
-
- rows = app.import_productos()
-
- p, _, _, _ = util.get_path_info(archivo)
- path_txt = util._join(p, 'productos_{}.txt'.format(rfc))
- log.info('\tGenerando archivo: {}'.format(path_txt))
-
- fields = (
- 'clave',
- 'clave_sat',
- 'unidad',
- 'categoria',
- 'descripcion',
- 'valor_unitario',
- 'existencia',
- 'inventario',
- 'codigo_barras',
- 'cuenta_predial',
- 'ultimo_precio',
- 'minimo',
- )
-
- data = ['|'.join(fields)]
- not_units = []
- for row in rows:
- impuestos = row.pop('impuestos', ())
- line = [str(row[r]) for r in fields]
- if line[10] == 'None':
- line[10] = '0.0'
- line[2] = _get_id_unidad(line[2])
- try:
- int(line[2])
- except ValueError:
- if not line[2] in not_units:
- not_units.append(line[2])
- msg = 'No se encontró la unidad: {}'.format(line[2])
- log.error(msg)
- continue
- line = '|'.join(line) + _get_impuestos(impuestos)
- data.append(line)
-
- with open(path_txt, 'w') as fh:
- fh.write('\n'.join(data))
-
- log.info('\tArchivo generado: {}'.format(path_txt))
- return
-
-
-def importar_bdfl():
- try:
- emisor = Emisor.select()[0]
- except IndexError:
- msg = 'Configura primero al emisor'
- return {'ok': False, 'msg': msg}
-
- name = '{}.sqlite'.format(emisor.rfc.lower())
- path = util._join('/tmp', name)
-
- log.info('Importando datos...')
- app = util.ImportFacturaLibre(path, emisor.rfc)
- if not app.is_connect:
- msg = app._error
- log.error('\t{}'.format(msg))
- return {'ok': False, 'msg': msg}
-
- data = app.import_data()
-
- _importar_socios(data['Socios'])
- _importar_facturas(data['Facturas'])
- _importar_categorias(data['Categorias'])
-
- msg = 'Importación terminada...'
- log.info(msg)
-
- return {'ok': True, 'msg': msg}
-
-
-def _importar_factura_libre(archivo):
- rfc = input('Introduce el RFC: ').strip().upper()
- if not rfc:
- msg = 'El RFC es requerido'
- log.error(msg)
- return
-
- args = util.get_con(rfc)
- if not args:
- return
-
- conectar(args)
-
- log.info('Importando datos...')
- app = util.ImportFacturaLibre(archivo, rfc)
- if not app.is_connect:
- log.error('\t{}'.format(app._error))
- return
-
- data = app.import_data()
-
- _importar_socios(data['Socios'])
- _importar_facturas(data['Facturas'])
- _importar_categorias(data['Categorias'])
-
- log.info('Importación terminada...')
- return
-
-
-def _exist_ticket(row):
- filters = (
- (Tickets.serie==row['serie']) &
- (Tickets.folio==row['folio'])
- )
- return Tickets.select().where(filters).exists()
-
-
-def _import_tickets(rows):
- log.info('\tImportando Tickets...')
- for row in rows:
- try:
- details = row.pop('details')
- taxes = row.pop('taxes')
- with database_proxy.atomic() as txn:
- if _exist_ticket(row):
- msg = '\tTicket existente: {}{}'.format(
- row['serie'], row['folio'])
- log.info(msg)
- continue
-
- if not row['factura'] is None and row['factura']:
- row['factura'] = Facturas.get(
- Facturas.serie==row['factura']['serie'],
- Facturas.folio==row['factura']['folio'])
- else:
- row['factura'] = None
-
- obj = Tickets.create(**row)
- for detail in details:
- detail['ticket'] = obj
- TicketsDetalle.create(**detail)
- for tax in taxes:
- imp = SATImpuestos.get(**tax['filter'])
- new = {
- 'ticket': obj,
- 'impuesto': imp,
- 'importe': tax['import'],
- }
- TicketsImpuestos.create(**new)
- except IntegrityError as e:
+ # ~ except IntegrityError as e:
# ~ print (e)
- msg = '\tTicket: id: {}'.format(row['serie'] + str(row['folio']))
- log.error(msg)
+ # ~ msg = '\tFactura: id: {}'.format(row['serie'] + str(row['folio']))
+ # ~ log.error(msg)
+ # ~ break
- log.info('\tTickets importadas...')
- return
+ # ~ log.info('\tFacturas importadas...')
+ # ~ return
-def _importar_productos(archivo):
- rfc = input('Introduce el RFC: ').strip().upper()
- if not rfc:
- msg = 'El RFC es requerido'
- log.error(msg)
- return
+# ~ def _importar_categorias(rows):
+ # ~ log.info('\tImportando Categorías...')
+ # ~ for row in rows:
+ # ~ with database_proxy.atomic() as txn:
+ # ~ try:
+ # ~ Categorias.create(**row)
+ # ~ except IntegrityError:
+ # ~ msg = '\tCategoria: ({}) {}'.format(row['padre'], row['categoria'])
+ # ~ log.error(msg)
- args = util.get_con(rfc)
- if not args:
- return
+ # ~ log.info('\tCategorías importadas...')
+ # ~ return
- conectar(args)
- log.info('Importando productos...')
- fields = (
- 'clave',
- 'clave_sat',
- 'unidad',
- 'categoria',
- 'descripcion',
- 'valor_unitario',
- 'existencia',
- 'inventario',
- 'codigo_barras',
- 'cuenta_predial',
- 'ultimo_precio',
- 'minimo',
- )
+# ~ def _get_id_unidad(unidad):
+ # ~ try:
+ # ~ if 'pieza' in unidad.lower():
+ # ~ unidad = 'pieza'
+ # ~ if 'metros' in unidad.lower():
+ # ~ unidad = 'metro'
+ # ~ if 'tramo' in unidad.lower():
+ # ~ unidad = 'paquete'
+ # ~ if 'juego' in unidad.lower():
+ # ~ unidad = 'par'
+ # ~ if 'bolsa' in unidad.lower():
+ # ~ unidad = 'globo'
+ # ~ if unidad.lower() == 'no aplica':
+ # ~ unidad = 'servicio'
- rows = util.read_file(archivo, 'r').split('\n')
- for i, row in enumerate(rows):
- if i == 0:
- continue
- data = row.split('|')
+ # ~ obj = SATUnidades.get(SATUnidades.name.contains(unidad))
+ # ~ except SATUnidades.DoesNotExist:
+ # ~ msg = '\tNo se encontró la unidad: {}'.format(unidad)
+ # ~ return unidad
+
+ # ~ return str(obj.id)
+
+
+# ~ def _get_impuestos(impuestos):
+ # ~ lines = '|'
+ # ~ for impuesto in impuestos:
+ # ~ if impuesto['tasa'] == '-2/3':
+ # ~ tasa = str(round(2/3, 6))
+ # ~ else:
+ # ~ if impuesto['tasa'] == 'EXENTO':
+ # ~ tasa = '0.00'
+ # ~ else:
+ # ~ tasa = str(round(float(impuesto['tasa']) / 100.0, 6))
+
+ # ~ info = (
+ # ~ IMPUESTOS.get(impuesto['nombre']),
+ # ~ impuesto['nombre'],
+ # ~ impuesto['tipo'][0],
+ # ~ tasa,
+ # ~ )
+ # ~ lines += '|'.join(info) + '|'
+ # ~ return lines
+
+
+# ~ def _generar_archivo_productos(archivo):
+ # ~ rfc = input('Introduce el RFC: ').strip().upper()
+ # ~ if not rfc:
+ # ~ msg = 'El RFC es requerido'
+ # ~ log.error(msg)
+ # ~ return
+
+ # ~ args = util.get_con(rfc)
+ # ~ if not args:
+ # ~ return
+
+ # ~ conectar(args)
+
+ # ~ log.info('Importando datos...')
+ # ~ app = util.ImportFacturaLibre(archivo, rfc)
+ # ~ if not app.is_connect:
+ # ~ log.error('\t{}'.format(app._error))
+ # ~ return
+
+ # ~ rows = app.import_productos()
+
+ # ~ p, _, _, _ = util.get_path_info(archivo)
+ # ~ path_txt = util._join(p, 'productos_{}.txt'.format(rfc))
+ # ~ log.info('\tGenerando archivo: {}'.format(path_txt))
+
+ # ~ fields = (
+ # ~ 'clave',
+ # ~ 'clave_sat',
+ # ~ 'unidad',
+ # ~ 'categoria',
+ # ~ 'descripcion',
+ # ~ 'valor_unitario',
+ # ~ 'existencia',
+ # ~ 'inventario',
+ # ~ 'codigo_barras',
+ # ~ 'cuenta_predial',
+ # ~ 'ultimo_precio',
+ # ~ 'minimo',
+ # ~ )
+
+ # ~ data = ['|'.join(fields)]
+ # ~ not_units = []
+ # ~ for row in rows:
+ # ~ impuestos = row.pop('impuestos', ())
+ # ~ line = [str(row[r]) for r in fields]
+ # ~ if line[10] == 'None':
+ # ~ line[10] = '0.0'
+ # ~ line[2] = _get_id_unidad(line[2])
+ # ~ try:
+ # ~ int(line[2])
+ # ~ except ValueError:
+ # ~ if not line[2] in not_units:
+ # ~ not_units.append(line[2])
+ # ~ msg = 'No se encontró la unidad: {}'.format(line[2])
+ # ~ log.error(msg)
+ # ~ continue
+ # ~ line = '|'.join(line) + _get_impuestos(impuestos)
+ # ~ data.append(line)
+
+ # ~ with open(path_txt, 'w') as fh:
+ # ~ fh.write('\n'.join(data))
+
+ # ~ log.info('\tArchivo generado: {}'.format(path_txt))
+ # ~ return
+
+
+# ~ def importar_bdfl():
+ # ~ try:
+ # ~ emisor = Emisor.select()[0]
+ # ~ except IndexError:
+ # ~ msg = 'Configura primero al emisor'
+ # ~ return {'ok': False, 'msg': msg}
+
+ # ~ name = '{}.sqlite'.format(emisor.rfc.lower())
+ # ~ path = util._join('/tmp', name)
+
+ # ~ log.info('Importando datos...')
+ # ~ app = util.ImportFacturaLibre(path, emisor.rfc)
+ # ~ if not app.is_connect:
+ # ~ msg = app._error
+ # ~ log.error('\t{}'.format(msg))
+ # ~ return {'ok': False, 'msg': msg}
+
+ # ~ data = app.import_data()
+
+ # ~ _importar_socios(data['Socios'])
+ # ~ _importar_facturas(data['Facturas'])
+ # ~ _importar_categorias(data['Categorias'])
+
+ # ~ msg = 'Importación terminada...'
+ # ~ log.info(msg)
+
+ # ~ return {'ok': True, 'msg': msg}
+
+
+# ~ def _importar_factura_libre(archivo):
+ # ~ rfc = input('Introduce el RFC: ').strip().upper()
+ # ~ if not rfc:
+ # ~ msg = 'El RFC es requerido'
+ # ~ log.error(msg)
+ # ~ return
+
+ # ~ args = util.get_con(rfc)
+ # ~ if not args:
+ # ~ return
+
+ # ~ conectar(args)
+
+ # ~ log.info('Importando datos...')
+ # ~ app = util.ImportFacturaLibre(archivo, rfc)
+ # ~ if not app.is_connect:
+ # ~ log.error('\t{}'.format(app._error))
+ # ~ return
+
+ # ~ data = app.import_data()
+
+ # ~ _importar_socios(data['Socios'])
+ # ~ _importar_facturas(data['Facturas'])
+ # ~ _importar_categorias(data['Categorias'])
+
+ # ~ log.info('Importación terminada...')
+ # ~ return
+
+
+# ~ def _exist_ticket(row):
+ # ~ filters = (
+ # ~ (Tickets.serie==row['serie']) &
+ # ~ (Tickets.folio==row['folio'])
+ # ~ )
+ # ~ return Tickets.select().where(filters).exists()
+
+
+# ~ def _import_tickets(rows):
+ # ~ log.info('\tImportando Tickets...')
+ # ~ for row in rows:
+ # ~ try:
+ # ~ details = row.pop('details')
+ # ~ taxes = row.pop('taxes')
+ # ~ with database_proxy.atomic() as txn:
+ # ~ if _exist_ticket(row):
+ # ~ msg = '\tTicket existente: {}{}'.format(
+ # ~ row['serie'], row['folio'])
+ # ~ log.info(msg)
+ # ~ continue
+
+ # ~ if not row['factura'] is None and row['factura']:
+ # ~ row['factura'] = Facturas.get(
+ # ~ Facturas.serie==row['factura']['serie'],
+ # ~ Facturas.folio==row['factura']['folio'])
+ # ~ else:
+ # ~ row['factura'] = None
+
+ # ~ obj = Tickets.create(**row)
+ # ~ for detail in details:
+ # ~ detail['ticket'] = obj
+ # ~ TicketsDetalle.create(**detail)
+ # ~ for tax in taxes:
+ # ~ imp = SATImpuestos.get(**tax['filter'])
+ # ~ new = {
+ # ~ 'ticket': obj,
+ # ~ 'impuesto': imp,
+ # ~ 'importe': tax['import'],
+ # ~ }
+ # ~ TicketsImpuestos.create(**new)
+ # ~ except IntegrityError as e:
+ # ~ print (e)
+ # ~ msg = '\tTicket: id: {}'.format(row['serie'] + str(row['folio']))
+ # ~ log.error(msg)
+
+ # ~ log.info('\tTickets importadas...')
+ # ~ return
+
+
+# ~ def _importar_productos(archivo):
+ # ~ rfc = input('Introduce el RFC: ').strip().upper()
+ # ~ if not rfc:
+ # ~ msg = 'El RFC es requerido'
+ # ~ log.error(msg)
+ # ~ return
+
+ # ~ args = util.get_con(rfc)
+ # ~ if not args:
+ # ~ return
+
+ # ~ conectar(args)
+ # ~ log.info('Importando productos...')
+
+ # ~ fields = (
+ # ~ 'clave',
+ # ~ 'clave_sat',
+ # ~ 'unidad',
+ # ~ 'categoria',
+ # ~ 'descripcion',
+ # ~ 'valor_unitario',
+ # ~ 'existencia',
+ # ~ 'inventario',
+ # ~ 'codigo_barras',
+ # ~ 'cuenta_predial',
+ # ~ 'ultimo_precio',
+ # ~ 'minimo',
+ # ~ )
+
+ # ~ rows = util.read_file(archivo, 'r').split('\n')
+ # ~ for i, row in enumerate(rows):
+ # ~ if i == 0:
+ # ~ continue
+ # ~ data = row.split('|')
# ~ print (data)
- new = {}
- for i, f in enumerate(fields):
- if not len(data[0]):
- continue
+ # ~ new = {}
+ # ~ for i, f in enumerate(fields):
+ # ~ if not len(data[0]):
+ # ~ continue
- if i in (2, 3):
- try:
- new[f] = int(data[i])
- except ValueError:
- continue
- elif i in (5, 6, 10, 11):
- new[f] = float(data[i])
- elif i == 7:
- new[f] = bool(data[i])
- else:
- new[f] = data[i]
+ # ~ if i in (2, 3):
+ # ~ try:
+ # ~ new[f] = int(data[i])
+ # ~ except ValueError:
+ # ~ continue
+ # ~ elif i in (5, 6, 10, 11):
+ # ~ new[f] = float(data[i])
+ # ~ elif i == 7:
+ # ~ new[f] = bool(data[i])
+ # ~ else:
+ # ~ new[f] = data[i]
- impuestos = data[i + 1:-1]
- if not impuestos:
- taxes = [SATImpuestos.select().where(SATImpuestos.id==6)]
- else:
- taxes = []
- try:
- for i in range(0, len(impuestos), 4):
- w = {
- 'key': impuestos[i],
- 'name': impuestos[i+1],
- 'tipo': impuestos[i+2],
- 'tasa': float(impuestos[i+3]),
- }
- taxes.append(SATImpuestos.get_o_crea(w))
- except IndexError:
- print ('IE', data)
- continue
+ # ~ impuestos = data[i + 1:-1]
+ # ~ if not impuestos:
+ # ~ taxes = [SATImpuestos.select().where(SATImpuestos.id==6)]
+ # ~ else:
+ # ~ taxes = []
+ # ~ try:
+ # ~ for i in range(0, len(impuestos), 4):
+ # ~ w = {
+ # ~ 'key': impuestos[i],
+ # ~ 'name': impuestos[i+1],
+ # ~ 'tipo': impuestos[i+2],
+ # ~ 'tasa': float(impuestos[i+3]),
+ # ~ }
+ # ~ taxes.append(SATImpuestos.get_o_crea(w))
+ # ~ except IndexError:
+ # ~ print ('IE', data)
+ # ~ continue
- with database_proxy.transaction():
- try:
- obj = Productos.create(**new)
- obj.impuestos = taxes
- except IntegrityError as e:
- pass
+ # ~ with database_proxy.transaction():
+ # ~ try:
+ # ~ obj = Productos.create(**new)
+ # ~ obj.impuestos = taxes
+ # ~ except IntegrityError as e:
+ # ~ pass
- log.info('Importación terminada...')
- return
+ # ~ log.info('Importación terminada...')
+ # ~ return
def _import_from_folder(path):
diff --git a/source/app/seafileapi/__init__.py b/source/app/seafileapi/__init__.py
deleted file mode 100644
index d6c3b8d..0000000
--- a/source/app/seafileapi/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from seafileapi.client import SeafileApiClient
-
-def connect(server, username, password):
- client = SeafileApiClient(server, username, password)
- return client
diff --git a/source/app/seafileapi/admin.py b/source/app/seafileapi/admin.py
deleted file mode 100644
index 08fc2c4..0000000
--- a/source/app/seafileapi/admin.py
+++ /dev/null
@@ -1,7 +0,0 @@
-
-class SeafileAdmin(object):
- def lists_users(self, maxcount=100):
- pass
-
- def list_user_repos(self, username):
- pass
diff --git a/source/app/seafileapi/client.py b/source/app/seafileapi/client.py
deleted file mode 100644
index 52a6ea6..0000000
--- a/source/app/seafileapi/client.py
+++ /dev/null
@@ -1,77 +0,0 @@
-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
diff --git a/source/app/seafileapi/exceptions.py b/source/app/seafileapi/exceptions.py
deleted file mode 100644
index b11498d..0000000
--- a/source/app/seafileapi/exceptions.py
+++ /dev/null
@@ -1,25 +0,0 @@
-
-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
diff --git a/source/app/seafileapi/files.py b/source/app/seafileapi/files.py
deleted file mode 100644
index ed01e64..0000000
--- a/source/app/seafileapi/files.py
+++ /dev/null
@@ -1,250 +0,0 @@
-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__
diff --git a/source/app/seafileapi/group.py b/source/app/seafileapi/group.py
deleted file mode 100644
index 731d7ef..0000000
--- a/source/app/seafileapi/group.py
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-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
diff --git a/source/app/seafileapi/repo.py b/source/app/seafileapi/repo.py
deleted file mode 100644
index 01811a2..0000000
--- a/source/app/seafileapi/repo.py
+++ /dev/null
@@ -1,99 +0,0 @@
-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)
diff --git a/source/app/seafileapi/repos.py b/source/app/seafileapi/repos.py
deleted file mode 100644
index 70a8fa7..0000000
--- a/source/app/seafileapi/repos.py
+++ /dev/null
@@ -1,26 +0,0 @@
-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]
diff --git a/source/app/seafileapi/utils.py b/source/app/seafileapi/utils.py
deleted file mode 100644
index 7903414..0000000
--- a/source/app/seafileapi/utils.py
+++ /dev/null
@@ -1,57 +0,0 @@
-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
diff --git a/source/app/settings.py b/source/app/settings.py
index 9346afe..9b7af37 100644
--- a/source/app/settings.py
+++ b/source/app/settings.py
@@ -30,11 +30,6 @@ try:
except ImportError:
DEFAULT_PASSWORD = 'salgueiro3.3'
-try:
- from conf import SEAFILE_SERVER
-except ImportError:
- SEAFILE_SERVER = {}
-
try:
from conf import TITLE_APP
except ImportError:
@@ -47,7 +42,7 @@ except ImportError:
DEBUG = DEBUG
-VERSION = '1.39.1'
+VERSION = '1.40.0'
EMAIL_SUPPORT = ('soporte@empresalibre.mx',)
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
@@ -78,8 +73,8 @@ PATH_SESSIONS = {
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))
+# ~ 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'))
@@ -222,6 +217,7 @@ PATHS = {
'BK': path_bk,
'LOCAL': path_local,
'SAT': path_sat,
+ 'xslt': PATH_XSLT,
}
VALUES_PDF = {
@@ -237,6 +233,8 @@ VALUES_PDF = {
RFCS = {
'PUBLIC': 'XAXX010101000',
'FOREIGN': 'XEXX010101000',
+ 'CVD110412TF6': 'finkok',
+ 'SCD110105654': 'comercio',
}
URL = {
@@ -249,3 +247,32 @@ DEFAULT_GLOBAL = {
'descripcion': 'Venta',
'clave_sat': '01010101',
}
+
+TEMPLATE_CANCEL = """
+
+ {uuid}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+"""
diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js
index 398dd04..b004781 100644
--- a/source/static/js/controller/admin.js
+++ b/source/static/js/controller/admin.js
@@ -19,6 +19,9 @@ var msg = ''
var tb_options = null
var tb_sat = null
+var file_cer = null
+var file_key = null
+
var controllers = {
init: function(){
@@ -32,7 +35,9 @@ var controllers = {
$$('chk_escuela').attachEvent('onChange', chk_escuela_change)
$$('chk_ong').attachEvent('onChange', chk_ong_change)
$$('cmd_subir_certificado').attachEvent('onItemClick', cmd_subir_certificado_click)
- $$('up_cert').attachEvent('onUploadComplete', up_cert_upload_complete)
+ //~ $$('up_cert').attachEvent('onUploadComplete', up_cert_upload_complete)
+ //~ $$('up_cert').attachEvent('onAfterFileAdd', up_cert_after_file_add)
+ $$('up_cert').attachEvent('onBeforeFileAdd', up_cert_before_file_add)
$$('cmd_agregar_serie').attachEvent('onItemClick', cmd_agregar_serie_click)
$$('grid_folios').attachEvent('onItemClick', grid_folios_click)
$$('chk_folio_custom').attachEvent('onItemClick', chk_config_item_click)
@@ -135,6 +140,7 @@ var controllers = {
$$('chk_ticket_user_show_doc').attachEvent('onItemClick', chk_config_item_click)
$$('txt_ticket_printer').attachEvent('onKeyPress', txt_ticket_printer_key_press)
$$('lst_pac').attachEvent('onChange', lst_pac_on_change)
+ $$('cmd_save_pac').attachEvent('onItemClick', cmd_save_pac_click)
$$('cmd_subir_bdfl').attachEvent('onItemClick', cmd_subir_bdfl_click)
$$('cmd_subir_cfdixml').attachEvent('onItemClick', cmd_subir_cfdixml_click)
@@ -279,7 +285,7 @@ function get_emisor(){
function get_certificado(){
var form = $$('form_cert')
- webix.ajax().get("/values/cert", {}, {
+ webix.ajax().get("/cert", {'opt': 'cert'}, {
error: function(text, data, xhr) {
msg = 'Error al consultar'
msg_error(msg)
@@ -480,7 +486,7 @@ function get_config_values(opt){
var values = data.json()
Object.keys(values).forEach(function(key){
if(key=='lst_pac'){
- set_value(key, values[key])
+ $$('lst_pac').setValue(values[key])
}else{
$$(key).setValue(values[key])
if(key=='chk_config_leyendas_fiscales'){
@@ -601,105 +607,6 @@ function chk_ong_change(new_value, old_value){
}
-function cmd_subir_certificado_click(){
- var form = $$('form_upload')
-
- if (!form.validate()){
- msg = 'Valores inválidos'
- msg_error(msg)
- return
- }
-
- var values = form.getValues()
-
- if(!values.contra.trim()){
- msg = 'La contraseña no puede estar vacía'
- msg_error(msg)
- return
- }
-
- if($$('lst_cert').count() < 2){
- msg = 'Selecciona al menos dos archivos: CER y KEY del certificado.'
- msg_error(msg)
- return
- }
-
- if($$('lst_cert').count() > 2){
- msg = 'Selecciona solo dos archivos: CER y KEY del certificado.'
- msg_error(msg)
- return
- }
-
- var fo1 = $$('up_cert').files.getItem($$('up_cert').files.getFirstId())
- var fo2 = $$('up_cert').files.getItem($$('up_cert').files.getLastId())
-
- var ext = ['key', 'cer']
- if(ext.indexOf(fo1.type.toLowerCase()) == -1 || ext.indexOf(fo2.type.toLowerCase()) == -1){
- msg = 'Archivos inválidos, se requiere un archivo CER y un KEY.'
- msg_error(msg)
- return
- }
-
- if(fo1.type == fo2.type && fo1.size == fo2.size){
- msg = 'Selecciona archivos diferentes: un archivo CER y un KEY.'
- msg_error(msg)
- return
- }
-
- var serie = $$('form_cert').getValues()['cert_serie']
-
- if(serie){
- msg = 'Ya existe un certificado guardado
¿Deseas reemplazarlo?'
- webix.confirm({
- title: 'Certificado Existente',
- ok: 'Si',
- cancel: 'No',
- type: 'confirm-error',
- text: msg,
- callback:function(result){
- if(result){
- $$('up_cert').send()
- }
- }
- })
- }else{
- $$('up_cert').send()
- }
-}
-
-
-function up_cert_upload_complete(response){
- if(response.status != 'server'){
- msg = 'Ocurrio un error al subir los archivos'
- msg_error(msg)
- return
- }
-
- msg = 'Archivos subidos correctamente. Esperando validación'
- msg_ok(msg)
-
- var values = $$('form_upload').getValues()
- $$('form_upload').setValues({})
- $$('up_cert').files.data.clearAll()
-
- webix.ajax().post('/values/cert', values, {
- error:function(text, data, XmlHttpRequest){
- msg = 'Ocurrio un error, consulta a soporte técnico'
- msg_error(msg)
- },
- success:function(text, data, XmlHttpRequest){
- var values = data.json()
- if(values.ok){
- $$('form_cert').setValues(values.data)
- msg_ok(values.msg)
- }else{
- msg_error(values.msg)
- }
- }
- })
-}
-
-
function cmd_agregar_serie_click(){
var form = $$('form_folios')
var grid = $$('grid_folios')
@@ -2558,37 +2465,6 @@ function opt_make_pdf_from_on_change(new_value, old_value){
}
-function lst_pac_on_change(nv, ov){
- if(nv=='default'){
- webix.ajax().del('/config', {id: 'lst_pac'}, function(text, xml, xhr){
- var msg = 'PAC predeterminado establecido correctamente'
- if(xhr.status == 200){
- msg_ok(msg)
- }else{
- msg = 'No se pudo eliminar'
- msg_error(msg)
- }
- })
- }else{
- webix.ajax().post('/config', {'lst_pac': nv}, {
- error: function(text, data, xhr) {
- msg = 'Error al guardar la configuración'
- msg_error(msg)
- },
- success: function(text, data, xhr) {
- var values = data.json();
- if (values.ok){
- msg = 'PAC establecido correctamente'
- msg_ok(msg)
- }else{
- msg_error(values.msg)
- }
- }
- })
- }
-}
-
-
function admin_config_other_options(id){
if(id=='chk_config_leyendas_fiscales'){
var value = Boolean($$(id).getValue())
@@ -2693,3 +2569,171 @@ function delete_leyenda_fiscal(id){
}
})
}
+
+
+function lst_pac_on_change(nv, ov){
+ webix.ajax().get('/config', {'fields': 'pac', 'pac': nv}, {
+ error: function(text, data, xhr) {
+ msg = 'Error al consultar'
+ msg_error(msg)
+ },
+ success: function(text, data, xhr) {
+ var values = data.json()
+ Object.keys(values).forEach(function(key){
+ set_value(key, values[key])
+ })
+ }
+ })
+
+}
+
+
+function cmd_save_pac_click(){
+ var pac = $$('lst_pac').getValue()
+ var user = $$('user_timbrado').getValue()
+ var token = $$('token_timbrado').getValue()
+
+ if(!pac.trim()){
+ msg = 'Selecciona un PAC'
+ msg_error(msg)
+ return
+ }
+ if(!user.trim()){
+ msg = 'El Usuario es requerido'
+ msg_error(msg)
+ return
+ }
+ if(!token.trim()){
+ msg = 'El Token es requerido'
+ msg_error(msg)
+ return
+ }
+
+ var values = {
+ opt: 'save_pac',
+ lst_pac: pac,
+ user_timbrado: user,
+ token_timbrado: token,
+ }
+
+ webix.ajax().post('/config', values, {
+ error: function(text, data, xhr) {
+ msg = 'Error al guardar el PAC'
+ msg_error(msg)
+ },
+ success: function(text, data, xhr) {
+ var values = data.json();
+ if (values.ok){
+ msg = 'PAC guardado correctamente'
+ msg_ok(msg)
+ }else{
+ msg_error(values.msg)
+ }
+ }
+ })
+}
+
+
+function cmd_subir_certificado_click(){
+ var form = $$('form_upload')
+
+ if (!form.validate()){
+ msg = 'Valores inválidos'
+ msg_error(msg)
+ return
+ }
+
+ var values = form.getValues()
+
+ if(!values.contra.trim()){
+ msg = 'La contraseña no puede estar vacía'
+ msg_error(msg)
+ return
+ }
+
+ var serie = $$('form_cert').getValues()['cert_serie']
+
+ if(serie){
+ msg = 'Ya existe un certificado guardado
¿Deseas reemplazarlo?'
+ webix.confirm({
+ title: 'Certificado Existente',
+ ok: 'Si',
+ cancel: 'No',
+ type: 'confirm-error',
+ text: msg,
+ callback:function(result){
+ if(!result){
+ return
+ }
+ }
+ })
+ }
+
+ $$('form_upload').setValues({})
+ $$('up_cert').files.data.clearAll()
+
+ values['cer'] = file_cer
+ values['key'] = file_key
+ validate_cert(values)
+}
+
+
+function up_cert_before_file_add(file){
+ if (file.type.toLowerCase() != 'cer' && file.type.toLowerCase() != 'key'){
+ msg_error('Selecciona un archivo CER o KEY')
+ return false
+ }
+
+ var count = $$('lst_cert').count()
+ if (count > 1){
+ msg = 'Selecciona solo dos archivos: CER y KEY del certificado.'
+ msg_error(msg)
+ return false
+ }
+
+ if (count > 0){
+ var f = $$('up_cert').files.getItem($$('up_cert').files.getFirstId())
+ if (f.type.toLowerCase() == file.type.toLowerCase()){
+ msg = 'Selecciona archivos diferentes: un archivo CER y un KEY.'
+ msg_error(msg)
+ return false
+ }
+ }
+
+ var reader = new FileReader();
+ if (file.type.toLowerCase() == 'cer'){
+ reader.addEventListener('load', (event) => {
+ file_cer = event.target.result;
+ });
+ reader.readAsDataURL(file.file);
+ } else {
+ reader.addEventListener('load', (event) => {
+ file_key = event.target.result;
+ });
+ reader.readAsDataURL(file.file);
+ }
+}
+
+
+function validate_cert(values){
+ msg = 'Archivos recibidos correctamente. Esperando validación'
+ msg_ok(msg)
+
+ values['opt'] = 'validate_cert'
+ webix.ajax().post('/cert', values, {
+ error:function(text, data, XmlHttpRequest){
+ msg = 'Ocurrio un error, consulta a soporte técnico'
+ msg_error(msg)
+ },
+ success:function(text, data, XmlHttpRequest){
+ var values = data.json()
+ if(values.ok){
+ $$('form_cert').setValues(values.data)
+ msg_ok(values.msg)
+ }else{
+ msg_error(values.msg)
+ }
+ }
+ })
+}
+
diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js
index 1cbd0a3..c95f30f 100644
--- a/source/static/js/controller/invoices.js
+++ b/source/static/js/controller/invoices.js
@@ -1366,6 +1366,7 @@ function send_cancel(id){
msg_ok(values.msg)
gi.updateItem(id, values.row)
}else{
+ msg_error('No fue posible cancelar')
webix.alert({
title: 'Error al Cancelar',
text: values.msg,
diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js
index b0de215..5af4922 100644
--- a/source/static/js/ui/admin.js
+++ b/source/static/js/ui/admin.js
@@ -238,11 +238,11 @@ var emisor_otros_datos= [
{cols: [{view: 'datepicker', id: 'ong_fecha_dof', name: 'ong_fecha_dof',
label: 'Fecha de DOF: ', disabled: true, format: '%d-%M-%Y',
placeholder: 'Fecha de publicación en el DOF'}, {}]},
- {template: 'Timbrado y Soporte', type: 'section'},
- {view: 'text', id: 'correo_timbrado',
- name: 'correo_timbrado', label: 'Usuario para Timbrado: '},
- {view: 'text', id: 'token_timbrado',
- name: 'token_timbrado', label: 'Token de Timbrado: '},
+ {template: 'Soporte', type: 'section'},
+ //~ {view: 'text', id: 'correo_timbrado',
+ //~ name: 'correo_timbrado', label: 'Usuario para Timbrado: '},
+ //~ {view: 'text', id: 'token_timbrado',
+ //~ name: 'token_timbrado', label: 'Token de Timbrado: '},
{view: 'text', id: 'token_soporte',
name: 'token_soporte', label: 'Token de Soporte: '},
]
@@ -278,13 +278,16 @@ var col_fiel = {rows: [
]}
+ //~ {view: 'uploader', id: 'up_cert', autosend: false, link: 'lst_cert',
+ //~ value: 'Seleccionar certificado', upload: '/values/files'}, {}]},
+
var emisor_certificado = [
{cols: [col_sello, col_fiel]},
{template: 'Cargar Certificado', type: 'section'},
{view: 'form', id: 'form_upload', rows: [
{cols: [{},
{view: 'uploader', id: 'up_cert', autosend: false, link: 'lst_cert',
- value: 'Seleccionar certificado', upload: '/values/files'}, {}]},
+ value: 'Seleccionar certificado'}, {}]},
{cols: [{},
{view: 'list', id: 'lst_cert', name: 'certificado',
type: 'uploader', autoheight:true, borderless: true}, {}]},
@@ -644,7 +647,7 @@ var options_templates = [
var options_pac = [
- {id: 'default', value: 'Predeterminado'},
+ {id: 'finkok', value: 'Finkok'},
{id: 'comercio', value: 'Comercio Digital'},
]
@@ -690,12 +693,26 @@ var options_admin_otros = [
{},
]},
{maxHeight: 15},
+
+ {template: 'Timbrado', type: 'section'},
{cols: [{maxWidth: 15},
{view: 'richselect', id: 'lst_pac', name: 'lst_pac', width: 300,
- label: 'PAC: ', value: 'default', required: false,
- options: options_pac}, {view: 'label', label: 'NO cambies este valor, a menos que se te haya indicado'},
+ label: 'PAC: ', value: '', required: true,
+ labelAlign: 'right', options: options_pac}, {view: 'label',
+ label: ' NO cambies este valor, a menos que se te haya indicado'},
+ ]},
+ {cols: [{maxWidth: 15},
+ {view: 'text', id: 'user_timbrado', name: 'user_timbrado',
+ label: 'Usuario: ', labelAlign: 'right', required: true},
+ {view: 'text', id: 'token_timbrado', name: 'token_timbrado',
+ label: 'Token: ', labelAlign: 'right', required: true},
+ ]},
+ {cols: [{maxWidth: 15}, {},
+ {view: 'button', id: 'cmd_save_pac', label: 'Guardar',
+ autowidth: true, type: 'form'}, {},
]},
{maxHeight: 20},
+
{template: 'Ayudas varias', type: 'section'},
{cols: [{maxWidth: 15},
{view: 'checkbox', id: 'chk_config_anticipo', labelWidth: 0,
diff --git a/source/templates/plantilla_factura.ods b/source/templates/plantilla_factura.ods
index 0505175..d54d86d 100644
Binary files a/source/templates/plantilla_factura.ods and b/source/templates/plantilla_factura.ods differ
diff --git a/source/xslt/servicioconstruccion.xslt b/source/xslt/servicioconstruccion.xslt
index 4abc2d5..0ac87d9 100644
--- a/source/xslt/servicioconstruccion.xslt
+++ b/source/xslt/servicioconstruccion.xslt
@@ -1,5 +1,5 @@
-
+