forked from Mapan/odoo17e
221 lines
11 KiB
Python
221 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import datetime, timedelta
|
|
import pytz
|
|
import ipaddress
|
|
import json
|
|
import requests
|
|
import socket
|
|
import hmac
|
|
from hashlib import sha256
|
|
from werkzeug import urls
|
|
|
|
from odoo import api, models, _
|
|
from odoo.http import request
|
|
import logging
|
|
from odoo.exceptions import UserError
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
SANDBOX_API_URL = 'https://test-api.service.hmrc.gov.uk'
|
|
PRODUCTION_API_URL = 'https://api.service.hmrc.gov.uk'
|
|
TIMEOUT = 10
|
|
|
|
|
|
class HmrcService(models.AbstractModel):
|
|
"""
|
|
Service in order to pass through our authentication proxy
|
|
"""
|
|
_name = 'hmrc.service'
|
|
_description = 'HMRC service'
|
|
|
|
@api.model
|
|
def _login(self):
|
|
"""
|
|
Checks if there is a userlogin (proxy) or a refresh of the tokens needed and ask for new ones
|
|
If needed, it returns the url action to log in with HMRC
|
|
Raise when something unexpected happens (enterprise contract not valid e.g.)
|
|
:return: False when no login through hmrc needed by the user, otherwise the url action
|
|
"""
|
|
user = self.env.user
|
|
login_needed = False
|
|
if user.l10n_uk_user_token:
|
|
if not user.l10n_uk_hmrc_vat_token or user.l10n_uk_hmrc_vat_token_expiration_time < datetime.now() + timedelta(minutes=1):
|
|
try:
|
|
url = self._get_proxy_server() + '/onlinesync/l10n_uk/get_tokens'
|
|
dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
|
|
data = json.dumps({'params': {'user_token': self.env.user.l10n_uk_user_token, 'dbuuid': dbuuid}})
|
|
resp = requests.request('GET', url, data=data,
|
|
headers={'content-type': "application/json"}, timeout=TIMEOUT) #json-rpc
|
|
resp.raise_for_status()
|
|
response = resp.json()
|
|
response = response.get('result', {})
|
|
self._write_tokens(response)
|
|
except:
|
|
# If it is a connection error, don't delete credentials and re-raise
|
|
raise
|
|
else: #In case no error was thrown, but an error is indicated
|
|
if response.get('error'):
|
|
self._clean_tokens()
|
|
self._cr.commit() # Even with the raise, we want to commit the cleaning of the tokens in the db
|
|
raise UserError(_(
|
|
'There was a problem refreshing the tokens. Please log in again. %(error)s',
|
|
error=response.get('message'),
|
|
))
|
|
else:
|
|
# Let the user know they established the connection and can submit the report.
|
|
self.env['bus.bus']._sendone(self.env.user.partner_id, 'simple_notification', {
|
|
'type': 'success',
|
|
'message': _("You are successfully connected to HMRC. You can now send the VAT report."),
|
|
})
|
|
else:
|
|
# if no user_token, ask for one
|
|
url = self._get_proxy_server() + '/onlinesync/l10n_uk/get_user'
|
|
dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
|
|
data = json.dumps({'params': {'dbuuid': dbuuid}})
|
|
resp = requests.request('POST', url, data=data, headers={'content-type': 'application/json', 'Accept': 'text/plain'})
|
|
resp.raise_for_status()
|
|
contents = resp.json()
|
|
contents = contents.get('result')
|
|
if contents.get('error'):
|
|
raise UserError(contents.get('message'))
|
|
user.sudo().write({'l10n_uk_user_token': contents.get('user_token')})
|
|
login_needed = True
|
|
|
|
if login_needed:
|
|
url = self.env['hmrc.service']._get_oauth_url(user.l10n_uk_user_token)
|
|
return {
|
|
'type': 'ir.actions.act_url',
|
|
'url': url,
|
|
'target': 'self',
|
|
}
|
|
return False
|
|
|
|
@api.model
|
|
def _get_fraud_prevention_info(self, client_data=None):
|
|
"""
|
|
https://developer.service.hmrc.gov.uk/api-documentation/docs/fraud-prevention
|
|
"""
|
|
gov_dict = {}
|
|
try:
|
|
environ = request.httprequest.environ
|
|
headers = request.httprequest.headers
|
|
remote_address = request.httprequest.remote_addr
|
|
remote_needed = not ipaddress.ip_address(remote_address).is_private
|
|
hostname = request.httprequest.host.split(":")[0]
|
|
server_public_ip = socket.gethostbyname(hostname)
|
|
public_ip_needed = not ipaddress.ip_address(server_public_ip).is_private
|
|
tz = self.env.context.get('tz')
|
|
if tz:
|
|
tz_hour = datetime.now(pytz.timezone(tz)).strftime('%z')
|
|
utc_offset = 'UTC' + tz_hour[:3] + ':' + tz_hour[-2:]
|
|
else:
|
|
utc_offset = 'UTC+00:00'
|
|
|
|
ICP = self.env['ir.config_parameter'].sudo()
|
|
enterprise_code = ICP.get_param('database.enterprise_code')
|
|
db_secret = ICP.get_param('database.secret')
|
|
if enterprise_code:
|
|
hashed_license = hmac.new(enterprise_code.encode(),
|
|
db_secret.encode(),
|
|
sha256).hexdigest()
|
|
else:
|
|
hashed_license = ''
|
|
gov_vendor_version = self.sudo().env.ref('base.module_base').latest_version
|
|
|
|
gov_dict['Gov-Client-Connection-Method'] = 'WEB_APP_VIA_SERVER'
|
|
if remote_needed: #no need when on a private network
|
|
gov_dict['Gov-Client-Public-IP'] = urls.url_quote(remote_address)
|
|
gov_dict['Gov-Client-Public-Port'] = urls.url_quote(str(environ.get('REMOTE_PORT')))
|
|
if 'totp_enabled' in self.env.user._fields and self.env.user.totp_enabled:
|
|
# We can not percent encode the separator, so we have to split the string as such to percent encode each key and value
|
|
gov_dict['Gov-Client-Multi-Factor'] = "{}={type}&{}={time}&{}={unique}".format(
|
|
urls.url_quote('type'),
|
|
urls.url_quote('timestamp'),
|
|
urls.url_quote('unique-reference'),
|
|
type=urls.url_quote('TOTP'),
|
|
time=urls.url_quote(datetime.utcnow().isoformat(timespec='milliseconds')+'Z', unsafe=':'), # We need to specify to percent encode ':'
|
|
unique=urls.url_quote(self.env.user._l10n_uk_hmrc_unique_reference()))
|
|
gov_dict['Gov-Client-Timezone'] = utc_offset
|
|
gov_dict['Gov-Client-Browser-JS-User-Agent'] = (environ.get('HTTP_USER_AGENT'))
|
|
gov_dict['Gov-Vendor-Version'] = '&'.join([urls.url_quote("Odoo") + "=" + urls.url_quote(gov_vendor_version)]*2) # We can not percent encode the separator and we need to do it for the key and the value. Client and Server sides are the same
|
|
gov_dict['Gov-Client-User-IDs'] = urls.url_quote("Odoo") + "=" + urls.url_quote(self.env.user.name) # We can not percent encode the separator, same as Gov-Vendor-Version
|
|
gov_dict['Gov-Client-Browser-Do-Not-Track'] = 'true' if headers.get('DNT') == '1' else 'false'
|
|
gov_dict['Gov-Client-Screens'] = f"width={client_data['screen_width']}&height={client_data['screen_height']}&scaling-factor={client_data['screen_scaling_factor']}&colour-depth={client_data['screen_color_depth']}" if client_data else ''
|
|
gov_dict['Gov-Client-Window-Size'] = f"width={client_data['window_width']}&height={client_data['window_height']}" if client_data else ''
|
|
gov_dict['Gov-Client-Public-Ip-Timestamp'] = datetime.utcnow().isoformat(timespec='milliseconds')+'Z'
|
|
gov_dict['Gov-Vendor-Product-Name'] = urls.url_quote("Odoo")
|
|
gov_dict['Gov-Client-Device-Id'] = client_data['hmrc_gov_client_device_id'] if client_data else ''
|
|
gov_dict['Gov-Vendor-Forwarded'] = f"by={server_public_ip}&for={remote_address}"
|
|
if public_ip_needed: # No need when on a private network
|
|
gov_dict['Gov-Vendor-Public-IP'] = server_public_ip
|
|
if hashed_license:
|
|
gov_dict['Gov-Vendor-License-IDs'] = urls.url_quote("Odoo") + "=" + urls.url_quote(hashed_license)
|
|
except Exception:
|
|
_logger.warning("Could not construct fraud prevention headers", exc_info=True)
|
|
return gov_dict
|
|
|
|
@api.model
|
|
def _write_tokens(self, tokens):
|
|
vals = {}
|
|
vals['l10n_uk_hmrc_vat_token_expiration_time'] = tokens.get('expiration_time')
|
|
vals['l10n_uk_hmrc_vat_token'] = tokens.get('access_token')
|
|
self.env.user.sudo().write(vals)
|
|
|
|
@api.model
|
|
def _clean_tokens(self):
|
|
vals = {}
|
|
vals['l10n_uk_user_token'] = ''
|
|
vals['l10n_uk_hmrc_vat_token_expiration_time'] = False
|
|
vals['l10n_uk_hmrc_vat_token'] = ''
|
|
self.env.user.sudo().write(vals)
|
|
|
|
def _get_local_hmrc_oauth_url(self):
|
|
""" The user will be redirected to this url after accepting (or not) permission grant.
|
|
"""
|
|
return self._get_proxy_server() + '/onlinesync/l10n_uk/hmrc'
|
|
|
|
@api.model
|
|
def _get_state(self, userlogin):
|
|
action = self.env.ref('account_reports.action_account_report_gt')
|
|
# Search your own host
|
|
url = request.httprequest.scheme + '://' + request.httprequest.host
|
|
return json.dumps({
|
|
'url': url,
|
|
'user': userlogin,
|
|
'action': action.id,
|
|
})
|
|
|
|
@api.model
|
|
def _get_oauth_url(self, login):
|
|
""" Generates the url to hmrc oauth endpoint.
|
|
"""
|
|
oauth_url = self._get_endpoint_url('/oauth/authorize')
|
|
url_params = {
|
|
'response_type': 'code',
|
|
'client_id': self._get_hmrc_client_id(),
|
|
'scope': 'read:vat write:vat',
|
|
'state': self._get_state(login),
|
|
'redirect_uri': self._get_local_hmrc_oauth_url(),
|
|
}
|
|
return oauth_url + '?' + urls.url_encode(url_params)
|
|
|
|
@api.model
|
|
def _get_endpoint_url(self, endpoint):
|
|
mode = self.env['ir.config_parameter'].sudo().get_param("l10n_uk_reports.hmrc_mode", 'production')
|
|
base_url = PRODUCTION_API_URL if mode == 'production' else SANDBOX_API_URL
|
|
return base_url + endpoint
|
|
|
|
@api.model
|
|
def _get_proxy_server(self):
|
|
mode = self.env['ir.config_parameter'].sudo().get_param("l10n_uk_reports.hmrc_mode", 'production')
|
|
proxy_server = 'https://l10n-uk-hmrc.api.odoo.com' if mode == 'production' else 'https://l10n-uk-hmrc.test.odoo.com'
|
|
return proxy_server
|
|
|
|
@api.model
|
|
def _get_hmrc_client_id(self):
|
|
mode = self.env['ir.config_parameter'].sudo().get_param("l10n_uk_reports.hmrc_mode", 'production')
|
|
hmrc_client_id = 'GqJgi8Hal1hsEwbG6rY6i9Ag1qUa' if mode == 'production' else 'dTdANDSeX4fiw63DicmUaAVQDSMa'
|
|
return hmrc_client_id
|