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)