forked from Mapan/odoo17e
167 lines
6.2 KiB
Python
167 lines
6.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import base64
|
|
import logging
|
|
import ssl
|
|
from cryptography.hazmat.primitives import hashes
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
from datetime import datetime
|
|
from lxml import etree
|
|
from pytz import timezone
|
|
|
|
from odoo import _, api, fields, models, tools
|
|
from odoo.addons.account.tools.certificate import crypto_load_certificate
|
|
from odoo.exceptions import ValidationError, UserError
|
|
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from OpenSSL import crypto
|
|
except ImportError:
|
|
_logger.warning('OpenSSL library not found. If you plan to use l10n_mx_edi, please install the library from https://pypi.python.org/pypi/pyOpenSSL')
|
|
|
|
|
|
class Certificate(models.Model):
|
|
_name = 'l10n_mx_edi.certificate'
|
|
_description = 'SAT Digital Sail'
|
|
_order = "date_start desc, id desc"
|
|
|
|
content = fields.Binary(
|
|
string='Certificate',
|
|
help='Certificate in .cer format',
|
|
required=True,
|
|
attachment=False,
|
|
)
|
|
company_id = fields.Many2one(
|
|
comodel_name='res.company',
|
|
string="Company",
|
|
required=True,
|
|
default=lambda self: self.env.company,
|
|
ondelete='cascade',
|
|
)
|
|
key = fields.Binary(
|
|
string='Certificate Key',
|
|
help='Certificate Key in .key format',
|
|
required=True,
|
|
attachment=False,
|
|
)
|
|
password = fields.Char(
|
|
string='Certificate Password',
|
|
help='Password for the Certificate Key',
|
|
required=True,
|
|
)
|
|
serial_number = fields.Char(
|
|
string='Serial number',
|
|
help='The serial number to add to electronic documents',
|
|
readonly=True,
|
|
index=True,
|
|
)
|
|
date_start = fields.Datetime(
|
|
string='Available date',
|
|
help='The date on which the certificate starts to be valid',
|
|
readonly=True,
|
|
)
|
|
date_end = fields.Datetime(
|
|
string='Expiration date',
|
|
help='The date on which the certificate expires',
|
|
readonly=True,
|
|
)
|
|
|
|
@tools.ormcache('content')
|
|
def _get_pem_cer(self, content):
|
|
'''Get the current content in PEM format
|
|
'''
|
|
self.ensure_one()
|
|
return ssl.DER_cert_to_PEM_cert(base64.decodebytes(content)).encode('UTF-8')
|
|
|
|
@tools.ormcache('key', 'password')
|
|
def _get_pem_key(self, key, password):
|
|
'''Get the current key in PEM format
|
|
'''
|
|
self.ensure_one()
|
|
private_key = serialization.load_der_private_key(base64.b64decode(key), password.encode())
|
|
return private_key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.PKCS8,
|
|
encryption_algorithm=serialization.NoEncryption()
|
|
)
|
|
|
|
@api.model
|
|
def _get_timezone(self):
|
|
""" Get the timezone to consider for the CFDI. """
|
|
return timezone('America/Mexico_City')
|
|
|
|
def _get_data(self):
|
|
'''Return the content (b64 encoded) and the certificate decrypted
|
|
'''
|
|
self.ensure_one()
|
|
cer_pem = self._get_pem_cer(self.content)
|
|
certificate = crypto_load_certificate(cer_pem)
|
|
for to_del in ['\n', ssl.PEM_HEADER, ssl.PEM_FOOTER]:
|
|
cer_pem = cer_pem.replace(to_del.encode('UTF-8'), b'')
|
|
return cer_pem, certificate
|
|
|
|
def _get_valid_certificate(self):
|
|
'''Search for a valid certificate that is available and not expired. '''
|
|
tz = self._get_timezone()
|
|
now = datetime.now(tz)
|
|
for record in self:
|
|
date_start = tz.localize(record.date_start)
|
|
date_end = tz.localize(record.date_end)
|
|
if date_start <= now <= date_end:
|
|
return record
|
|
return None
|
|
|
|
def _get_encrypted_cadena(self, cadena):
|
|
'''Encrypt the cadena using the private key.
|
|
'''
|
|
self.ensure_one()
|
|
key_pem = self._get_pem_key(self.key, self.password)
|
|
private_key = serialization.load_pem_private_key(bytes(key_pem), password=None)
|
|
cadena_crypted = private_key.sign(bytes(cadena.encode()), padding.PKCS1v15(), hashes.SHA256())
|
|
return base64.b64encode(cadena_crypted)
|
|
|
|
@api.model
|
|
def _get_cadena_chain(self, xml_tree, xslt_path):
|
|
""" Use the provided XSLT document to generate a pipe-delimited string
|
|
:param xml_tree: the source lxml document
|
|
:param xslt_path: Path to the XSLT document
|
|
:return: string
|
|
"""
|
|
cadena_transformer = etree.parse(tools.file_open(xslt_path))
|
|
return str(etree.XSLT(cadena_transformer)(xml_tree))
|
|
|
|
@api.constrains('content', 'key', 'password')
|
|
def _check_credentials(self):
|
|
'''Check the validity of content/key/password and fill the fields
|
|
with the certificate values.
|
|
'''
|
|
tz = self._get_timezone()
|
|
now = datetime.now(tz)
|
|
date_format = '%Y%m%d%H%M%SZ'
|
|
for record in self:
|
|
# Try to decrypt the certificate
|
|
try:
|
|
certificate = record._get_data()[1]
|
|
before = tz.localize(datetime.strptime(certificate.get_notBefore().decode("utf-8"), date_format))
|
|
after = tz.localize(datetime.strptime(certificate.get_notAfter().decode("utf-8"), date_format))
|
|
serial_number = certificate.get_serial_number()
|
|
except UserError as exc_orm: # ;-)
|
|
raise exc_orm
|
|
except Exception as e:
|
|
raise ValidationError(_('The certificate content is invalid %s.', e))
|
|
# Assign extracted values from the certificate
|
|
record.serial_number = ('%x' % serial_number)[1::2]
|
|
record.date_start = before.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
record.date_end = after.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
if now > after:
|
|
raise ValidationError(_('The certificate is expired since %s', record.date_end))
|
|
# Check the pair key/password
|
|
try:
|
|
key_pem = self._get_pem_key(self.key, self.password)
|
|
crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
|
|
except Exception:
|
|
raise ValidationError(_('The certificate key and/or password is/are invalid.'))
|