#!/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 . import logging import lxml.etree as ET import requests from requests.exceptions import ConnectionError from .conf import DEBUG, DEBUG_SOAP, AUTH logging.getLogger('requests').setLevel(logging.ERROR) TIMEOUT = 10 class PACComercioDigital(object): ws = 'https://{}.comercio-digital.mx/{}' URL = { 'timbra': ws.format('ws', 'timbre/timbrarV5.aspx'), 'cancel': ws.format('cancela', 'cancela3/cancelarUuid'), 'cancelxml': ws.format('cancela', 'cancela3/cancelarXml'), } if DEBUG: ws = 'https://pruebas.comercio-digital.mx/{}' URL = { 'timbra': ws.format('timbre/timbrarV5.aspx'), 'cancel': ws.format('cancela3/cancelarUuid'), 'cancelxml': ws.format('cancela3/cancelarXml'), } def __init__(self): self.error = '' def stamp(self, cfdi): auth = AUTH host = self.URL['timbra'].split('/')[2] headers = { 'Content-type': 'text/plain', 'usrws': auth['user'], 'pwdws': auth['pass'], 'tipo': 'XML', 'Host': host, 'Expect' : '100-continue', 'Connection' : 'Keep-Alive', } result = requests.post(self.URL['timbra'], data=cfdi, headers=headers, timeout=TIMEOUT) if result.status_code != 200: return '' if 'errmsg' in result.headers: self.error = result.headers['errmsg'] print(self.error) return '' return result.text def _get_data_cancel(self, cfdi, info): NS_CFDI = { 'cfdi': 'http://www.sat.gob.mx/cfd/3', 'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital', } tree = ET.fromstring(cfdi) tipo = tree.xpath( 'string(//cfdi:Comprobante/@TipoDeComprobante)', namespaces=NS_CFDI) total = tree.xpath( 'string(//cfdi:Comprobante/@Total)', namespaces=NS_CFDI) 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) uid = tree.xpath( 'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)', namespaces=NS_CFDI) data = ( f"USER={AUTH['user']}", f"PWDW={AUTH['pass']}", f"RFCE={rfc_emisor}", f"UUID={uid}", f"PWDK={info['pass']}", f"KEYF={info['key']}", f"CERT={info['cer']}", f"TIPO={info['tipo']}", f"ACUS=SI", f"RFCR={rfc_receptor}", f"TIPOC={tipo}", f"TOTAL={total}", ) return '\n'.join(data) def cancel(self, cfdi, info): url = self.URL['cancel'] host = url.split('/')[2] headers = { 'Content-type': 'text/plain', 'Host': host, 'Expect' : '100-continue', 'Connection' : 'Keep-Alive', } data = self._get_data_cancel(cfdi, info) result = requests.post(url, data=data, headers=headers, timeout=TIMEOUT) if result.status_code != 200: return '' if result.headers['codigo'] != '000': self.error = result.headers['errmsg'] print(self.error) return '' return result.content def _get_headers_cancel_xml(self, cfdi, info): auth = AUTH host = self.URL['cancelxml'].split('/')[2] NS_CFDI = { 'cfdi': 'http://www.sat.gob.mx/cfd/3', 'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital', } tree = ET.fromstring(cfdi) tipo = tree.xpath( 'string(//cfdi:Comprobante/@TipoDeComprobante)', namespaces=NS_CFDI) total = tree.xpath( 'string(//cfdi:Comprobante/@Total)', namespaces=NS_CFDI) rfc_receptor = tree.xpath( 'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)', namespaces=NS_CFDI) headers = { 'usrws': auth['user'], 'pwdws': auth['pass'], 'rfcr': rfc_receptor, 'total': total, 'tipocfdi': tipo, 'Content-type': 'text/plain', 'Host': host, 'Expect' : '100-continue', 'Connection' : 'Keep-Alive', } headers.update(info) return headers def cancel_xml(self, cfdi, xml, info): url = self.URL['cancelxml'] headers = self._get_headers_cancel_xml(cfdi, info) result = requests.post(url, data=xml, headers=headers, timeout=TIMEOUT) if result.status_code != 200: return '' if result.headers['codigo'] != '000': self.error = result.headers['errmsg'] print(self.error) return '' return result.content