ready to deployment

This commit is contained in:
2025-08-10 21:50:14 +02:00
commit 1efb04b057
37 changed files with 7379 additions and 0 deletions

4
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.venv
__pycache__
.env
test.http

175
backend/main.py Normal file
View File

@@ -0,0 +1,175 @@
from datetime import date, datetime, timedelta, timezone
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
import jwt
from jwt.exceptions import InvalidTokenError
from pydantic import BaseModel
from dotenv import load_dotenv
import os
import logging
import requests
import json
import numpy as np
import holidays
import calendar
from odoo_api.client import OdooAPIClient
# to get a string like this run:
# openssl rand -hex 32
load_dotenv()
SECRET_KEY = os.getenv('SECRET_KEY')
ALGORITHM = os.getenv('ALGORITHM')
ACCESS_TOKEN_EXPIRE_MINUTES = os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES')
ODDO_URL = os.getenv('ODDO_URL')
DB_NAME = os.getenv('DB_NAME')
origins_str = os.getenv('ORIGINS', '')
origins = [origin.strip() for origin in origins_str.split(',') if origin.strip()]
class Token(BaseModel):
access_token: str
token_type: str
class User(BaseModel):
uid: str
full_name: str | None = None
email: str | None = None
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
logger = logging.getLogger("uvicorn")
OdooClient = OdooAPIClient(ODDO_URL, DB_NAME, SECRET_KEY, ALGORITHM)
app = FastAPI()
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
logger.info(f"origins: {origins}")
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> User:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
uid = payload.get("uid")
email = payload.get("sub")
full_name = payload.get("full_name")
if uid is None or email is None:
raise credentials_exception
return User(uid=uid, email=email, full_name=full_name)
except InvalidTokenError:
raise credentials_exception
def odoo_api_login(username: str, password: str) -> User | None:
result = OdooClient.login(username, password)
if result.get('uid'):
return User(
uid=str(result.get('uid')),
full_name=result.get('name'),
email=result.get('username')
)
return None
def get_polish_holidays(year: int, month: int):
pl_holidays = holidays.Poland(years=year)
# Filtrowanie po miesiącu
holidays_list = [
{"date": date.strftime("%Y-%m-%d"), "name": name}
for date, name in sorted(pl_holidays.items())
if date.month == month
]
return holidays_list
def create_response(year: int, month: int, days: list) -> dict:
employee = days[0]["employee_id"][1]
days_in_month = calendar.monthrange(year, month)[1]
holidays = get_polish_holidays(year, month)
start = date(year, month, 1)
end = date(year, month, days_in_month) + timedelta(days=1)
# 4. Wyliczenie dni roboczych:
holiday_dates = [h['date'] for h in holidays]
working_days = int(np.busday_count(start.isoformat(), end.isoformat(), holidays=holiday_dates))
return {
"year": year,
"month": month,
"days_in_month": days_in_month,
"employee": employee,
"working_days": working_days,
"public_holidays": holidays,
"days": days
}
@app.post("/token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]) -> Token:
user = odoo_api_login(form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
logger.info(f"User '{user.uid}' logged in successfully.")
logger.info(
f"User '{user.uid}, {user.full_name}, {user.email}' logged in successfully.")
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email, "uid": user.uid, "full_name": user.full_name},
expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="bearer")
@app.get("/data/{year}/{month}")
async def read_own_items(
year: int,
month: int,
current_user: Annotated[User, Depends(get_current_user)],
):
logger.info(f"User '{current_user.full_name}' accessed data for {year}-{month}.")
data = OdooClient.get_hr_attendance_data(month=month, year=year)
if not data:
raise HTTPException(
status_code=404,
detail="No attendance data found for the specified month and year."
)
return create_response(year, month, data)

View File

128
backend/odoo_api/client.py Normal file
View File

@@ -0,0 +1,128 @@
import requests
import json
import logging
import calendar
from datetime import datetime, timedelta, timezone
from fastapi import FastAPI, Depends, HTTPException, status
logger = logging.getLogger("uvicorn")
class OdooAPIClient:
def __init__(self, odoo_url: str, db_name: str, secret_key: str, algorithm: str):
self.odoo_url = odoo_url
self.db_name = db_name
self.session = requests.Session()
def login(self, username: str, password: str) -> dict:
auth_url = f"{self.odoo_url}/web/session/authenticate"
payload = {
"jsonrpc": "2.0",
"method": "call",
"params": {
"db": self.db_name,
"login": username,
"password": password
},
"id": 1
}
headers = {'Content-Type': 'application/json'}
response = self.session.post(auth_url, json=payload, headers=headers)
if response.status_code != 200:
# logger.error(
# f"Authentication failed with status code {response.status_code}")
raise Exception(
f"Authentication failed with status code {response.status_code}")
# for cookie in response.cookies:
# self._session_cookies[cookie.name] = cookie.value
data = response.json()
if "error" in data:
raise HTTPException(
status_code=502,
detail=f"Odoo RPC error: {data['error'].get('message', 'Unknown error')}"
)
return data["result"]
def get_hr_attendance_data(self, month=None, year=None, limit=None, domain=None):
if month is None:
month = datetime.now(timezone.utc).month
if year is None:
year = datetime.now(timezone.utc).year
if domain is None:
domain = [] # Default to no domain filter
url = f"{self.odoo_url}/hr.attendance"
first_day_of_month = datetime(year, month, 1)
last_day_of_month = datetime(
year, month, calendar.monthrange(year, month)[1], 23, 59, 59)
start_date_str = first_day_of_month.strftime('%Y-%m-%d %H:%M:%S')
end_date_str = last_day_of_month.strftime('%Y-%m-%d %H:%M:%S')
domain.extend([
('check_in', '>=', start_date_str),
('check_in', '<=', end_date_str)
])
kwargs = {
"domain": domain,
"fields": ["id", "employee_id", "check_in", "check_out", "worked_hours", "attendance_reason_ids"],
}
if limit is not None:
kwargs["limit"] = limit
result = self.call_odoo_method(
"hr.attendance", "web_search_read", kwargs=kwargs)
if "records" not in result:
# logger.error("Unexpected response format: 'records' not found")
raise HTTPException(
status_code=502,
detail="Unexpected response format: 'records' not found"
)
sorted_data = sorted(result["records"], key=lambda x: x["check_in"])
return sorted_data
def call_odoo_method(self, model: str, method: str, args: list = None, kwargs: dict = None):
if args is None:
args = []
if kwargs is None:
kwargs = {}
url = f"{self.odoo_url}/web/dataset/call_kw/{model}/{method}"
headers = {'Content-Type': 'application/json'}
payload = {
"jsonrpc": "2.0",
"method": "call",
"params": {
"model": model,
"method": method,
"args": args,
"kwargs": kwargs
},
"id": 1
}
response = self.session.post(url, json=payload, headers=headers)
# response = requests.post(url, json=payload, headers=headers, cookies=self._session_cookies)
if response.status_code != 200:
# logger.error(
# f"Failed to call Odoo method {method} on model {model}: {response.status_code}")
raise HTTPException(
status_code=response.status_code,
detail=f"Failed to call Odoo method {method} on model {model}"
)
data = response.json()
if "error" in data:
raise HTTPException(
status_code=502,
detail=f"Odoo RPC error: {data['error'].get('message', 'Unknown error')}"
)
return data["result"]