212 lines
8.1 KiB
Python
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)
|