Files
enterprise-addons/l10n_at_pos/models/fiskaly_client.py
T

212 lines
8.1 KiB
Python

import requests
import logging
import uuid
from odoo import _
from contextlib import contextmanager
from odoo.addons.iap import jsonrpc
from odoo.exceptions import ValidationError
from ...pos_enterprise.models.data_validator import object_of, list_of
_logger = logging.getLogger(__name__)
auth_schema = object_of({
'api_key': True,
'api_secret': True,
})
fon_schema = object_of({
'fon_participant_id': True,
'fon_user_id': True,
'fon_user_pin': True
})
scu_schema = object_of({
'legal_entity_id': object_of({
'vat_id': True,
})
})
sign_order_schema = object_of({
'schema': object_of({
'ekabs_v0': object_of({
'head': object_of({
'number': True,
'date': True,
'seller': object_of({
'name': True,
'tax_number': True,
'address': True,
}),
'buyer': object_of({
'name': True,
'address': True,
}),
}),
'data': object_of({
'currency': True,
'full_amount_incl_vat': True,
'payment_types': list_of(object_of({
'name': True,
'amount': True,
})),
'vat_amounts': list_of(object_of({
'vat_rate': True,
'percentage': True,
'incl_vat': True,
'excl_vat': True,
'vat': True,
})),
'lines': list_of(object_of({
'text': True,
'vat_amounts': list_of(object_of({
'percentage': True,
'incl_vat': True
})),
'item': object_of({
'number': True,
'quantity': True,
'price_per_unit': True
})
}))
}),
}),
}),
'receipt_type': True,
})
class FiskalyClient:
def __init__(self, company, api_key, api_secret, version=1):
self.base_url = f"https://rksv.fiskaly.com/api/v{version}/"
# Company is here just in case there is a need to refresh the token
self.company = company
self.api_key = api_key
self.api_secret = api_secret
@contextmanager
def _make_session(self, bearer_token):
session = requests.Session()
session.headers.update({
'Content-Type': 'application/json',
'Authorization': f'Bearer {bearer_token}'
})
retry = requests.adapters.HTTPAdapter(max_retries=requests.adapters.Retry(
total=3,
status_forcelist=range(500, 600), # Retry on all server errors per Fiskaly docs
))
session.mount('https://', retry)
def request(method, endpoint, data=None, params=None):
url = self.base_url + endpoint
try:
resp = session.request(method, url, json=data, params=params, timeout=5)
# Handle 401 Unauthorized specially - to refresh the token and retry
if resp.status_code == 401:
_logger.info("Token expired, attempting to re-authenticate.")
try:
updated_token = self.auth()
session.headers["Authorization"] = f"Bearer {updated_token}"
# Retry the request with new token
resp = session.request(method, url, json=data, params=params, timeout=5)
except ValidationError as e:
# If authentication fails, bubble up the error
_logger.error("Failed to refresh token: %s", str(e))
raise
resp.raise_for_status()
_logger.info('Successfully responded %s %s', method, url)
return resp.json()
except requests.exceptions.RequestException as error:
raise ValidationError(_("Fiskaly API request failed, %s", error))
with session:
yield request
def auth(self):
"""Authenticate with Fiskaly and return a bearer token."""
data = {"api_key": self.api_key, "api_secret": self.api_secret}
try:
url = self.base_url + "auth"
resp = requests.post(url, json=data, timeout=5)
resp.raise_for_status()
_logger.info('Successfully authenticated with Fiskaly')
access_token = resp.json().get('access_token')
# Update the company record with the new token
if self.company:
self.company.sudo().l10n_at_fiskaly_access_token = access_token
return access_token
except requests.exceptions.RequestException as error:
raise ValidationError(_("Fiskaly authentication failed: %s", error))
def fon_auth(self, bearer_token, data):
_is_valid, error = fon_schema(data)
if error:
raise ValidationError(_("Fiskaly schema validation error, %s", error))
with self._make_session(bearer_token) as req:
return req("PUT", endpoint="fon/auth", data=data)
def setup_scu(self, data, bearer_token):
_is_valid, error = scu_schema(data)
if error:
raise ValidationError(_("Fiskaly schema validation error, %s", error))
with self._make_session(bearer_token) as req:
# Reuse existing active SCU if limit is already reached
existing = req("GET", endpoint='signature-creation-unit', params={"states[]": ["INITIALIZED", "OUTAGE"]})
if existing.get('data'):
return existing['data'][0]['_id']
scuid = str(uuid.uuid4())
req("PUT", endpoint=f'signature-creation-unit/{scuid}', data=data)
req("PATCH", endpoint=f'signature-creation-unit/{scuid}', data={"state": "INITIALIZED"})
return scuid
def setup_cash_reg(self, bearer_token):
reg_id = str(uuid.uuid4())
with self._make_session(bearer_token) as req:
req("PUT", endpoint=f'cash-register/{reg_id}', data={})
req("PATCH", endpoint=f'cash-register/{reg_id}', data={"state": "REGISTERED"})
req("PATCH", endpoint=f'cash-register/{reg_id}', data={"state": "INITIALIZED"})
return reg_id
def sign_order(self, reg_id, bearer_token, receipt_id, data):
_is_valid, error = sign_order_schema(data)
if error:
raise ValidationError(_("Fiskaly schema validation error, %s", error))
endpoint = f"cash-register/{reg_id}/receipt/{receipt_id}"
with self._make_session(bearer_token) as req:
return req("PUT", endpoint, data=data)
def get_dep_data(self, bearer_token, cash_regid, start_time, end_time):
endpoint = f"cash-register/{cash_regid}/export"
params = {"start_time_signature": start_time, "end_time_signature": end_time} if end_time - start_time > 0 else None
with self._make_session(bearer_token) as req:
return req("GET", endpoint, params=params)
def get_closing_receipt(self, reg_id, receipt_type, offset, bearer_token):
endpoint = f"cash-register/{reg_id}/receipt"
params = {
"receipt_types[]": receipt_type,
"offset": offset,
"order": "DESC",
"limit": 1
}
with self._make_session(bearer_token) as req:
return req("GET", endpoint, params=params)
L10N_AT_FISKALY_URL_LIVE = "https://l10n-at-pos.api.odoo.com/api/l10n_at_pos/"
L10N_AT_FISKALY_URL_TEST = "https://l10n-at-pos.test.odoo.com/api/l10n_at_pos/"
IAP_VERSION = 1
iap_schema = object_of({
'db_uuid': True,
'company_id': True
})
def fiskaly_iap_rpc(record, endpoint, params):
_is_valid, error = iap_schema(params)
if error:
raise ValidationError(_("Fiskaly schema validation error, %s", error))
iap_url = L10N_AT_FISKALY_URL_LIVE if not record.l10n_at_pos_test_mode else L10N_AT_FISKALY_URL_TEST
base_url = iap_url + str(IAP_VERSION)
return jsonrpc(base_url + endpoint, params=params)