diff --git a/bwh_bot/handlers/wfh.py b/bwh_bot/handlers/wfh.py
index 6d54aae..fca01b8 100644
--- a/bwh_bot/handlers/wfh.py
+++ b/bwh_bot/handlers/wfh.py
@@ -136,14 +136,16 @@ def _summary_buttons(self, data):
[InlineKeyboardButton("Confirm & Submit", callback_data=f"{p}:confirm")],
] + nav_buttons(p)
+ def _total_wfh_days(self, data):
+ days = frappe.utils.date_diff(data["to_date"], data["from_date"]) + 1
+ if data.get("half_day"):
+ days -= 0.5
+ return days
+
def _show_summary(self, state, chat_id, message_id):
data = self.get_data(state)
- days = frappe.utils.date_diff(data["to_date"], data["from_date"]) + 1
half_day = data.get("half_day", False)
-
- half_day_text = ""
- if half_day:
- half_day_text = f"\nHalf Day: Yes ({data['from_date']})"
+ total_wfh_days = self._total_wfh_days(data)
edit_message_text(
chat_id, message_id,
@@ -151,8 +153,8 @@ def _show_summary(self, state, chat_id, message_id):
f"Work From Home Summary\n\n"
f"From: {data['from_date']}\n"
f"To: {data['to_date']}\n"
- f"Days: {days}"
- f"{half_day_text}\n\n"
+ f"Type: {'Half Day' if half_day else 'Full Day'}\n"
+ f"Total WFH Days: {total_wfh_days:g}\n\n"
f"Confirm and submit?"
),
parse_mode="HTML",
@@ -165,16 +167,19 @@ def _handle_confirm(self, state, ctx):
chat_id, message_id, cqid = ctx["chat_id"], ctx["message_id"], ctx["callback_query_id"]
try:
+ total_wfh_days = self._total_wfh_days(data)
+
+ # Capture the WFH days in the custom field instead of ticking the
+ # Half Day checkbox — that checkbox would split the day into half
+ # present / half absent in attendance, which we don't want for WFH.
doc_data = {
"doctype": "Attendance Request",
"employee": data["employee"],
"from_date": data["from_date"],
"to_date": data["to_date"],
"reason": "Work From Home",
+ "custom_total_wfh_days": total_wfh_days,
}
- if data.get("half_day"):
- doc_data["half_day"] = 1
- doc_data["half_day_date"] = data["from_date"]
frappe.db.savepoint("before_attendance_request")
doc = frappe.get_doc(doc_data)
@@ -191,6 +196,7 @@ def _handle_confirm(self, state, ctx):
f"ID: {doc.name}\n"
f"From: {data['from_date']}\n"
f"To: {data['to_date']}\n"
+ f"Total WFH Days: {total_wfh_days:g}\n"
f"Status: Submitted\n"
),
parse_mode="HTML",
diff --git a/bwh_bot/hooks.py b/bwh_bot/hooks.py
index 2240a43..e9369b2 100644
--- a/bwh_bot/hooks.py
+++ b/bwh_bot/hooks.py
@@ -86,7 +86,11 @@
# ------------
# before_install = "bwh_bot.install.before_install"
-# after_install = "bwh_bot.install.after_install"
+after_install = "bwh_bot.install.after_install"
+
+# Migration
+# ---------
+after_migrate = "bwh_bot.install.after_migrate"
# Uninstallation
# ------------
@@ -141,7 +145,19 @@
# Scheduled Tasks
# ---------------
+# Cron times are evaluated in the server's timezone.
scheduler_events = {
+ "cron": {
+ # 10:30 AM — who is on leave and who is working from home today
+ "30 10 * * *": [
+ "bwh_bot.tasks.send_daily_leave_notification",
+ "bwh_bot.tasks.send_daily_wfh_notification",
+ ],
+ # 3:00 PM — second WFH reminder for the day
+ "0 15 * * *": [
+ "bwh_bot.tasks.send_daily_wfh_notification",
+ ],
+ },
"monthly": [
"bwh_bot.tasks.create_monthly_petty_cash_journal_entry"
],
diff --git a/bwh_bot/install.py b/bwh_bot/install.py
new file mode 100644
index 0000000..4a5dae9
--- /dev/null
+++ b/bwh_bot/install.py
@@ -0,0 +1,32 @@
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+CUSTOM_FIELDS = {
+ "Attendance Request": [
+ {
+ "fieldname": "custom_total_wfh_days",
+ "label": "Total WFH Days",
+ "fieldtype": "Float",
+ "precision": "1",
+ "insert_after": "half_day_date",
+ "description": (
+ "Total Work From Home days captured by BWH Bot. "
+ "For a half day this is 0.5; for a multi-day request it is the number of days. "
+ "This is used instead of the Half Day checkbox so WFH is not split into "
+ "half present / half absent in attendance."
+ ),
+ }
+ ],
+}
+
+
+def after_install():
+ _make_custom_fields()
+
+
+def after_migrate():
+ _make_custom_fields()
+
+
+def _make_custom_fields():
+ create_custom_fields(CUSTOM_FIELDS, ignore_validate=True)
diff --git a/bwh_bot/tasks.py b/bwh_bot/tasks.py
index a973532..0b83938 100644
--- a/bwh_bot/tasks.py
+++ b/bwh_bot/tasks.py
@@ -1,6 +1,71 @@
import frappe
from frappe.utils import add_months, get_first_day, get_last_day, today
+from bwh_bot.telegram_utils import broadcast_to_whitelisted_chats
+
+
+def _format_date_range(from_date, to_date):
+ from_str = frappe.utils.formatdate(from_date, "d MMM")
+ if str(from_date) == str(to_date):
+ return from_str
+ return f"{from_str} → {frappe.utils.formatdate(to_date, 'd MMM')}"
+
+
+def send_daily_leave_notification():
+ """Daily cron: notify whitelisted chats about employees on approved leave today."""
+ current_day = today()
+ leaves = frappe.get_all(
+ "Leave Application",
+ filters={
+ "docstatus": 1,
+ "status": "Approved",
+ "from_date": ["<=", current_day],
+ "to_date": [">=", current_day],
+ },
+ fields=["employee_name", "leave_type", "from_date", "to_date", "half_day"],
+ order_by="employee_name asc",
+ )
+
+ if not leaves:
+ return
+
+ lines = []
+ for leave in leaves:
+ date_range = _format_date_range(leave.from_date, leave.to_date)
+ half_day_text = " · Half Day" if leave.half_day else ""
+ lines.append(f"• {leave.employee_name} — {leave.leave_type} ({date_range}){half_day_text}")
+
+ message = "🌴 On Leave Today\n\n" + "\n".join(lines)
+ broadcast_to_whitelisted_chats(message)
+
+
+def send_daily_wfh_notification():
+ """Daily cron: notify whitelisted chats about employees working from home today."""
+ current_day = today()
+ requests = frappe.get_all(
+ "Attendance Request",
+ filters={
+ "docstatus": 1,
+ "reason": "Work From Home",
+ "from_date": ["<=", current_day],
+ "to_date": [">=", current_day],
+ },
+ fields=["employee_name", "from_date", "to_date", "custom_total_wfh_days"],
+ order_by="employee_name asc",
+ )
+
+ if not requests:
+ return
+
+ lines = []
+ for req in requests:
+ date_range = _format_date_range(req.from_date, req.to_date)
+ half_day_text = " · Half Day" if req.custom_total_wfh_days == 0.5 else ""
+ lines.append(f"• {req.employee_name} ({date_range}){half_day_text}")
+
+ message = "🏠 Working From Home Today\n\n" + "\n".join(lines)
+ broadcast_to_whitelisted_chats(message)
+
def create_monthly_petty_cash_journal_entry():
"""Monthly cron: aggregate previous month's petty cash usage into a draft Journal Entry."""
diff --git a/bwh_bot/telegram_utils.py b/bwh_bot/telegram_utils.py
index e750db9..23e2595 100644
--- a/bwh_bot/telegram_utils.py
+++ b/bwh_bot/telegram_utils.py
@@ -60,6 +60,16 @@ def set_message_reaction(chat_id, message_id, emoji="👍"):
)
+def broadcast_to_whitelisted_chats(text, parse_mode="HTML"):
+ """Send a message to every whitelisted chat, logging (not raising) on failure."""
+ settings = frappe.get_single("BWH Bot Settings")
+ for chat in settings.whitelisted_chats:
+ try:
+ send_message(chat.chat_id, text, parse_mode=parse_mode)
+ except Exception:
+ frappe.log_error(f"Failed to send Telegram notification to chat {chat.chat_id}")
+
+
def is_whitelisted(chat_id):
settings = frappe.get_single("BWH Bot Settings")
chat_id = str(chat_id)