From f81ba1681b302b03122366b3591be2e5cdb5d14c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 15:33:47 +0200 Subject: [PATCH] =?UTF-8?q?fix(msp):=20Performance-Chart=20=E2=80=94=20Abw?= =?UTF-8?q?esenheiten=20aus=20itsdave=5Fhr=20statt=20hrms-Attendance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_attendance_list fragte den DocType `Attendance` ab, den es seit dem v16-Stand ohne hrms nicht mehr gibt. Der DoesNotExistError riss die gesamte Kette get_data -> compare_hours -> get_target_hours mit, der Dashboard-Chart "Meine Performance" blieb fuer alle Nutzer auf "Loading" haengen (ITSD-22440). Quelle sind jetzt genehmigte HR Absence Requests, deren Abwesenheitsart das Soll reduziert (HR Absence Type.affects_target_hours). Halbe Tage (half_day_start/half_day_end) halbieren das Tagessoll. Co-Authored-By: Claude Opus 5 (1M context) --- msp/tools.py | 67 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/msp/tools.py b/msp/tools.py index 85845a0..4be0d90 100644 --- a/msp/tools.py +++ b/msp/tools.py @@ -3,6 +3,7 @@ import json from datetime import datetime, date, timedelta import math from frappe.database import get_db +from frappe.utils import getdate from erpnext.stock.utils import get_stock_balance import pandas as pd import requests @@ -251,20 +252,64 @@ def get_holidays_list(from_date, to_date): return holidays_list +def get_hr_employee(employee): + """Loest einen ERPNext-Employee auf den zugehoerigen HR Employee auf.""" + if not frappe.db.exists("DocType", "HR Employee"): + return None + + hr_employee = frappe.db.get_value("HR Employee", {"source_employee": employee}, "name") + if not hr_employee and frappe.db.exists("HR Employee", employee): + hr_employee = employee + + return hr_employee + + def get_attendance_list(employee, from_date, to_date): + """Soll-mindernde Abwesenheiten je Tag aus itsdave_hr. - # List of attendance data for the employee in the specified period - attendance_data = frappe.get_all("Attendance", - filters={ - "employee": employee, - "attendance_date": (">=", from_date), - "attendance_date": ("<=", to_date) - }, - fields=["attendance_date", "status"]) + Liefert {Datum: "On Leave"} fuer ganze und {Datum: "Half Day"} fuer halbe + Abwesenheitstage. Beruecksichtigt werden nur genehmigte Antraege, deren + Abwesenheitsart das Soll reduziert (HR Absence Type.affects_target_hours). + """ + hr_employee = get_hr_employee(employee) + if not hr_employee: + return {} - # Dictionary with date as key and attendance status as value - attendance_dict = {entry["attendance_date"]: entry["status"] for entry in attendance_data} - return attendance_dict + from_date = getdate(from_date) + to_date = getdate(to_date) + + absences = frappe.get_all("HR Absence Request", + filters={ + "employee": hr_employee, + "status": "Approved", + "from_date": ("<=", to_date), + "to_date": (">=", from_date) + }, + fields=["absence_type", "from_date", "to_date", + "half_day_start", "half_day_end"], + order_by="from_date asc, name asc") + + day_factors = {} + + for absence in absences: + if not frappe.get_cached_value("HR Absence Type", absence["absence_type"], "affects_target_hours"): + continue + + absence_from = getdate(absence["from_date"]) + absence_to = getdate(absence["to_date"]) + + current_date = max(from_date, absence_from) + last_date = min(to_date, absence_to) + + while current_date <= last_date: + half_day = ((absence["half_day_start"] and current_date == absence_from) + or (absence["half_day_end"] and current_date == absence_to)) + factor = 0.5 if half_day else 1.0 + day_factors[current_date] = min(1.0, day_factors.get(current_date, 0.0) + factor) + current_date += timedelta(days=1) + + return {day: ("On Leave" if factor >= 1.0 else "Half Day") + for day, factor in day_factors.items()} @frappe.whitelist()