240 lines
7.6 KiB
Python
240 lines
7.6 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 comerciodigital import PACComercioDigital
|
|
|
|
|
|
NAME = 'comercio'
|
|
|
|
|
|
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
|
|
|
|
|
|
class TestStamp(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
print(f'In method: {self._testMethodName}')
|
|
self.pac = PACComercioDigital()
|
|
|
|
def test_cfdi_stamp(self):
|
|
cfdi = TestCfdi().xml
|
|
result = self.pac.stamp(cfdi)
|
|
cfdi_uuid = self.pac.cfdi_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_uuid = self.pac.cfdi_uuid
|
|
|
|
self.assertFalse(bool(self.pac.error))
|
|
self.assertTrue(bool(uuid.UUID(cfdi_uuid)))
|
|
|
|
time.sleep(1)
|
|
cert = cfdi.cert
|
|
info = {
|
|
'key': cert[0],
|
|
'cer': cert[1],
|
|
'pass': '12345678a',
|
|
'tipo': 'cfdi3.3',
|
|
}
|
|
result = self.pac.cancel(result, info)
|
|
self.assertFalse(bool(self.pac.error))
|
|
|
|
tree = ET.fromstring(result)
|
|
cancel_uuid = tree.xpath('string(//Acuse/Folios/UUID)')
|
|
status = tree.xpath('string(//Acuse/Folios/EstatusUUID)')
|
|
|
|
self.assertEqual(cfdi_uuid, cancel_uuid)
|
|
self.assertEqual(status, expected)
|
|
|
|
def test_cfdi_cancel_xml(self):
|
|
expected = '201'
|
|
cfdi = TestCfdi()
|
|
result = self.pac.stamp(cfdi.xml)
|
|
cfdi_uuid = self.pac.cfdi_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.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)
|
|
info = {
|
|
'tipo': 'cfdi3.3',
|
|
}
|
|
result = self.pac.cancel_xml(result, sign_xml, info)
|
|
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)
|
|
|
|
def test_cfdi_status(self):
|
|
expected = ''
|
|
cfdi = TestCfdi()
|
|
result = self.pac.stamp(cfdi.xml)
|
|
cfdi_uuid = self.pac.cfdi_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.encode())
|
|
rfc_emisor = tree.xpath(
|
|
'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)',
|
|
namespaces=NS_CFDI)
|
|
rfc_receptor = tree.xpath(
|
|
'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)',
|
|
namespaces=NS_CFDI)
|
|
total = tree.xpath(
|
|
'string(//cfdi:Comprobante/@Total)',
|
|
namespaces=NS_CFDI)
|
|
|
|
time.sleep(3)
|
|
data = {
|
|
'rfc_receptor': rfc_receptor,
|
|
'rfc_emisor': rfc_emisor,
|
|
'total': total,
|
|
'uuid': cfdi_uuid,
|
|
}
|
|
result = self.pac.status(data)
|
|
self.assertEqual(result, expected)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|