cfdi-pac/source/tests/tests_finkok.py

220 lines
6.8 KiB
Python

#!/usr/bin/env python3
import base64
import datetime
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(5)
result = self.pac.cancel_xml(sign_xml)
self.assertFalse(bool(self.pac.error))
tree = ET.fromstring(result)
uid = tree.xpath('string(//Acuse/Folios/UUID)')
status = tree.xpath('string(//Acuse/Folios/EstatusUUID)')
self.assertEqual(cfdi_uuid, uid)
self.assertEqual(status, expected)
if __name__ == '__main__':
unittest.main()