diff --git a/erpnext/venue/report/item_booking_rate/item_booking_rate.py b/erpnext/venue/report/item_booking_rate/item_booking_rate.py index 66c742de7720a11066e7c580ee373b6ecbb46d73..ce8e79df4c735e6943c0a55ac5d4b10a2c43a692 100644 --- a/erpnext/venue/report/item_booking_rate/item_booking_rate.py +++ b/erpnext/venue/report/item_booking_rate/item_booking_rate.py @@ -2,7 +2,7 @@ # For license information, please see license.txt from collections import defaultdict -from datetime import timedelta +from datetime import datetime, timedelta import frappe from frappe import _ @@ -10,7 +10,7 @@ from frappe.query_builder.functions import Sum from frappe.utils import flt, format_date, get_datetime, getdate from frappe.utils.dateutils import get_dates_from_timegrain -from erpnext.venue.doctype.item_booking.item_booking import get_item_calendar +ALL_DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] def execute(filters=None): @@ -33,24 +33,26 @@ def get_data(filters): "starts_on": ("between", filters.get("date_range")), "status": ("in", (f"{status_filter}")), }, - fields=["status", "name", "item", "item_name", "starts_on", "ends_on"], + fields=["status", "name", "item", "booking_resource", "starts_on", "ends_on"], ) items_dict = defaultdict(dict) + # items_capacity = defaultdict(list) item_list = list(set(ib.item for ib in item_booking)) + holidays = get_holidays(filters.get("company"), filters.get("date_range")) for item in item_list: - calendar, capacity_per_day, total_capacity = get_calendar_capacity( - filters.get("company"), filters.get("date_range"), item + capacity_per_day, total_capacity = get_calendar_capacity( + filters.get("date_range"), item, holidays ) - items_dict[item]["calendar"] = calendar.get("calendar") - items_dict[item]["calendar_name"] = calendar.get("name") + # items_capacity[item]["capacity_per_day"] = capacity_per_day items_dict[item]["capacity"] = total_capacity + # Get minutes booked for ib in item_booking: - if "item_name" not in items_dict[ib["item"]]: - items_dict[ib["item"]]["item_name"] = ib["item_name"] + if "booking_resource" not in items_dict[ib["item"]]: + items_dict[ib["item"]]["booking_resource"] = ib["booking_resource"] if "total" not in items_dict[ib["item"]]: items_dict[ib["item"]]["total"] = 0.0 @@ -71,24 +73,18 @@ def get_data(filters): ib["total_price"] = get_item_booking_price(ib.name) diff = timedelta(0) - capacity = 0.0 for date in get_dates_from_timegrain(ib.get("starts_on"), ib.get("ends_on")): - for line in items_dict[ib["item"]]["calendar"]: - if line.day == date.strftime("%A"): - schedule_start = get_datetime(date) + line.start_time - schedule_end = get_datetime(date) + line.end_time - booking_start = get_datetime(ib.get("starts_on")) - booking_end = get_datetime(ib.get("ends_on")) - if getdate(ib.get("starts_on")) != getdate(date): - booking_start = schedule_start - if getdate(ib.get("ends_on")) != getdate(date): - booking_end = schedule_end - - diff += booking_end - booking_start - capacity += line.get("capacity") + booking_start = get_datetime(ib.get("starts_on")) + booking_end = get_datetime(ib.get("ends_on")) + if getdate(ib.get("starts_on")) != getdate(date): + booking_start = get_datetime(date) + if getdate(ib.get("ends_on")) != getdate(date): + booking_end = get_datetime(date) + timedelta(days=1) + + diff += booking_end - booking_start ib["total"] = flt(diff.total_seconds() / 3600.0) - ib["capacity"] = capacity + if filters.show_billing: ib["average_price"] = ib["total_price"] / (ib["total"] or 1) ib["free_hours"] = ib["total"] if ib["total_price"] == 0 else 0.0 @@ -106,8 +102,11 @@ def get_data(filters): x["total_price"] for x in items_dict[item]["bookings"] ) / (items_dict[item]["total"] or 1) + for item in items_dict: + items_dict[item]["percent"] = (items_dict[item]["total"] / items_dict[item]["capacity"] if items_dict[item]["capacity"] else 1.0) *100.0 + sorted_list = sorted( - [{"item": f"{x}: {items_dict[x]['item_name']}", **items_dict[x]} for x in items_dict], + [{"item": f"{x}: {items_dict[x]['booking_resource']}", **items_dict[x]} for x in items_dict], key=lambda x: x["item"].lower(), ) @@ -116,8 +115,7 @@ def get_data(filters): for row in sorted_list: output.append(row) for booking in row.get("bookings"): - booking.item = None - booking.item_name = None + booking["item"] = None booking["item_booking"] = booking.name booking["booking_dates"] = ( f"{format_date(booking.starts_on)} - {format_date(booking.ends_on)}" @@ -125,19 +123,15 @@ def get_data(filters): else format_date(booking.starts_on) ) output.append(booking) - else: output = sorted_list - for line in output: - line["percent"] = (flt(line["total"]) / flt(line["capacity"]) if line["capacity"] else 1.0) * 100.0 - chart_data = get_chart_data(output) return output, chart_data -def get_calendar_capacity(company, date_range, item): +def get_holidays(company, date_range): holiday_list = frappe.get_cached_value("Company", company, "default_holiday_list") holidays = frappe.get_all( "Holiday", @@ -148,24 +142,32 @@ def get_calendar_capacity(company, date_range, item): pluck="holiday_date", ) - calendar = get_item_calendar(item) + return holidays + + +def get_calendar_capacity(date_range, item, holidays=None): + if not holidays: + holidays = [] - for line in calendar.get("calendar"): - line["capacity"] = (line.get("end_time") - line.get("start_time")).total_seconds() / 3600 + calendars = get_item_calendars(item) + + + for day in calendars: + start_time = calendars[day].get("start_time") or timedelta() + end_time = calendars[day].get("end_time") or timedelta(days=1) + calendars[day]["capacity"] = (end_time - start_time).total_seconds() / 3600 capacity_per_day = {} total_capacity = 0.0 for date in get_dates_from_timegrain(date_range[0], date_range[1]): if date not in holidays: - capacity_per_day[date] = sum( - flt(x.capacity) for x in calendar.get("calendar") if x["day"] == date.strftime("%A") - ) + capacity_per_day[date] = calendars[date.strftime("%A")]["capacity"] total_capacity += capacity_per_day[date] else: capacity_per_day[date] = 0.0 - return calendar, capacity_per_day, total_capacity + return capacity_per_day, total_capacity def get_item_booking_price(item_booking): @@ -184,17 +186,51 @@ def get_item_booking_price(item_booking): return flt(amounts[0][0]) +def get_item_calendars(item: str): + # Override the calendar with a custom one + daily_calendars = defaultdict(dict) + for hook in frappe.get_hooks("get_item_booking_calendar"): + calendar = frappe.call(hook, item=item) + if not calendar: + continue + if isinstance(calendar, str): + calendar = frappe.get_doc("Item Booking Calendar", calendar) + if calendar: + lines = calendar.get("booking_calendar") + for day in ALL_DAYS: + daily_calendars[day]["start_time"] = min(l.start_time for l in lines if l.day == day) + daily_calendars[day]["end_time"] = max(l.end_time for l in lines if l.day == day) + return daily_calendars + + for filters in [ + dict(item=item), + dict(uom=""), + ]: + lines = [] + if filtered_calendars := frappe.get_all( + "Item Booking Calendar", + fields=["name", "item", "uom"], + filters=filters, + limit=1, + ): + lines += frappe.get_all( + "Item Booking Calendars", + filters={"parent": filtered_calendars[0].name, "parenttype": "Item Booking Calendar"}, + fields=["start_time", "end_time", "day", "whole"], + ) + if lines: + for day in ALL_DAYS: + start_times = [l.start_time for l in lines if l.day == day] + daily_calendars[day]["start_time"] = min(start_times) if start_times else None + end_times = [l.end_time for l in lines if l.day == day] + daily_calendars[day]["end_time"] = max(end_times) if end_times else None + + return daily_calendars + def get_columns(filters): columns = [ {"label": _("Item"), "fieldtype": "Data", "fieldname": "item", "width": 300}, - { - "label": _("Reference Calendar"), - "fieldtype": "Link", - "fieldname": "calendar_name", - "options": "Item Booking Calendar", - "width": 250, - }, ] if filters.show_bookings: @@ -273,17 +309,18 @@ def get_columns(filters): def get_chart_data(data): + chart_data = [x for x in data if x.get("bookings")] return { "data": { - "labels": [x.get("item_name") for x in data if not x.get("item_booking")], + "labels": [x.get("booking_resource") for x in chart_data], "datasets": [ { "name": _("Capacity (Hours)"), - "values": [round(x.get("capacity"), 2) for x in data if not x.get("item_booking")], + "values": [round(x.get("capacity") or 0.0, 2) for x in chart_data], }, { "name": _("Bookings (Hours)"), - "values": [round(x.get("total"), 2) for x in data if not x.get("item_booking")], + "values": [round(x.get("total") or 0.0, 2) for x in chart_data], }, ], },