odoo_hours setup and ready
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
__pycache__
|
||||
db.sqlite3
|
||||
env.*
|
||||
.env
|
||||
@@ -12,6 +12,9 @@ https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from django.core.management.commands.runserver import Command as runserver
|
||||
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/
|
||||
|
||||
# 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 = os.environ.get("SECRET_KEY")
|
||||
# SECRET_KEY = 'django-insecure-snqnsca90!r=^iu6yyhpxy^+mjm%7dvrg(lb!6fr3(!9yh(c30'
|
||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
# DEBUG = bool(os.environ.get("DEBUG", default=0))
|
||||
# DEBUG = True
|
||||
DEBUG = bool(os.getenv("DEBUG", default=0))
|
||||
|
||||
ALLOWED_HOSTS = ['192.168.1.146']
|
||||
# ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")
|
||||
# ALLOWED_HOSTS = ['192.168.1.146']
|
||||
ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS").split(" ")
|
||||
|
||||
|
||||
# Application definition
|
||||
@@ -46,7 +49,8 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'homepage'
|
||||
'homepage',
|
||||
'odoo_hours'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -18,8 +18,10 @@ from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
from homepage import views as homepage
|
||||
from odoo_hours import views as odoo_hours
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', homepage.index, name="homepage")
|
||||
path('', homepage.index, name="homepage"),
|
||||
path("odoo-hours/", odoo_hours.index, name="odoo-hours"),
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<h5 class="card-title">Odoo godziny</h5>
|
||||
<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>
|
||||
|
||||
104
app/odoo_hours/templates/calendar.html
Normal file
104
app/odoo_hours/templates/calendar.html
Normal 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>
|
||||
54
app/odoo_hours/templates/hours_form.html
Normal file
54
app/odoo_hours/templates/hours_form.html
Normal 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
157
app/odoo_hours/text_conv.py
Normal 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)
|
||||
@@ -1,3 +1,23 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
from datetime import datetime
|
||||
from .text_conv import generate_monthly_summary, process_input_txt
|
||||
# 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")
|
||||
Reference in New Issue
Block a user