cfdi-test/source/finkok/finkok.py

838 lines
24 KiB
Python

#!/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 <http://www.gnu.org/licenses/>.
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, DEBUG_SOAP, AUTH
logging.getLogger('zeep').setLevel(logging.ERROR)
logging.getLogger('requests').setLevel(logging.ERROR)
TIMEOUT = 10
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',
'1000': 'Comprobante recibido satisfactoriamente',
}
def __init__(self):
self.error = ''
self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT)
self._plugins = [DebugPlugin()]
def _validate_result(self, result):
# ~ print('RESULT\r', result)
if hasattr(result, 'CodEstatus'):
ce = result.CodEstatus
if not ce is None:
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 == self.CODE['1000']:
return result
if self.CODE['200'] != ce:
print('CodEstatus', type(ce), ce)
return result
if hasattr(result, 'Incidencias'):
fault = result.Incidencias.Incidencia[0]
cod_error = fault.CodigoError
msg_error = fault.MensajeIncidencia
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<BR><BR><b>Documento muy grande para timbrar</b>'
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 stamp(self, cfdi, auth={}):
if 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,
}
result = self._get_result(client, 'stamp', args)
if self.error:
return ''
# ~ data = {
# ~ 'xml': self._to_string(result.xml),
# ~ 'uuid': result.UUID,
# ~ 'fecha': result.Fecha,
# ~ }
return result.xml
def cfdi_quick_stamp(self, cfdi, auth={}):
if not auth:
auth = FINKOK['AUTH']
method = 'timbra'
client = Client(
self.URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': auth['USER'],
'password': auth['PASS'],
'xml': cfdi,
}
result = self._get_result(client, 'quick_stamp', args)
if self.error:
return {}
data = {
'xml': self._to_string(result.xml),
'uuid': result.UUID,
'fecha': result.Fecha,
}
return data
def cfdi_stamped(self, cfdi, auth={}):
if not auth:
auth = FINKOK['AUTH']
method = 'timbra'
client = Client(
self.URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': auth['USER'],
'password': auth['PASS'],
'xml': cfdi,
}
result = self._get_result(client, 'stamped', args)
if self.error:
return {}
data = {
'xml': self._to_string(result.xml),
'uuid': result.UUID,
'fecha': result.Fecha,
}
return data
def cfdi_query_pending(self, uuid, auth={}):
if not auth:
auth = FINKOK['AUTH']
method = 'timbra'
client = Client(
self.URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': auth['USER'],
'password': auth['PASS'],
'uuid': uuid,
}
result = self._get_result(client, 'query_pending', args)
if self.error:
return {}
STATUS = {
'S': 'Timbrado, aún no eviado al SAT',
'F': 'Timbrado y enviado al SAT',
}
data = {
'status': STATUS[result.status],
'xml': self._to_string(unescape(result.xml)),
}
return data
def cfdi_sign_stamp(self, cfdi, auth={}):
if not auth:
auth = FINKOK['AUTH']
method = 'timbra'
client = Client(
self.URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': auth['USER'],
'password': auth['PASS'],
'xml': cfdi,
}
result = self._get_result(client, 'sign_stamp', args)
if self.error:
return {}
data = {
'xml': self._to_string(result.xml),
'uuid': result.UUID,
'fecha': result.Fecha,
}
return data
# For test
def cfdi_cancel(self, rfc, uuid, cer, key, auth={}):
if not auth:
auth = FINKOK['AUTH']
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=uuid)),
'username': auth['USER'],
'password': auth['PASS'],
'taxpayer_id': rfc,
'cer': cer,
'key': key,
'store_pending': False,
}
result = self._get_result(client, 'cancel', args)
if self.error:
return {}
return result
def cfdi_status(self, uuid, auth={}):
if not auth:
auth = FINKOK['AUTH']
method = 'timbra'
client = Client(
self.URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': auth['USER'],
'password': auth['PASS'],
'uuid': uuid,
}
result = self._get_result(client, 'query_pending', args)
if self.error:
return {}
STATUS = {
'C': 'Cancelado',
'S': 'Timbrado, aún no eviado al SAT',
'F': 'Timbrado y enviado al SAT',
}
data = {
'estatus': STATUS[result.status],
'xml': self._to_string(unescape(result.xml)),
'fecha': result.date,
}
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 or True == 'O' == On demand
Returns:
True or False
origin PAC
'message':
'Account Created successfully'
'Account Already exists'
'success': True or False
"""
auth = FINKOK['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_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:
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 = FINKOK['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:
return ''
if not result.success:
self.error = result.message
return ''
return result.token
# ~ Send issue to PAC
def client_reset_token(self, email):
auth = FINKOK['RESELLER']
method = 'util'
client = Client(
self.URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': auth['USER'],
'password': auth['PASS'],
'token': email,
}
result = self._get_result(client, 'reset_token', args)
if self.error:
return ''
if not result.success:
self.error = result.message
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 = FINKOK['RESELLER']
if not isinstance(credit, int):
self.error = 'El credito debe ser un entero'
return 0
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:
return ''
if not result.success:
self.error = result.message
return 0
return result.credit
def client_edit(self, rfc, status=True):
"""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 or False == 'S' == Suspendido
Returns:
dict
'message':
'Account Created successfully'
'Account Already exists'
'success': True or False
"""
auth = FINKOK['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_get(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 = FINKOK['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,
}
try:
self.result = client.service.get(**args)
except Fault as e:
self.error = str(e)
return {}
except TransportError as e:
self.error = str(e)
return {}
except ConnectionError:
self.error = 'Verifica la conexión a internet'
return {}
success = bool(self.result.users)
if not success:
self.error = self.result.message or 'RFC no existe'
return {}
data = self.result.users.ResellerUser[0]
client = {
'status': data.status,
'counter': data.counter,
'credit': data.credit,
}
return client
def client_get_timbres(self, rfc, auth={}):
"""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 = FINKOK['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,
}
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_server_datetime(self):
"""Regresa la fecha y hora del servidor de timbrado del PAC
"""
auth = FINKOK['RESELLER']
method = 'util'
client = Client(
self.URL[method], transport=self._transport, plugins=self._plugins)
try:
self.result = client.service.datetime(auth['USER'], auth['PASS'])
except Fault as e:
self.error = str(e)
return None
except TransportError as e:
self.error = str(e)
return None
except ConnectionError:
self.error = 'Verifica la conexión a internet'
return None
try:
dt = datetime.datetime.strptime(
self.result.datetime, '%Y-%m-%dT%H:%M:%S')
except ValueError:
self.error = 'Error al obtener la fecha'
return None
return dt
def get_report_credit(self, rfc):
"""Obtiene un reporte de los timbres agregados
"""
auth = FINKOK['RESELLER']
args = {
'username': auth['USER'],
'password': auth['PASS'],
'taxpayer_id': rfc,
}
method = 'util'
client = Client(
self.URL[method], transport=self._transport, plugins=self._plugins)
try:
self.result = client.service.report_credit(**args)
except Fault as e:
self.error = str(e)
return []
except TransportError as e:
self.error = str(e)
return []
except ConnectionError:
self.error = 'Verifica la conexión a internet'
return []
if self.result.result is None:
# ~ PAC - Debería regresar RFC inexistente o sin registros
self.error = 'RFC no existe o no tiene registros'
return []
return self.result.result.ReportTotalCredit
def _get_dates(self, year, month):
y = year
n = datetime.datetime.now()
if not year:
y = n.year
return
def get_report_total(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)
try:
result = client.service.report_total(**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
if result.result is None:
# ~ PAC - Debería regresar RFC inexistente o sin registros
# ~ self.error = 'RFC no existe o no tiene registros'
self.error = result.error
return 0
return result.result.ReportTotal[0].total or 0
def get_report_uuid(self, rfc, date_from, date_to, invoice_type='I'):
"""Obtiene un reporte de los CFDI timbrados
"""
auth = FINKOK['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)
try:
self.result = client.service.report_uuid(**args)
except Fault as e:
self.error = str(e)
return []
except TransportError as e:
self.error = str(e)
return []
except ConnectionError:
self.error = 'Verifica la conexión a internet'
return []
if self.result.invoices is None:
# ~ PAC - Debería regresar RFC inexistente o sin registros
self.error = 'RFC no existe o no tiene registros'
return []
return self.result.invoices.ReportUUID
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 cfdi_get_by_xml(self, xml, auth):
if not auth:
auth = FINKOK['AUTH']
method = 'timbra'
client = Client(
self.URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': auth['USER'],
'password': auth['PASS'],
'xml': xml,
}
try:
result = client.service.stamped(**args)
except Fault as e:
self.error = str(e)
return {}
except TransportError as e:
self.error = str(e)
return {}
except ConnectionError as e:
msg = '502 - Error de conexión'
self.error = msg
return {}
print(result)
error = 'Error: {}\n{}'.format(code_error, msg_error)
self.error = self.CODE.get(code_error, error)
return {}
def cfdi_get_by_uuid(self, uuid, rfc, invoice_type='I', auth={}):
if not auth:
auth = FINKOK['AUTH']
method = 'util'
client = Client(
URL[method], transport=self._transport, plugins=self._plugins)
args = {
'username': auth['USER'],
'password': auth['PASS'],
'uuid': uuid,
'taxpayer_id': rfc,
'invoice_type': invoice_type,
}
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 {}
except ConnectionError as e:
msg = '502 - Error de conexión'
self.error = msg
return {}
print(result)
error = 'Error: {}\n{}'.format(code_error, msg_error)
self.error = self.CODE.get(code_error, error)
return {}