""" MojEracun version of the proxy user model, set up as an AbstractModel as the data is held on res_company. The integration requires a lot of adjustments to work directly with an extrenal service provier and the original edi_proxy_user cannot be used as a basis as it is too closely tied to Odoo's own IAP server structure. """ import logging import requests from json import JSONDecodeError from odoo.exceptions import UserError _logger = logging.getLogger(__name__) TIMEOUT = 30 class MojEracunServiceError(Exception): def __init__(self, code, message=False): self.code = code self.message = message super().__init__(message or code) # ------------------------------------------------------------------------- # HELPER METHODS # ------------------------------------------------------------------------- def _get_server_url(company, edi_mode=None): edi_mode = edi_mode or company.l10n_hr_mer_connection_mode urls = { 'prod': 'https://www.moj-eracun.hr', 'test': 'https://demo.moj-eracun.hr', 'demo': '???', } return urls[edi_mode] def _make_request(company, endpoint_type, params=False): """ Returns: For multiple document endpoints (such as query*): list of dicts For single document endpoints (such as markPaid): dict For receive specifically: bytestring """ endpoints = { 'send': '/apis/v2/send', 'query_inbox': '/apis/v2/queryInbox', 'receive': '/apis/v2/receive', 'update_status': '/apis/v2/UpdateDokumentProcessStatus', 'query_status_inbox': '/apis/v2/queryDocumentProcessStatusInbox', 'query_status_outbox': '/apis/v2/queryDocumentProcessStatusOutbox', 'notify_import': False, # Includes eID and has to be handled separately 'mark_paid': '/api/fiscalization/markPaid', 'reject': '/api/fiscalization/reject', 'fisc_outbox': '/api/fiscalization/statusOutbox', 'fisc_inbox': '/api/fiscalization/statusInbox', } endpoint = f"/apis/v2/notifyimport/{params.pop('eid')}" if endpoint_type == 'notify_import' else endpoints.get(endpoint_type) if not endpoint: raise MojEracunServiceError('Invalid API endpoint') payload = params url = f"{_get_server_url(company)}{endpoint}" # Last barrier : in case the demo mode is not handled by the caller, we block access. if company.l10n_hr_mer_connection_mode == 'demo': raise MojEracunServiceError("block_demo_mode", "Can't access the proxy in demo mode") try: response = requests.post( url, json=payload, timeout=TIMEOUT, headers={'content-type': 'application/json', 'charset': 'utf-8'} ) except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, requests.exceptions.Timeout, requests.exceptions.HTTPError): raise MojEracunServiceError('connection_error', company.env._('The url that this service requested returned an error. The url it tried to contact was %s', url)) # Structure-specific error handling if response.status_code != 200: try: error_message = response.json() error_message = error_message.get('errors') or error_message.get('message') except (JSONDecodeError, TypeError): error_message = False raise UserError(company.env._("Error handling request: %s", error_message) if error_message else company.env._("HTTP %s: Connection error.", response.status_code)) if endpoint != endpoints['receive']: try: response_json = response.json() except (JSONDecodeError, TypeError): raise MojEracunServiceError('Invalid response format received') if 'error' in response_json: message = company.env._('The url that this service requested returned an error. The url it tried to contact was %(url)s. %(error_message)s', url=url, error_message=response_json['error']['message']) if response_json['error']['code'] == 404: message = company.env._('The url that this service tried to contact does not exist. The url was “%s”', url) raise MojEracunServiceError('connection_error', message) return response_json else: return response.content def _call_mer_service(company, endpoint, params=None): params_base = { 'Username': company.l10n_hr_mer_username, 'Password': company.l10n_hr_mer_password, 'CompanyId': company.l10n_hr_mer_company_ident, 'CompanyBu': company.partner_id.l10n_hr_business_unit_code, 'SoftwareId': company.l10n_hr_mer_software_ident, } if params: params = params_base | params else: params = params_base params_to_delete = [] for key, value in params.items(): if not value: params_to_delete.append(key) for key in params_to_delete: params.pop(key) try: response = _make_request( company, endpoint, params=params, ) except MojEracunServiceError as e: raise UserError(e) return response # ------------------------------------------------------------------------- # MOJERACUN API CALL METHODS # ------------------------------------------------------------------------- def _mer_api_send(company, xml_file): """ Send electronic document to a recipient. """ params = { 'File': xml_file, } response_dict = _call_mer_service(company, 'send', params=params) return response_dict def _mer_api_query_inbox(company, filter=None, electronic_id=None, status_id=None, date_from=None, date_to=None): """ Status description. Query methods are refered as basic MER document statuses. When receiving response from query methods, you will get croatian names of statuses (U obradi, Poslan, Preuzet, Povučeno preuzimanje, Neuspjelo). """ params = { 'Filter': filter, 'ElectronicId': electronic_id, 'StatusId': status_id, 'From': date_from, 'To': date_to, } response_list = _call_mer_service(company, 'query_inbox', params=params) return response_list def _mer_api_receive_document(company, electronic_id): """ Receive method is used for downloading documents. Both sent and incoming, eg. Inbox and Outbox documents. """ params = { 'ElectronicId': electronic_id, } response_bstr = _call_mer_service(company, 'receive', params=params) if response_bstr[:5] != b'