odoo_hours setup and ready

This commit is contained in:
2024-11-25 19:11:28 +00:00
parent 78241144e2
commit 111c51a072
8 changed files with 353 additions and 11 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.venv .venv
__pycache__ __pycache__
db.sqlite3 db.sqlite3
env.* env.*
.env

View File

@@ -12,6 +12,9 @@ https://docs.djangoproject.com/en/5.1/ref/settings/
from pathlib import Path from pathlib import Path
import os import os
from dotenv import load_dotenv
load_dotenv()
from django.core.management.commands.runserver import Command as runserver from django.core.management.commands.runserver import Command as runserver
runserver.default_port = "7100" runserver.default_port = "7100"
@@ -25,15 +28,15 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-snqnsca90!r=^iu6yyhpxy^+mjm%7dvrg(lb!6fr3(!9yh(c30' # SECRET_KEY = 'django-insecure-snqnsca90!r=^iu6yyhpxy^+mjm%7dvrg(lb!6fr3(!9yh(c30'
# SECRET_KEY = os.environ.get("SECRET_KEY") SECRET_KEY = os.getenv("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True # DEBUG = True
# DEBUG = bool(os.environ.get("DEBUG", default=0)) DEBUG = bool(os.getenv("DEBUG", default=0))
ALLOWED_HOSTS = ['192.168.1.146'] # ALLOWED_HOSTS = ['192.168.1.146']
# ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ") ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS").split(" ")
# Application definition # Application definition
@@ -46,7 +49,8 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'homepage' 'homepage',
'odoo_hours'
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@@ -18,8 +18,10 @@ from django.contrib import admin
from django.urls import path from django.urls import path
from homepage import views as homepage from homepage import views as homepage
from odoo_hours import views as odoo_hours
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', homepage.index, name="homepage") path('', homepage.index, name="homepage"),
path("odoo-hours/", odoo_hours.index, name="odoo-hours"),
] ]

View File

@@ -23,7 +23,7 @@
<h5 class="card-title">Odoo godziny</h5> <h5 class="card-title">Odoo godziny</h5>
<p class="card-text text-muted">Oblicz godziny pracy na podstawie tabeli odoo.</p> <p class="card-text text-muted">Oblicz godziny pracy na podstawie tabeli odoo.</p>
<a href="#" class="btn btn-outline-primary">ENTER</a> <a href="{% url 'odoo-hours' %}" class="btn btn-outline-primary">ENTER</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kalendarz pracy</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<style>
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr); /* 7 dni tygodnia */
{% comment %} gap: 10px; {% endcomment %}
}
.day {
text-align: center;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.day .date {
font-weight: bold;
margin-bottom: 5px;
}
.day .hours {
font-size: 2rem;
color: #555;
}
.day.active {
background-color: #007bff;
color: #fff;
}
</style>
</head>
<body class="bg-light">
<div class="container mx-auto my-4">
<h1 class="text-center mb-4">Kalendarz pracy</h1>
<div class="row g-3 calendar">
{% for _ in empty_days %}
<div class="col">
{% comment %}
<div class="card" style="height: 100%">
<div class="card-header text-center">X</div>
<div class="card-body text-center"></div>
</div>
{% endcomment %}
</div>
{% endfor %} {% for day in days %}
<div class="col">
<div class="card text-center" style="min-height: 228px; height: 100%">
{% if day.weekday == 5 %}
<div class="card-header bg-warning">
<h5 class="mb-0">{{ day.day}}</h5>
</div>
{% elif day.weekday == 6 %}
<div class="card-header text-white bg-danger">
<h5 class="mb-0">{{ day.day}}</h5>
</div>
{% elif day.is_holiday %}
<div class="card-header bg-info">
<h5 class="mb-0">{{ day.day}}</h5>
</div>
{% else %}
<div class="card-header text-white bg-secondary">
<h5 class="mb-0">{{ day.day}}</h5>
</div>
{% endif %}
<div class="card-body pt-1">
{% if day.is_holiday == False and day.weekday < 5 %}
<div>
<span
class="d-block text-end {% if '-' in day.daily_diffrance %} text-danger {% else %} text-success {% endif %}"
>
{{ day.daily_diffrance}}
</span>
</div>
<div>
<h1 class="display-6">{{ day.daily_actual}}</h1>
</div>
<div>
<div class="row row-cols-2 justify-content-center text-nowrap">
<strong class="d-block col">Expected:</strong>
<span class="d-block col">{{day.total_expected}}</span>
<strong class="d-block col">Actual:</strong>
<span class="d-block col">{{day.total_actual}}</span>
</div>
</div>
{% endif %}
</div>
{% if '-' in day.total_diffrance %}
<div class="card-footer bg-danger pt-0 pb-1">
<span class="text-white fs-5">{{day.total_diffrance}}</span>
</div>
{% else %}
<div class="card-footer bg-success pt-0 pb-1">
<span class="text-white fs-5">{{day.total_diffrance}}</span>
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous"
/>
<title>Odoo Hours</title>
</head>
<body class="bg-light">
<div class="container d-flex justify-content-center mt-5">
<div class="card shadow-sm w-100" style="max-width: 800px">
<div class="card-body">
<h1 class="text-center mb-4">Przetwarzanie tekstu</h1>
<form method="POST">
{% csrf_token %}
<div class="mb-3">
<label for="inputText" class="form-label">Wklej swój tekst:</label>
<textarea
name="input_text"
id="inputText"
class="form-control"
rows="20"
placeholder="Wpisz lub wklej tutaj swój tekst"
required
></textarea>
<div class="invalid-feedback">Pole tekstowe nie może być puste.</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Przetwórz tekst</button>
</div>
</form>
{% if error %}
<div class="mt-4">
<h2 class="text-center">Error:</h2>
<p class="text-center alert alert-danger">{{ error }}</p>
</div>
{% endif %}
</div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous"
></script>
</body>
</html>

157
app/odoo_hours/text_conv.py Normal file
View File

@@ -0,0 +1,157 @@
import re
from datetime import datetime, timedelta
import holidays
import calendar
import json
def timedelta_to_string(td: timedelta) -> str:
"""Convert a timedelta object to a string in the format HH:MM."""
total_minutes = int(td.total_seconds() / 60)
hours = abs(total_minutes) // 60
minutes = abs(total_minutes) % 60
sign = "-" if td.total_seconds() < 0 else ""
return f"{sign}{hours:02}:{minutes:02}"
def process_input_txt(text: str) -> list:
"""
Process text containing work records and return a sorted list of dictionaries
containing the extracted data.
Args:
text (str): Text containing work records.
Returns:
list[dict]: List of dictionaries containing the fields 'enter', 'exit', and 'duration'.
"""
pattern = re.compile(
r"""
(?P<name>\S+\s\S+)\s+ # Name
(?P<start_date>\d{2}\.\d{2}\.\d{4}\s\d{2}:\d{2}:\d{2})\s+ # Start date
(?P<end_date>\d{2}\.\d{2}\.\d{4}\s\d{2}:\d{2}:\d{2})\s+ # End date
(?P<duration>\d{2}:\d{2}) # Duration
""",
re.VERBOSE,
)
results = []
for line in text.splitlines():
line = line.strip()
if not line:
continue
match = pattern.match(line)
if match:
try:
start_date = datetime.strptime(match.group("start_date"), "%d.%m.%Y %H:%M:%S")
end_date = datetime.strptime(match.group("end_date"), "%d.%m.%Y %H:%M:%S")
duration = timedelta(
hours=int(match.group("duration")[:2]), minutes=int(match.group("duration")[3:])
)
results.append(
{
"enter": start_date,
"exit": end_date,
"duration": duration,
}
)
except ValueError:
# Ignore invalid dates
continue
# Sort results by start date
results.sort(key=lambda x: x["enter"])
return results
def generate_monthly_summary(work_records: list, year: int, month: int) -> list:
# Polska lista dni świątecznych
pl_holidays = holidays.Poland(years=year)
# Liczba dni w miesiącu
_, num_days = calendar.monthrange(year, month)
summary = []
# Oblicz godziny pracy na dzień (8h 15min = 8.25h)
daily_work_hours = timedelta(hours=8, minutes=15)
holiday_work_hours = timedelta(hours=0, minutes=15)
# Suma godzin oczekiwanych i rzeczywistych
total_expected = timedelta(0)
total_actual = timedelta(0)
# Tworzenie wpisów dla każdego dnia
for day in range(1, num_days + 1):
date = datetime(year, month, day)
weekday = date.weekday() # 0 = poniedziałek, ..., 6 = niedziela
is_holiday = date in pl_holidays
should_work = weekday < 5 and not is_holiday # Praca w dni robocze poza świętami
# Aktualizuj oczekiwany czas pracy
if should_work:
total_expected += daily_work_hours
# Znajdź godziny faktyczne dla tego dnia
daily_actual = sum(
(record["duration"]
for record in work_records
if record["enter"].date() == date.date()) , timedelta()
)
total_actual += daily_actual # Dodanie timedelta do sumy
# Dodaj dane do listy podsumowania
summary.append({
"day": date.date(),
"weekday": weekday,
"is_holiday": is_holiday,
"total_expected": timedelta_to_string(total_expected),
"total_actual": timedelta_to_string(total_actual),
"total_diffrance": timedelta_to_string(total_actual - total_expected),
"daily_actual": timedelta_to_string(daily_actual),
"daily_diffrance": timedelta_to_string(daily_actual - (daily_work_hours if should_work else holiday_work_hours if daily_actual >= holiday_work_hours else timedelta(0))),
})
return summary
# text3 = """
# Worked hours
# Attendance Reason
# Marcin Nowak 22.11.2024 07:12:30 22.11.2024 16:05:33 08:53
# Marcin Nowak 21.11.2024 07:25:38 21.11.2024 16:43:19 09:18
# Marcin Nowak 20.11.2024 07:31:46 20.11.2024 15:47:36 08:16
# Marcin Nowak 19.11.2024 07:10:39 19.11.2024 15:54:14 08:44
# Marcin Nowak 18.11.2024 07:13:11 18.11.2024 16:04:15 08:51
# Marcin Nowak 15.11.2024 07:06:31 15.11.2024 15:41:49 08:35
# Marcin Nowak 14.11.2024 06:56:23 14.11.2024 16:11:52 09:15
# Marcin Nowak 13.11.2024 07:12:25 13.11.2024 17:19:58 10:08
# Marcin Nowak 12.11.2024 07:40:57 12.11.2024 16:10:42 08:30
# Marcin Nowak 08.11.2024 07:01:22 08.11.2024 15:48:01 08:47
# Marcin Nowak 07.11.2024 07:07:53 07.11.2024 16:26:30 09:19
# Marcin Nowak 06.11.2024 07:11:46 06.11.2024 16:08:43 08:57
# Marcin Nowak 05.11.2024 07:16:33 05.11.2024 16:10:52 08:54
# Marcin Nowak 04.11.2024 07:28:10 04.11.2024 15:51:19 08:23
# """
# # Przetwórz dane wejściowe
# work_records = process_input_txt(text3)
# date = work_records[-1]["enter"].date()
# monthly_summary = generate_monthly_summary(work_records, date.year, date.month)
# # Przykład wyświetlenia podsumowania
# import pprint
# pprint.pprint(monthly_summary)

View File

@@ -1,3 +1,23 @@
from django.shortcuts import render from django.shortcuts import render
from datetime import datetime
from .text_conv import generate_monthly_summary, process_input_txt
# Create your views here. # Create your views here.
def index(request):
if request.method == "POST":
input_text = request.POST.get("input_text", "")
if input_text:
# Przetwarzanie tekstu
try:
work_records = process_input_txt(input_text)
last_day = work_records[-1]["enter"].date()
empty_days = datetime(year=last_day.year, month=last_day.month, day=1).weekday()
monthly_summary = generate_monthly_summary(work_records, last_day.year, last_day.month)
return render(request, "calendar.html", {
"days": monthly_summary,
"empty_days": range(empty_days),
})
except:
return render(request, "hours_form.html", {"error": "Cos poszlo nie tak!"})
return render(request, "hours_form.html")