diff --git a/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py b/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py
index 150aa9f59..85ec43d87 100644
--- a/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py
+++ b/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py
@@ -189,19 +189,16 @@ class HrmsEmployeeDashboard(http.Controller):
return self._yearly_attendance_calendar(month_start, month_end, attendances, leaves, public_holidays)
today = fields.Date.context_today(request.env.user)
- attendance_by_day = defaultdict(float)
- for attendance in attendances:
- day = fields.Datetime.context_timestamp(request.env.user, attendance.check_in).date()
- attendance_by_day[day] += attendance.worked_hours or 0.0
+ attendance_by_day = self._attendance_hours_by_day(attendances)
- leave_by_day = self._days_from_date_range_records(leaves, "request_date_from", "request_date_to", "holiday_status_id")
+ leave_by_day = self._leave_days(leaves)
holiday_by_day = self._days_from_datetime_range_records(public_holidays, "date_from", "date_to")
days = []
cursor = month_start
if calendar_view == "monthly":
- cursor = month_start - timedelta(days=(month_start.weekday() + 1) % 7)
- end_cursor = month_end + timedelta(days=(5 - month_end.weekday()) % 7)
+ cursor = month_start - timedelta(days=month_start.weekday())
+ end_cursor = month_end + timedelta(days=(6 - month_end.weekday()))
else:
end_cursor = month_end
@@ -209,34 +206,91 @@ class HrmsEmployeeDashboard(http.Controller):
outside_period = cursor < month_start or cursor > month_end
status = "future" if cursor > today else "absent"
label = ""
- hours = round(attendance_by_day.get(cursor, 0.0), 2)
+ attendance_hours = attendance_by_day.get(cursor, {})
+ hours = round(attendance_hours.get("worked_hours", 0.0), 2)
+ break_hours = round(attendance_hours.get("break_hours", 0.0), 2)
+ has_worked_hours = hours > 0.004
+ is_holiday = cursor in holiday_by_day
+ is_leave = cursor in leave_by_day
+ is_half_day_leave = bool(leave_by_day.get(cursor, {}).get("is_half_day"))
+ is_weekend = cursor.weekday() in (5, 6)
+ expected_hours = 0.0
+ if not outside_period and not is_holiday and not (is_weekend and not has_worked_hours):
+ expected_hours = self._expected_hours_for_day(request.env.user.employee_id.sudo(), cursor)
+ leave_hours = expected_hours if is_leave else 0.0
+ show_metrics = (
+ has_worked_hours
+ or (
+ not is_weekend
+ and not is_holiday
+ and (not is_leave or is_half_day_leave)
+ )
+ )
if outside_period:
status = "empty"
- elif hours:
- status = "present"
- elif cursor in holiday_by_day:
+ show_metrics = False
+ elif is_leave:
+ status = "leave"
+ label = _("Time Off")
+ show_metrics = is_half_day_leave
+ elif is_holiday:
status = "holiday"
label = holiday_by_day[cursor]
- elif cursor in leave_by_day:
- status = "leave"
- label = leave_by_day[cursor]
- elif cursor.weekday() in (5, 6):
+ show_metrics = has_worked_hours
+ elif has_worked_hours:
+ status = "present"
+ elif is_weekend:
status = "weekend"
- label = _("Weekend")
+ show_metrics = False
days.append({
"type": "day",
"date": cursor.strftime("%Y-%m-%d"),
"day": cursor.day,
"weekday": cursor.strftime("%a"),
- "is_weekend": cursor.weekday() in (5, 6),
+ "is_weekend": is_weekend,
"outside_period": outside_period,
"status": status,
"label": label,
+ "show_metrics": show_metrics,
+ "is_half_day_leave": is_half_day_leave,
"hours": hours,
+ "worked_hours": hours,
+ "break_hours": break_hours,
+ "expected_hours": round(expected_hours, 2),
+ "leave_hours": round(leave_hours, 2),
+ "balance_hours": round(hours - expected_hours, 2),
+ "worked_display": self._format_hours(hours),
+ "break_display": self._format_hours(break_hours),
+ "expected_display": self._format_hours(expected_hours),
+ "leave_display": self._format_hours(leave_hours),
+ "balance_display": self._format_signed_hours(hours - expected_hours),
})
cursor += timedelta(days=1)
return days
+ def _leave_days(self, leaves):
+ result = {}
+ for leave in leaves:
+ start = fields.Date.to_date(leave.request_date_from)
+ end = fields.Date.to_date(leave.request_date_to)
+ is_half_day = self._is_half_day_leave(leave)
+ while start and end and start <= end:
+ result[start] = {
+ "label": _("Time Off"),
+ "is_half_day": is_half_day,
+ }
+ start += timedelta(days=1)
+ return result
+
+ def _is_half_day_leave(self, leave):
+ if "request_unit_half" in leave._fields and leave.request_unit_half:
+ return True
+ if "request_unit_type" in leave._fields and leave.request_unit_type == "half_day":
+ return True
+ if "request_unit" in leave._fields and leave.request_unit == "half_day":
+ return True
+ return bool(leave.number_of_days and leave.number_of_days < 1)
+
def _yearly_attendance_calendar(self, range_start, range_end, attendances, leaves, public_holidays):
days = self._attendance_calendar(range_start, range_end, attendances, leaves, public_holidays, "monthly")
months = []
@@ -259,10 +313,59 @@ class HrmsEmployeeDashboard(http.Controller):
"leave": counts["leave"],
"holiday": counts["holiday"],
"hours": round(sum(day["hours"] for day in month_days), 2),
+ "worked_hours": round(sum(day["worked_hours"] for day in month_days), 2),
+ "break_hours": round(sum(day["break_hours"] for day in month_days), 2),
+ "expected_hours": round(sum(day["expected_hours"] for day in month_days), 2),
+ "worked_display": self._format_hours(sum(day["worked_hours"] for day in month_days)),
+ "break_display": self._format_hours(sum(day["break_hours"] for day in month_days)),
+ "expected_display": self._format_hours(sum(day["expected_hours"] for day in month_days)),
})
cursor += relativedelta(months=1)
return months
+ def _attendance_hours_by_day(self, attendances):
+ grouped = defaultdict(list)
+ for attendance in attendances:
+ check_in = fields.Datetime.context_timestamp(request.env.user, attendance.check_in)
+ grouped[check_in.date()].append(attendance)
+
+ result = {}
+ for day, day_attendances in grouped.items():
+ sorted_attendances = sorted(day_attendances, key=lambda attendance: attendance.check_in)
+ worked_hours = sum(attendance.worked_hours or 0.0 for attendance in sorted_attendances)
+ break_hours = 0.0
+ for previous, current in zip(sorted_attendances, sorted_attendances[1:]):
+ if previous.check_out and current.check_in:
+ previous_out = fields.Datetime.context_timestamp(request.env.user, previous.check_out)
+ current_in = fields.Datetime.context_timestamp(request.env.user, current.check_in)
+ if current_in > previous_out:
+ break_hours += (current_in - previous_out).total_seconds() / 3600
+ result[day] = {
+ "worked_hours": worked_hours,
+ "break_hours": break_hours,
+ }
+ return result
+
+ def _expected_hours_for_day(self, employee, day):
+ calendar = employee.resource_calendar_id or employee.company_id.resource_calendar_id
+ if not calendar:
+ return 0.0
+ start_dt = datetime.combine(day, time.min)
+ end_dt = datetime.combine(day, time.max)
+ return round(calendar.get_work_hours_count(start_dt, end_dt, compute_leaves=False), 2)
+
+ def _format_hours(self, hours):
+ hours = abs(hours or 0.0)
+ total_minutes = int(round(hours * 60))
+ hour_part, minute_part = divmod(total_minutes, 60)
+ if minute_part:
+ return "%sh %02dm" % (hour_part, minute_part)
+ return "%sh" % hour_part
+
+ def _format_signed_hours(self, hours):
+ sign = "+" if (hours or 0.0) >= 0 else "-"
+ return "%s%s" % (sign, self._format_hours(hours))
+
def _days_from_date_range_records(self, records, start_field, end_field, label_field=False):
result = {}
for record in records:
@@ -289,12 +392,27 @@ class HrmsEmployeeDashboard(http.Controller):
counts = defaultdict(int)
for day in days:
counts[day["status"]] += 1
+ expected_hours = sum(day["expected_hours"] for day in days if not day["outside_period"])
+ worked_hours = sum(attendances.mapped("worked_hours"))
+ break_hours = sum(day["break_hours"] for day in days if not day["outside_period"])
+ leave_hours = sum(day["leave_hours"] for day in days if not day["outside_period"])
+ remaining_hours = expected_hours - worked_hours - leave_hours
return {
"present": counts["present"],
"absent": counts["absent"],
"leave": counts["leave"],
"holiday": counts["holiday"],
- "hours": round(sum(attendances.mapped("worked_hours")), 2),
+ "hours": round(worked_hours, 2),
+ "worked_hours": round(worked_hours, 2),
+ "break_hours": round(break_hours, 2),
+ "expected_hours": round(expected_hours, 2),
+ "leave_hours": round(leave_hours, 2),
+ "remaining_hours": round(remaining_hours, 2),
+ "worked_display": self._format_hours(worked_hours),
+ "break_display": self._format_hours(break_hours),
+ "expected_display": self._format_hours(expected_hours),
+ "leave_display": self._format_hours(leave_hours),
+ "remaining_display": self._format_signed_hours(remaining_hours),
}
def _holiday_list(self, holidays):
@@ -317,25 +435,15 @@ class HrmsEmployeeDashboard(http.Controller):
totals[month.strftime("%Y-%m")] += 0.0
month += relativedelta(months=1)
- HrExpense = request.env["hr.expense"].sudo()
- emp_expenses = HrExpense.search([
+ expenses = request.env["hr.expense"].sudo().search([
("employee_id", "=", employee.id),
("date", ">=", date_from),
("date", "<=", date_to),
])
- for expense in emp_expenses:
- totals[expense.date.strftime("%Y-%m")] += expense.total_amount or 0.0
- state_totals[expense.state or "draft"] += expense.total_amount or 0.0
-
- if request.env.registry.get("hr.expense"):
- hr_expenses = request.env["hr.expense"].sudo().search([
- ("employee_id", "=", employee.id),
- ("date", ">=", date_from),
- ("date", "<=", date_to),
- ])
- for expense in hr_expenses:
- totals[expense.date.strftime("%Y-%m")] += expense.total_amount_currency or expense.total_amount or 0.0
- state_totals[expense.state or "draft"] += expense.total_amount_currency or expense.total_amount or 0.0
+ for expense in expenses:
+ amount = self._expense_amount(expense)
+ totals[expense.date.strftime("%Y-%m")] += amount
+ state_totals[expense.state or "draft"] += amount
return {
"labels": labels,
@@ -345,6 +453,11 @@ class HrmsEmployeeDashboard(http.Controller):
"currency": employee.company_id.currency_id.symbol or "",
}
+ def _expense_amount(self, expense):
+ if "total_amount_currency" in expense._fields and expense.total_amount_currency:
+ return expense.total_amount_currency
+ return expense.total_amount or 0.0
+
def _month_keys(self, first_month, last_month):
keys = []
month = first_month
diff --git a/addons_extensions/hrms_emp_dashboard/static/src/css/hrms_emp_dashboard.css b/addons_extensions/hrms_emp_dashboard/static/src/css/hrms_emp_dashboard.css
index 1308ba71f..3687fefd1 100644
--- a/addons_extensions/hrms_emp_dashboard/static/src/css/hrms_emp_dashboard.css
+++ b/addons_extensions/hrms_emp_dashboard/static/src/css/hrms_emp_dashboard.css
@@ -184,8 +184,11 @@
.hrms-kpi.present strong { color: #16a34a; }
.hrms-kpi.absent strong { color: #dc2626; }
+.hrms-kpi.break strong { color: #2563eb; }
.hrms-kpi.leave strong,
.hrms-kpi.holiday strong { color: #f97316; }
+.hrms-kpi.remaining.positive strong { color: #16a34a; }
+.hrms-kpi.remaining.negative strong { color: #dc2626; }
.hrms-grid {
display: grid;
@@ -230,65 +233,217 @@
.hrms-calendar {
display: grid;
- grid-template-columns: repeat(7, minmax(82px, 1fr));
- gap: 10px;
+ grid-template-columns: repeat(7, minmax(130px, 1fr));
+ gap: 0;
+ overflow-x: auto;
+ border: 1px solid #dbe3ed;
+ border-radius: 8px;
+ background: #dbe3ed;
}
.hrms-calendar-weekdays {
display: grid;
- grid-template-columns: repeat(7, minmax(82px, 1fr));
- gap: 10px;
- margin-bottom: 8px;
+ grid-template-columns: repeat(7, minmax(130px, 1fr));
+ gap: 0;
+ overflow-x: auto;
+ border: 1px solid #dbe3ed;
+ border-bottom: 0;
+ border-radius: 8px 8px 0 0;
+ background: #f8fafc;
}
.hrms-calendar-weekdays span {
- color: #64748b;
- font-size: 11px;
+ padding: 10px 8px;
+ color: #0f172a;
+ font-size: 13px;
font-weight: 800;
text-align: center;
- text-transform: uppercase;
+ border-right: 1px solid #dbe3ed;
+}
+
+.hrms-calendar-weekdays span:last-child {
+ border-right: 0;
}
.hrms-calendar.yearly {
grid-template-columns: repeat(4, minmax(160px, 1fr));
+ gap: 10px;
+ border: 0;
+ background: transparent;
}
.hrms-day {
- min-height: 82px;
- padding: 8px;
+ min-height: 132px;
+ padding: 10px;
+ border: 0;
+ border-right: 1px solid #dbe3ed;
+ border-bottom: 1px solid #dbe3ed;
+ border-radius: 0;
+ background: #fff;
+ position: relative;
+ overflow: hidden;
+}
+
+.hrms-calendar.yearly .hrms-day {
+ min-height: 126px;
border: 1px solid #e5e7eb;
border-radius: 8px;
- background: #fff;
}
.hrms-day span,
.hrms-day small {
- display: block;
color: #64748b;
- font-size: 11px;
+ font-size: 12px;
}
-.hrms-day strong {
- display: block;
- width: 34px;
- height: 34px;
- line-height: 31px;
- margin-top: 6px;
+.hrms-day-top {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 6px;
+ min-height: 24px;
+}
+
+.hrms-day-top strong {
+ color: #0f172a;
+ font-size: 13px;
+ line-height: 1.2;
+}
+
+.hrms-day-top strong span {
+ display: inline;
+ margin-left: 4px;
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.hrms-day-badge {
+ max-width: 76px;
+ padding: 2px 6px;
+ border-radius: 6px;
+ background: #eef2ff;
+ color: #4338ca;
+ font-size: 10px;
+ font-weight: 800;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.hrms-day-balance {
+ height: 22px;
+ line-height: 22px;
+ margin: 10px 0 8px;
border-radius: 999px;
text-align: center;
- border: 2px solid #cbd5e1;
- color: #0f172a;
+ font-size: 12px;
+ font-weight: 800;
}
-.hrms-day.present strong { border-color: #16a34a; background: #dcfce7; }
-.hrms-day.absent strong { border-color: #dc2626; color: #dc2626; background: #fff; }
-.hrms-day.leave strong,
-.hrms-day.holiday strong { border-color: #f97316; background: #ffedd5; color: #9a3412; }
-.hrms-day.weekend strong { border-color: #94a3b8; background: #f1f5f9; color: #475569; }
+.hrms-day-balance.positive {
+ background: #dcfce7;
+ color: #16a34a;
+}
+
+.hrms-day-balance.negative {
+ background: #fee2e2;
+ color: #dc2626;
+}
+
+.hrms-day-message {
+ min-height: 76px;
+ margin-top: 10px;
+ padding: 12px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ text-align: center;
+}
+
+.hrms-day-message-icon {
+ color: inherit;
+ font-size: 16px;
+}
+
+.hrms-day-message strong {
+ color: inherit;
+ font-size: 13px;
+ line-height: 1.25;
+ overflow-wrap: anywhere;
+}
+
+.hrms-day-metrics {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 6px;
+}
+
+.hrms-day-metrics div {
+ min-width: 0;
+}
+
+.hrms-day-metrics span {
+ display: block;
+ color: #64748b;
+ font-size: 10px;
+ font-weight: 800;
+}
+
+.hrms-day-metrics strong {
+ display: block;
+ margin-top: 3px;
+ color: #0f172a;
+ font-size: 13px;
+ line-height: 1.15;
+ word-break: keep-all;
+}
+
+.hrms-calendar-month-stats {
+ display: grid;
+ gap: 5px;
+ margin-top: 10px;
+}
+
+.hrms-calendar-month-stats small {
+ display: block;
+ font-size: 12px;
+}
+
+.hrms-day.present { box-shadow: inset 0 3px 0 #22c55e; }
+.hrms-day.absent { box-shadow: inset 0 3px 0 #ef4444; }
+.hrms-day.leave {
+ background: #fff5f5;
+ box-shadow: inset 0 3px 0 #ef4444;
+}
+.hrms-day.leave .hrms-day-message {
+ background: #fee2e2;
+ color: #b91c1c;
+ border: 1px solid #fecaca;
+}
+.hrms-day.holiday {
+ background: #f8fafc;
+ box-shadow: inset 0 3px 0 #94a3b8;
+}
+.hrms-day.holiday .hrms-day-message {
+ background: #e5e7eb;
+ color: #334155;
+ border: 1px solid #cbd5e1;
+}
+.hrms-day.weekend {
+ background: #f8fafc;
+ box-shadow: none;
+}
+.hrms-day.weekend .hrms-day-message {
+ min-height: 58px;
+ background: transparent;
+ color: #94a3b8;
+ border: 1px dashed #e2e8f0;
+}
.hrms-day.future { opacity: 0.55; }
.hrms-day.empty {
background: #f8fafc;
- border-style: dashed;
opacity: 0.45;
}
@@ -416,13 +571,17 @@
@media (max-width: 700px) {
.hrms-kpi-row,
.hrms-equipment-grid,
- .hrms-calendar,
.hrms-calendar.yearly,
.hrms-leave-summary,
.hrms-custom-dates {
grid-template-columns: 1fr;
}
+ .hrms-calendar,
+ .hrms-calendar-weekdays {
+ grid-template-columns: repeat(7, minmax(118px, 1fr));
+ }
+
.hrms-panel-header,
.hrms-panel-actions,
.hrms-action-buttons {
@@ -488,4 +647,4 @@
#hrmsExpenseChart,
#hrmsExpenseStateChart{
min-height:340px;
-}
\ No newline at end of file
+}
diff --git a/addons_extensions/hrms_emp_dashboard/static/src/js/hrms_emp_dashboard.js b/addons_extensions/hrms_emp_dashboard/static/src/js/hrms_emp_dashboard.js
index 05fa78f80..e32a870d6 100644
--- a/addons_extensions/hrms_emp_dashboard/static/src/js/hrms_emp_dashboard.js
+++ b/addons_extensions/hrms_emp_dashboard/static/src/js/hrms_emp_dashboard.js
@@ -14,7 +14,7 @@ class HrmsEmployeeDashboard extends Component {
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0);
const weekStart = new Date(today);
- weekStart.setDate(today.getDate() - today.getDay());
+ weekStart.setDate(today.getDate() - ((today.getDay() + 6) % 7));
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
@@ -107,7 +107,6 @@ class HrmsEmployeeDashboard extends Component {
}
this.destroyCharts();
const leaves = this.state.data.leave_balances || [];
- debugger;
this.renderLeaveChart(leaves);
this.renderExpenseChart();
this.renderExpenseStateChart();
@@ -155,7 +154,6 @@ class HrmsEmployeeDashboard extends Component {
return;
}
const expenses = this.state.data.expenses;
- debugger;
this.chartBase("#hrmsExpenseChart", {
chart: {
type: "area",
@@ -183,8 +181,6 @@ class HrmsEmployeeDashboard extends Component {
type: "donut",
events: {
dataPointSelection: (event, chartContext, config) => {
- debugger;
- const stateIndex = chartContext.dataPointIndex;
const state = expenses.state_labels[config.dataPointIndex];
this.openExpenses(state);
},
@@ -242,7 +238,7 @@ class HrmsEmployeeDashboard extends Component {
this.state.calendarDateTo = this.formatDate(new Date(today.getFullYear(), 11, 31));
} else if (this.state.calendarView === "weekly") {
const weekStart = new Date(today);
- weekStart.setDate(weekStart.getDate() - weekStart.getDay());
+ weekStart.setDate(weekStart.getDate() - ((weekStart.getDay() + 6) % 7));
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
this.state.calendarDateFrom = this.formatDate(weekStart);
@@ -400,7 +396,6 @@ class HrmsEmployeeDashboard extends Component {
}
openExpenses(state = false) {
- debugger;
const domain = [["employee_id", "=", this.state.data.employee.id]];
if (state) {
domain.push(["state", "=", state.toLowerCase()]);
diff --git a/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml b/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml
index f94819b85..6631f230a 100644
--- a/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml
+++ b/addons_extensions/hrms_emp_dashboard/static/src/xml/hrms_emp_dashboard.xml
@@ -74,11 +74,14 @@