1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_ar_edi/models/l10n_ar_afipws_connection.py
2024-12-10 09:04:09 +07:00

213 lines
10 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import UserError
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.serialization import Encoding, load_pem_private_key
from cryptography.hazmat.primitives.serialization.pkcs7 import (
PKCS7Options,
PKCS7SignatureBuilder,
)
from cryptography.x509 import load_pem_x509_certificate
from lxml import builder
from lxml import etree
from requests.adapters import HTTPAdapter
from requests.exceptions import HTTPError
from urllib3.util.ssl_ import create_urllib3_context
import time
import datetime
import base64
import logging
from odoo.tools.zeep import Client, Transport
_logger = logging.getLogger(__name__)
# Exclude some ciphers in order avoid failure on servers where the DH key is too small
AFIP_CIPHERS = "DEFAULT:!DH"
class L10nArHTTPAdapter(HTTPAdapter):
""" An adapter to block DH ciphers which may not work for *.afip.gov.ar """
def init_poolmanager(self, *args, **kwargs):
context = create_urllib3_context(ciphers=AFIP_CIPHERS)
kwargs["ssl_context"] = context
return super().init_poolmanager(*args, **kwargs)
class ARTransport(Transport):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.session.mount('https://', L10nArHTTPAdapter()) # block DH ciphers for AFIP servers
def post(self, address, message, headers):
""" We overwrite this method only to be able to save the xml request and response.
This will only affect to the connections that are made n this field and it do not extend the original
Transport class of zeep package.
NOTE: we try using the HistoryPlugin to save the xml request/reponse but seems this one could have problems when using with multi thread/workers"""
response = super().post(address, message, headers)
self.xml_request = etree.tostring(
etree.fromstring(message), pretty_print=True).decode('utf-8')
self.xml_response = etree.tostring(
etree.fromstring(response.content), pretty_print=True).decode('utf-8')
return response
class SimpleTransport:
def __init__(self, transport):
self.__obj = transport
@property
def xml_request(self):
return self.__obj.xml_request
@property
def xml_response(self):
return self.__obj.xml_response
class L10nArAfipwsConnection(models.Model):
_name = "l10n_ar.afipws.connection"
_description = "AFIP Webservice Connection"
_rec_name = "l10n_ar_afip_ws"
_order = "expiration_time desc"
company_id = fields.Many2one('res.company', required=True, index=True, auto_join=True)
uniqueid = fields.Char('Unique ID', readonly=True)
token = fields.Text(readonly=True)
sign = fields.Text(readonly=True)
generation_time = fields.Datetime(readonly=True)
expiration_time = fields.Datetime(readonly=True)
type = fields.Selection(
[('production', 'Production'), ('testing', 'Testing')], readonly=True,
required=True, help='This field is not configure by the user, is extracted from the environment configured in'
' the company when the connection was created. It\'s needed because if you change from environment to do quick'
' tests we could avoid using the last connection and use the one that matches with the new environment')
l10n_ar_afip_ws = fields.Selection(selection='_get_l10n_ar_afip_ws', string='AFIP WS', required=True)
def _get_l10n_ar_afip_ws(self):
""" Return the list of values of the selection field. """
return [('wscdc', _('Verification of Invoices (WSCDC)'))] + self.env['account.journal']._get_l10n_ar_afip_ws()
@api.model
def _l10n_ar_get_afip_ws_url(self, afip_ws, environment_type):
""" Function to be inherited on each module that adds a new webservice """
ws_data = {'wsfe': {'production': "https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL",
'testing': "https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL"},
'wsfex': {'production': "https://servicios1.afip.gov.ar/wsfexv1/service.asmx?WSDL",
'testing': "https://wswhomo.afip.gov.ar/wsfexv1/service.asmx?WSDL"},
'wsbfe': {'production': "https://servicios1.afip.gov.ar/wsbfev1/service.asmx?WSDL",
'testing': "https://wswhomo.afip.gov.ar/wsbfev1/service.asmx?WSDL"},
'wscdc': {'production': "https://servicios1.afip.gov.ar/WSCDC/service.asmx?WSDL",
'testing': "https://wswhomo.afip.gov.ar/WSCDC/service.asmx?WSDL"}}
return ws_data.get(afip_ws, {}).get(environment_type)
def _get_client(self, return_transport=False):
""" Get zeep client to connect to the webservice """
wsdl = self._l10n_ar_get_afip_ws_url(self.l10n_ar_afip_ws, self.type)
auth = {'Token': self.token, 'Sign': self.sign, 'Cuit': self.company_id.partner_id.ensure_vat()}
try:
transport = ARTransport(operation_timeout=60, timeout=60)
client = Client(wsdl, transport=transport)
except Exception as error:
self._l10n_ar_process_connection_error(error, self.type, self.l10n_ar_afip_ws)
if return_transport:
return client, auth, SimpleTransport(transport)
return client, auth
@api.model
def _l10n_ar_process_connection_error(self, error, env_type, afip_ws):
""" Review the type of exception received and show a useful message """
if hasattr(error, 'name'):
error_name = error.name
elif hasattr(error, 'message'):
error_name = error.message
else:
error_name = repr(error)
error_msg = _('There was a problem with the connection to the %s webservice: %s', afip_ws, error_name)
# Find HINT for error message
certificate_expired = _('It seems like the certificate has expired. Please renew your AFIP certificate')
token_in_use = 'El CEE ya posee un TA valido para el acceso al WSN solicitado'
data = {
'Computador no autorizado a acceder al servicio': _(
'The certificate is not authorized (delegated) to work with this web service'),
"ns1:cms.sign.invalid: Firma inválida o algoritmo no soportado": certificate_expired,
"ns1:cms.cert.expired: Certificado expirado": certificate_expired,
'500 Server Error: Internal Server': _('Webservice is down'),
token_in_use: _(
'Are you invoicing from another computer or system? This error could happen when a access token'
' that is requested to AFIP has been requested multiple times and the last one requested is still valid.'
' You will need to wait 12 hours to generate a new token and be able to connect to AFIP'
'\n\n If not, then could be a overload of AFIP service, please wait some time and try again'),
'No se puede decodificar el BASE64': _('The certificate and private key do not match'),
}
hint_msg = next((value for item, value in data.items() if item in error_name), None)
if token_in_use in error_name and env_type == 'testing':
hint_msg = _(
'The testing certificate is been used for another person, you can wait 10 minutes and'
' try again or you can change the testing certificate. If this message persist you can:\n\n'
' 1) Configure another of the demo certificates pre loaded in demo data'
' (On Settings click the ⇒ "Set another demo certificate" button).\n'
' 2) Configure your own testing certificates')
if hint_msg:
error_msg += '\n\nPISTA: ' + hint_msg
else:
if isinstance(error, HTTPError) and error.response.status_code == 503:
error_msg += '\n\n' + _('The AFIP electronic billing webservice is not available. Wait a few minutes for it to reset and try to validate the action again.')
else:
error_msg += '\n\n' + _('Please report this error to your Odoo provider')
raise UserError(error_msg)
def _l10n_ar_get_token_data(self, company, afip_ws):
""" Call AFIP Authentication webservice to get token & sign data """
private_key, certificate = company.sudo()._get_key_and_certificate()
environment_type = company._get_environment_type()
generation_time = fields.Datetime.now()
expiration_time = fields.Datetime.add(generation_time, hours=12)
uniqueId = str(int(time.mktime(datetime.datetime.now().timetuple())))
request_xml = (builder.E.loginTicketRequest({
'version': '1.0'},
builder.E.header(builder.E.uniqueId(uniqueId),
builder.E.generationTime(generation_time.strftime('%Y-%m-%dT%H:%M:%S-00:00')),
builder.E.expirationTime(expiration_time.strftime('%Y-%m-%dT%H:%M:%S-00:00'))),
builder.E.service(afip_ws)))
request = etree.tostring(request_xml, pretty_print=True)
# sign request
pkey = load_pem_private_key(private_key, None)
signcert = load_pem_x509_certificate(certificate)
signed_request = (
PKCS7SignatureBuilder()
.set_data(request)
.add_signer(signcert, pkey, hashes.SHA256())
.sign(Encoding.DER, [PKCS7Options.Binary])
)
wsdl = {'production': "https://wsaa.afip.gov.ar/ws/services/LoginCms?WSDL",
'testing': "https://wsaahomo.afip.gov.ar/ws/services/LoginCms?WSDL"}.get(environment_type)
try:
_logger.info('Connect to AFIP to get token: %s %s %s' % (afip_ws, company.l10n_ar_afip_ws_crt_fname, company.name))
transport = ARTransport(operation_timeout=60, timeout=60)
client = Client(wsdl, transport=transport)
response = client.service.loginCms(base64.b64encode(signed_request).decode())
except Exception as error:
return self._l10n_ar_process_connection_error(error, environment_type, afip_ws)
response = etree.fromstring(response.encode('utf-8'))
return {'uniqueid': uniqueId,
'generation_time': generation_time,
'expiration_time': expiration_time,
'token': response.xpath('/loginTicketResponse/credentials/token')[0].text,
'sign': response.xpath('/loginTicketResponse/credentials/sign')[0].text}