Skip to content

Commit 1f4029b

Browse files
authored
Feature/retenciones rest (#23)
* add retention services * update version * add UT * add UT * add service readme
1 parent d160f44 commit 1f4029b

File tree

8 files changed

+224
-1
lines changed

8 files changed

+224
-1
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,7 +1917,53 @@ from Utils.response_version import ResponseVersion
19171917

19181918
----------------
19191919

1920+
## Timbrado Retenciones ##
19201921

1922+
<details>
1923+
<summary>
1924+
Timbrado Retenciones V3
1925+
</summary>
1926+
1927+
**TimbrarRetencionesV3**
1928+
Recibe el contenido de un **XML** de retenciones ya sellado en formato **String**. Si el comprobante y el token/autenticación son correctos, devuelve el complemento timbrado en un string, en caso contrario lanza un error.
1929+
1930+
Este método recibe los siguientes parámetros:
1931+
* Url Servicios SW
1932+
* Usuario y contraseña o Token
1933+
* Archivo en formato **String**
1934+
1935+
**Ejemplo de consumo de la librería para timbrar XML de retenciones en formato string utilizando token**
1936+
```python
1937+
from Stamp_Retentions.Stamp_Retentions import Stamp_Retentions
1938+
1939+
xml = open("retencion.xml", "r", encoding='utf-8').read()
1940+
stamp_ret = Stamp_Retentions("http://services.test.sw.com.mx", "T2lYQ0t4L0R....ReplaceForRealToken")
1941+
response = stamp_ret.stamp_retetions_v3(xml)
1942+
1943+
if response.get_status() == "error":
1944+
print(response.get_message())
1945+
print(response.get_messageDetail())
1946+
else:
1947+
print(response.get_data())
1948+
```
1949+
1950+
**Ejemplo de consumo de la librería para timbrar XML de retenciones usando usuario y contraseña**
1951+
```python
1952+
from Stamp_Retentions.Stamp_Retentions import Stamp_Retentions
1953+
1954+
xml = open("retencion.xml", "r", encoding='utf-8').read()
1955+
stamp_ret = Stamp_Retentions("http://services.test.sw.com.mx", None, "user", "password")
1956+
response = stamp_ret.stamp_retetions_v3(xml)
1957+
1958+
if response.get_status() == "error":
1959+
print(response.get_message())
1960+
print(response.get_messageDetail())
1961+
else:
1962+
print(response.get_data())
1963+
```
1964+
</details>
1965+
1966+
----------------
19211967
Para mayor referencia de un listado completo de los servicios favor de visitar el siguiente [link](http://developers.sw.com.mx/).
19221968

19231969
Si deseas contribuir a la librería o tienes dudas envianos un correo a **soporte@sw.com.mx**.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from Stamp_Retentions.Stamp_RetentionsRequest import Stamp_RetentionsRequest
2+
from Utils.Services import Services
3+
4+
class Stamp_Retentions(Services):
5+
def __init__(self, url, token, user = None, password = None):
6+
super(Stamp_Retentions, self).__init__(url, token, user, password)
7+
8+
def stamp_retetions_v3(self, xml):
9+
return Stamp_RetentionsRequest.stamp(self.get_url(), self.get_token(), xml, "/retencion/stamp/v3")
10+
11+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from Stamp_Retentions.Stamp_RetentionsResponse import Stamp_RetentionsResponse
2+
from Utils.requestHelper import RequestHelper
3+
4+
class Stamp_RetentionsRequest:
5+
@staticmethod
6+
def stamp(url, token, xml, path):
7+
endpoint = url + path
8+
response = RequestHelper.post_multipart_request(endpoint, token, xml)
9+
return Stamp_RetentionsResponse(response)
10+
11+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import json
2+
import traceback
3+
from Utils.response import Response
4+
5+
class Stamp_RetentionsResponse(Response):
6+
def __init__(self, response):
7+
try:
8+
self.status_code = response.status_code
9+
if(bool(response.text and response.text.strip())):
10+
self.response = json.loads(response.text.encode().decode('utf8'))
11+
if(self.status_code == 200):
12+
self.data = self.response["data"]
13+
self.status = self.response["status"]
14+
else:
15+
# Some error responses may not include status
16+
self.status = self.response.get("status", "error")
17+
self.message = self.response.get("message")
18+
if "messageDetail" in self.response:
19+
self.messageDetail = self.response["messageDetail"]
20+
else:
21+
self.status = "error"
22+
self.message = response.reason
23+
self.messageDetail = response.request
24+
except:
25+
traceback.print_exc()
26+
27+

Stamp_Retentions/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""
2+
Stamp_Retentions module for Retentions stamping service
3+
"""
4+
5+

Test/resources/retenciones20.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<retenciones:Retenciones xmlns:retenciones="http://www.sat.gob.mx/esquemas/retencionpago/2"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://www.sat.gob.mx/esquemas/retencionpago/2 http://www.sat.gob.mx/esquemas/retencionpago/2/retencionpagov2.xsd"
5+
Version="2.0" FolioInt="7e6b73c1055bd042070b" FechaExp="2025-08-28T10:00:00"
6+
LugarExpRetenc="45110" CveRetenc="01" NoCertificado="30001000000500003416"
7+
Certificado="MIIFsDCCA5igAwIBAgIUMzAwMDEwMDAwMDA1MDAwMDM0MTYwDQYJKoZIhvcNAQELBQAwggErMQ8wDQYDVQQDDAZBQyBVQVQxLjAsBgNVBAoMJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGjAYBgNVBAsMEVNBVC1JRVMgQXV0aG9yaXR5MSgwJgYJKoZIhvcNAQkBFhlvc2Nhci5tYXJ0aW5lekBzYXQuZ29iLm14MR0wGwYDVQQJDBQzcmEgY2VycmFkYSBkZSBjYWxpejEOMAwGA1UEEQwFMDYzNzAxCzAJBgNVBAYTAk1YMRkwFwYDVQQIDBBDSVVEQUQgREUgTUVYSUNPMREwDwYDVQQHDAhDT1lPQUNBTjERMA8GA1UELRMIMi41LjQuNDUxJTAjBgkqhkiG9w0BCQITFnJlc3BvbnNhYmxlOiBBQ0RNQS1TQVQwHhcNMjMwNTE4MTE0MzUxWhcNMjcwNTE4MTE0MzUxWjCB1zEnMCUGA1UEAxMeRVNDVUVMQSBLRU1QRVIgVVJHQVRFIFNBIERFIENWMScwJQYDVQQpEx5FU0NVRUxBIEtFTVBFUiBVUkdBVEUgU0EgREUgQ1YxJzAlBgNVBAoTHkVTQ1VFTEEgS0VNUEVSIFVSR0FURSBTQSBERSBDVjElMCMGA1UELRMcRUtVOTAwMzE3M0M5IC8gVkFEQTgwMDkyN0RKMzEeMBwGA1UEBRMVIC8gVkFEQTgwMDkyN0hTUlNSTDA1MRMwEQYDVQQLEwpTdWN1cnNhbCAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmecO6n2GS0zL025gbHGQVxznPDICoXzR2uUngz4DqxVUC/w9cE6FxSiXm2ap8Gcjg7wmcZfm85EBaxCx/0J2u5CqnhzIoGCdhBPuhWQnIh5TLgj/X6uNquwZkKChbNe9aeFirU/JbyN7Egia9oKH9KZUsodiM/pWAH00PCtoKJ9OBcSHMq8Rqa3KKoBcfkg1ZrgueffwRLws9yOcRWLb02sDOPzGIm/jEFicVYt2Hw1qdRE5xmTZ7AGG0UHs+unkGjpCVeJ+BEBn0JPLWVvDKHZAQMj6s5Bku35+d/MyATkpOPsGT/VTnsouxekDfikJD1f7A1ZpJbqDpkJnss3vQIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQsFAAOCAgEAFaUgj5PqgvJigNMgtrdXZnbPfVBbukAbW4OGnUhNrA7SRAAfv2BSGk16PI0nBOr7qF2mItmBnjgEwk+DTv8Zr7w5qp7vleC6dIsZFNJoa6ZndrE/f7KO1CYruLXr5gwEkIyGfJ9NwyIagvHHMszzyHiSZIA850fWtbqtythpAliJ2jF35M5pNS+YTkRB+T6L/c6m00ymN3q9lT1rB03YywxrLreRSFZOSrbwWfg34EJbHfbFXpCSVYdJRfiVdvHnewN0r5fUlPtR9stQHyuqewzdkyb5jTTw02D2cUfL57vlPStBj7SEi3uOWvLrsiDnnCIxRMYJ2UA2ktDKHk+zWnsDmaeleSzonv2CHW42yXYPCvWi88oE1DJNYLNkIjua7MxAnkNZbScNw01A6zbLsZ3y8G6eEYnxSTRfwjd8EP4kdiHNJftm7Z4iRU7HOVh79/lRWB+gd171s3d/mI9kte3MRy6V8MMEMCAnMboGpaooYwgAmwclI2XZCczNWXfhaWe0ZS5PmytD/GDpXzkX0oEgY9K/uYo5V77NdZbGAjmyi8cE2B2ogvyaN2XfIInrZPgEffJ4AB7kFA2mwesdLOCh0BLD9itmCve3A1FGR4+stO2ANUoiI3w3Tv2yQSg4bjeDlJ08lXaaFCLW2peEXMXjQUk7fmpb5MNuOUTW6BE="
8+
Sello="gTgvZnDTJlMEWc6nGptIOj44Th+w+JbhKuswAzQVcZA+xaoWJeCvZ7D49ZQKBdIMjwz0ory/a8NDM4A5ZyE+FqNYD4JITfnA92J8R0mw0DD6yNzTDE+81mvW8/UzAshryPTnllSOWVt5+zJSu8+AhlHA3vli22Afjb0KPfkklbpuaHWnS+B2g+kfcfXqxGNXzkx/NvQP1BmGyqsdHLLwabqLSD16/foNfvm+x1znKqpOK09sOE9ysEengaG3aRMiVYZiQynEwLuXdHDd6o1NsSX7liBYROMEz3xPDMmMkCgsY9vzIdkJIn1Nq/aDFta2pcuL/2l2zw2wHaEKDp8g+w==">
9+
<retenciones:Emisor RfcE="EKU9003173C9" NomDenRazSocE="ESCUELA KEMPER URGATE"
10+
RegimenFiscalE="601" />
11+
<retenciones:Receptor NacionalidadR="Nacional">
12+
<retenciones:Nacional RfcR="URE180429TM6" NomDenRazSocR="UNIVERSIDAD ROBOTICA ESPAÑOLA"
13+
DomicilioFiscalR="86991" />
14+
</retenciones:Receptor>
15+
<retenciones:Periodo MesIni="01" MesFin="03" Ejercicio="2023" />
16+
<retenciones:Totales MontoTotOperacion="2000.00" MontoTotGrav="2000.00" MontoTotExent="0"
17+
MontoTotRet="580.00">
18+
<retenciones:ImpRetenidos BaseRet="2000" ImpuestoRet="001" MontoRet="580.00"
19+
TipoPagoRet="03" />
20+
</retenciones:Totales>
21+
</retenciones:Retenciones>

Test/test_stamp_retentions.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import unittest
2+
import os
3+
import sys
4+
5+
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
6+
sys.path.append(PROJECT_ROOT)
7+
8+
from Stamp_Retentions.Stamp_Retentions import Stamp_Retentions
9+
10+
11+
class TestStampRetentions(unittest.TestCase):
12+
expected = "success"
13+
message = "307. El comprobante contiene un timbre previo."
14+
messageAuth = "401 - El rango de la fecha de generación no debe de ser mayor a 72 horas para la emisión del timbre."
15+
16+
@staticmethod
17+
def open_file(pathFile):
18+
try:
19+
return open(pathFile, "r", encoding='utf-8').read()
20+
except UnicodeDecodeError:
21+
return open(pathFile, "r", encoding='latin_1', errors='ignore').read()
22+
23+
def testStampRetentions_xml(self):
24+
"""Prueba timbrado con XML usando token"""
25+
print("\n=== Prueba: Timbrado con XML (token) ===")
26+
27+
stamp = Stamp_Retentions("http://services.test.sw.com.mx", os.environ["SDKTEST_TOKEN"])
28+
xml_content = TestStampRetentions.open_file("Test/resources/retenciones20.xml")
29+
response = stamp.stamp_retetions_v3(xml_content)
30+
31+
TestStampRetentions.log_response("xml", response)
32+
if response.get_status() == "error":
33+
self.assertTrue(self.message == response.get_message() or self.messageAuth == response.get_message())
34+
else:
35+
self.assertTrue(self.expected == response.get_status())
36+
37+
def testStampRetentions_xml_Error(self):
38+
"""Prueba error timbrado con XML CFDI"""
39+
print("\n=== Prueba: Timbrado con XML (token) ===")
40+
41+
stamp = Stamp_Retentions("http://services.test.sw.com.mx", os.environ["SDKTEST_TOKEN"])
42+
xml_content = TestStampRetentions.open_file("Test/resources/xml40.xml")
43+
response = stamp.stamp_retetions_v3(xml_content)
44+
45+
TestStampRetentions.log_response("xml", response)
46+
self.assertTrue(self.expected != response.get_status())
47+
48+
def testStampRetentions_auth(self):
49+
"""Prueba timbrado con autenticación de cuenta"""
50+
print("\n=== Prueba: Timbrado con auth ===")
51+
52+
stamp = Stamp_Retentions(
53+
"http://services.test.sw.com.mx",
54+
None,
55+
os.environ["SDKTEST_USER"],
56+
os.environ["SDKTEST_PASSWORD"]
57+
)
58+
xml_content = TestStampRetentions.open_file("Test/resources/retenciones20.xml")
59+
response = stamp.stamp_retetions_v3(xml_content)
60+
61+
TestStampRetentions.log_response("auth", response)
62+
if response.get_status() == "error":
63+
self.assertTrue(self.message == response.get_message() or self.messageAuth == response.get_message())
64+
else:
65+
self.assertTrue(self.expected == response.get_status())
66+
67+
def testStampRetentions_authError(self):
68+
"""Prueba error timbrado con autenticación de cuenta"""
69+
print("\n=== Prueba: Timbrado con auth ===")
70+
71+
stamp = Stamp_Retentions(
72+
"http://services.test.sw.com.mx",
73+
None,
74+
"wrongUser",
75+
os.environ["SDKTEST_PASSWORD"]
76+
)
77+
xml_content = TestStampRetentions.open_file("Test/resources/retenciones20.xml")
78+
response = stamp.stamp_retetions_v3(xml_content)
79+
TestStampRetentions.log_response("auth", response)
80+
self.assertTrue(self.expected != response.get_status())
81+
82+
@staticmethod
83+
def log_response(label, response):
84+
print(f"[{label}] status:", response.get_status())
85+
if response.get_status() == "error":
86+
print("message:", response.get_message())
87+
print("messageDetail:", response.get_messageDetail())
88+
else:
89+
data = response.get_data()
90+
if isinstance(data, dict) and "retencion" in data:
91+
xml = data["retencion"]
92+
print("retencion length:", len(xml))
93+
print("retencion head:", xml[:200])
94+
else:
95+
print("data:", data)
96+
97+
98+
if __name__ == "__main__":
99+
suite = unittest.TestLoader().loadTestsFromTestCase(TestStampRetentions)
100+
unittest.TextTestRunner(verbosity=2).run(suite)
101+
102+

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import setuptools
22
setuptools.setup(
33
name='sw-sdk-python',
4-
version='0.0.5.1',
4+
version='0.0.6.1',
55
description="SDK para Timbrado en SmarterWeb",
66
url="https://github.com/lunasoft/sw-sdk-python",
77
packages=setuptools.find_packages(),

0 commit comments

Comments
 (0)