307 lines
9.3 KiB
Python
307 lines
9.3 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import base64
|
|
import datetime
|
|
import random
|
|
import string
|
|
import sys
|
|
import time
|
|
import unittest
|
|
import uuid
|
|
import lxml.etree as ET
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
|
|
sys.path.append('..')
|
|
from pycert import SATCertificate
|
|
from finkok import PACFinkok
|
|
|
|
|
|
NAME = 'finkok'
|
|
|
|
|
|
TEMPLATE_CFDI = """<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" LugarExpedicion="06850" Moneda="MXN" SubTotal="10000.00" TipoCambio="1" TipoDeComprobante="I" Total="11600.00" FormaPago="01" MetodoPago="PUE" Version="3.3" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd">
|
|
<cfdi:Emisor Rfc="EKU9003173C9" RegimenFiscal="601"/>
|
|
<cfdi:Receptor Rfc="BASM740115RW0" UsoCFDI="G01"/>
|
|
<cfdi:Conceptos>
|
|
<cfdi:Concepto Cantidad="1.0" ClaveProdServ="60121001" ClaveUnidad="ACT" Descripcion="Asesoría en desarrollo" Importe="10000.00" ValorUnitario="10000.00">
|
|
<cfdi:Impuestos>
|
|
<cfdi:Traslados>
|
|
<cfdi:Traslado Base="10000.00" Importe="1600.00" Impuesto="002" TasaOCuota="0.160000" TipoFactor="Tasa"/>
|
|
</cfdi:Traslados>
|
|
</cfdi:Impuestos>
|
|
</cfdi:Concepto>
|
|
</cfdi:Conceptos>
|
|
<cfdi:Impuestos TotalImpuestosTrasladados="1600.00">
|
|
<cfdi:Traslados>
|
|
<cfdi:Traslado Importe="1600.00" Impuesto="002" TasaOCuota="0.160000" TipoFactor="Tasa"/>
|
|
</cfdi:Traslados>
|
|
</cfdi:Impuestos>
|
|
</cfdi:Comprobante>
|
|
"""
|
|
|
|
|
|
TEMPLATE_CANCEL = """<Cancelacion RfcEmisor="{rfc}" Fecha="{fecha}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://cancelacfd.sat.gob.mx">
|
|
<Folios>
|
|
<UUID>{uuid}</UUID>
|
|
</Folios>
|
|
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
|
|
<SignedInfo>
|
|
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
|
|
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
|
|
<Reference URI="">
|
|
<Transforms>
|
|
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
|
|
</Transforms>
|
|
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
|
|
<DigestValue />
|
|
</Reference>
|
|
</SignedInfo>
|
|
<SignatureValue />
|
|
<KeyInfo>
|
|
<X509Data>
|
|
<X509SubjectName />
|
|
<X509IssuerSerial />
|
|
<X509Certificate />
|
|
</X509Data>
|
|
<KeyValue />
|
|
</KeyInfo>
|
|
</Signature>
|
|
</Cancelacion>
|
|
"""
|
|
|
|
|
|
class TestCfdi(object):
|
|
|
|
def __init__(self):
|
|
self._xml = ''
|
|
self._make_cfdi()
|
|
|
|
@property
|
|
def xml(self):
|
|
return self._xml.decode()
|
|
|
|
def _make_cfdi(self):
|
|
path = Path(__file__)
|
|
path_cer = Path(path.parent).joinpath('certificados', f'{NAME}.cer')
|
|
path_key = Path(path.parent).joinpath('certificados', f'{NAME}.enc')
|
|
path_xslt = Path(path.parent).joinpath('xslt', 'cadena.xslt')
|
|
self._cer_ori = cer = path_cer.read_bytes()
|
|
self._key_ori = key = path_key.read_bytes()
|
|
|
|
self._cert = SATCertificate(cer, key)
|
|
self._doc = ET.parse(BytesIO(TEMPLATE_CFDI.encode()))
|
|
self._root = self._doc.getroot()
|
|
self._root.attrib['Fecha'] = datetime.datetime.now().isoformat()[:19]
|
|
self._root.attrib['NoCertificado'] = self._cert.serial_number
|
|
self._root.attrib['Certificado'] = self._cert.cer_txt
|
|
|
|
self._add_stamp(path_xslt)
|
|
|
|
self._xml = ET.tostring(self._root,
|
|
pretty_print=True, doctype='<?xml version="1.0" encoding="utf-8"?>')
|
|
return
|
|
|
|
def _add_stamp(self, path_xslt):
|
|
xslt = open(path_xslt, 'rb')
|
|
transfor = ET.XSLT(ET.parse(xslt))
|
|
cadena = str(transfor(self._doc)).encode()
|
|
stamp = self._cert.sign(cadena)
|
|
self._root.attrib['Sello'] = stamp
|
|
xslt.close()
|
|
return
|
|
|
|
def sign_xml(self, template):
|
|
tree = ET.fromstring(template.encode())
|
|
tree = self._cert.sign_xml(tree)
|
|
xml = ET.tostring(tree).decode()
|
|
return xml
|
|
|
|
@property
|
|
def cert(self):
|
|
cer = base64.b64encode(self._cer_ori).decode()
|
|
key = base64.b64encode(self._key_ori).decode()
|
|
return key, cer
|
|
|
|
@property
|
|
def cer_pem(self):
|
|
return self._cert.cer_pem
|
|
|
|
@property
|
|
def key_pem(self):
|
|
return self._cert.key_pem
|
|
|
|
|
|
class TestStamp(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
print(f'In method: {self._testMethodName}')
|
|
self.pac = PACFinkok()
|
|
|
|
def test_cfdi_stamp(self):
|
|
cfdi = TestCfdi().xml
|
|
result = self.pac.stamp(cfdi)
|
|
cfdi_uuid = result['uuid']
|
|
|
|
self.assertFalse(bool(self.pac.error))
|
|
self.assertTrue(bool(uuid.UUID(cfdi_uuid)))
|
|
|
|
def test_cfdi_cancel(self):
|
|
expected = '201'
|
|
cfdi = TestCfdi()
|
|
result = self.pac.stamp(cfdi.xml)
|
|
cfdi_xml = result['xml']
|
|
cfdi_uuid = result['uuid']
|
|
|
|
self.assertFalse(bool(self.pac.error))
|
|
self.assertTrue(bool(uuid.UUID(cfdi_uuid)))
|
|
|
|
time.sleep(3)
|
|
info = {
|
|
'cer': cfdi.cer_pem,
|
|
'key': cfdi.key_pem,
|
|
}
|
|
result = self.pac.cancel(cfdi_xml, info)
|
|
|
|
self.assertFalse(bool(self.pac.error))
|
|
|
|
tree = ET.fromstring(result['acuse'].encode())
|
|
NS = {'s': 'http://schemas.xmlsoap.org/soap/envelope/'}
|
|
path = 'string(//s:Body/*/*/*[1]/*[1])'
|
|
cancel_uuid = tree.xpath(path, namespaces=NS)
|
|
path = 'string(//s:Body/*/*/*[1]/*[2])'
|
|
status = tree.xpath(path, namespaces=NS)
|
|
|
|
self.assertEqual(cfdi_uuid, cancel_uuid)
|
|
self.assertEqual(status, expected)
|
|
return
|
|
|
|
|
|
def test_cfdi_cancel_xml(self):
|
|
expected = '201'
|
|
cfdi = TestCfdi()
|
|
result = self.pac.stamp(cfdi.xml)
|
|
cfdi_uuid = result['uuid']
|
|
|
|
self.assertFalse(bool(self.pac.error))
|
|
self.assertTrue(bool(uuid.UUID(cfdi_uuid)))
|
|
|
|
NS_CFDI = {
|
|
'cfdi': 'http://www.sat.gob.mx/cfd/3',
|
|
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
|
|
}
|
|
tree = ET.fromstring(result['xml'].encode())
|
|
rfc_emisor = tree.xpath(
|
|
'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)',
|
|
namespaces=NS_CFDI)
|
|
|
|
time.sleep(1)
|
|
data = {
|
|
'rfc': rfc_emisor,
|
|
'fecha': datetime.datetime.now().isoformat()[:19],
|
|
'uuid': cfdi_uuid,
|
|
}
|
|
template = TEMPLATE_CANCEL.format(**data)
|
|
sign_xml = cfdi.sign_xml(template)
|
|
|
|
time.sleep(60)
|
|
result = self.pac.cancel_xml(sign_xml)
|
|
|
|
self.assertFalse(bool(self.pac.error))
|
|
|
|
tree = ET.fromstring(result['acuse'].encode())
|
|
NS = {'s': 'http://schemas.xmlsoap.org/soap/envelope/'}
|
|
path = 'string(//s:Body/*/*/*[1]/*[1])'
|
|
cancel_uuid = tree.xpath(path, namespaces=NS)
|
|
path = 'string(//s:Body/*/*/*[1]/*[2])'
|
|
status = tree.xpath(path, namespaces=NS)
|
|
|
|
self.assertEqual(cfdi_uuid, cancel_uuid)
|
|
self.assertEqual(status, expected)
|
|
|
|
|
|
class TestClient(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
print(f'In method: {self._testMethodName}')
|
|
self.pac = PACFinkok()
|
|
self.RFC = 'MBS740115000'
|
|
|
|
def _get_random_date(self):
|
|
cd = datetime.date.today()
|
|
sd = datetime.date(1950, 1, 1)
|
|
days = random.randint(1, (cd-sd).days)
|
|
nd = sd + datetime.timedelta(days=days)
|
|
return nd.strftime('%y%m%d')
|
|
|
|
def _get_random_rfc(self):
|
|
i = ''.join(random.sample(string.ascii_lowercase, 4))
|
|
d = self._get_random_date()
|
|
h = uuid.uuid4().hex[:3]
|
|
rfc = f'{i}{d}{h}'.upper()
|
|
return rfc
|
|
|
|
def test_client_add(self):
|
|
rfc = self._get_random_rfc()
|
|
result = self.pac.client_add(rfc)
|
|
self.assertTrue(result)
|
|
return
|
|
|
|
def test_client_get_token(self):
|
|
expected = 60
|
|
rfc = self._get_random_rfc()
|
|
result = self.pac.client_add(rfc)
|
|
self.assertTrue(result)
|
|
|
|
result = self.pac.client_get_token(rfc, f'{rfc}@test.com')
|
|
self.assertEqual(len(result), expected)
|
|
return
|
|
|
|
def test_client_add_timbres(self):
|
|
result = self.pac.client_balance(rfc=self.RFC)
|
|
expected = result + 10
|
|
result = self.pac.client_add_timbres(self.RFC, 10)
|
|
self.assertEqual(result, expected)
|
|
return
|
|
|
|
def test_client_balance(self):
|
|
expected = 0
|
|
rfc = self._get_random_rfc()
|
|
result = self.pac.client_add(rfc)
|
|
self.assertTrue(result)
|
|
|
|
result = self.pac.client_balance(rfc=rfc)
|
|
self.assertEqual(result, expected)
|
|
return
|
|
|
|
def test_client_set_status(self):
|
|
expected = True
|
|
result = self.pac.client_set_status(self.RFC, False)
|
|
self.assertTrue(result)
|
|
|
|
result = self.pac.client_set_status(self.RFC, True)
|
|
self.assertTrue(result)
|
|
return
|
|
|
|
def test_client_switch(self):
|
|
expected = True
|
|
result = self.pac.client_switch(self.RFC, False)
|
|
self.assertTrue(result)
|
|
|
|
result = self.pac.client_switch(self.RFC, True)
|
|
self.assertTrue(result)
|
|
return
|
|
|
|
def test_client_report_folios(self):
|
|
expected = 0
|
|
date_from = '2021-01-01T00:00:00'
|
|
date_to = '2021-02-01T00:00:00'
|
|
result = self.pac.client_report_folios(self.RFC, date_from, date_to)
|
|
self.assertEqual(result, expected)
|
|
return
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|
|
|