fix(timesheet): scoping whitelisted methods output to projects and timesheets that are acccessible to users (backport #58267) (#58577)

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
This commit is contained in:
mergify[bot]
2026-09-01 12:09:00 +00:00
committed by GitHub
parent 01fecfe093
commit 4f409e5ccf

View File

@@ -7,6 +7,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.functions import Date
from frappe.utils import flt, get_datetime, getdate from frappe.utils import flt, get_datetime, getdate
from frappe.utils.deprecations import deprecated from frappe.utils.deprecations import deprecated
@@ -303,45 +304,58 @@ class Timesheet(Document):
@frappe.whitelist() @frappe.whitelist()
def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to_time=None): def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to_time=None):
condition = "" tsd = frappe.qb.DocType("Timesheet Detail")
ts = frappe.qb.DocType("Timesheet")
allowed_timesheets = frappe.get_list("Timesheet", pluck="name")
allowed_projects = frappe.get_list("Project", pluck="name")
if not allowed_timesheets:
return []
query = (
frappe.qb.from_(tsd)
.inner_join(ts)
.on(ts.name == tsd.parent)
.select(
tsd.name.as_("name"),
tsd.parent.as_("time_sheet"),
tsd.from_time.as_("from_time"),
tsd.to_time.as_("to_time"),
tsd.billing_hours.as_("billing_hours"),
tsd.billing_amount.as_("billing_amount"),
tsd.activity_type.as_("activity_type"),
tsd.description.as_("description"),
ts.currency.as_("currency"),
tsd.project_name.as_("project_name"),
)
.where(
(tsd.parenttype == "Timesheet")
& (tsd.docstatus == 1)
& (tsd.is_billable == 1)
& tsd.sales_invoice.isnull()
& (tsd.parent.isin(allowed_timesheets))
& ((tsd.project.isin(allowed_projects)) | (tsd.project.isnull()))
)
)
if project: if project:
condition += "AND tsd.project = %(project)s " query = query.where(tsd.project == project)
if parent: if parent:
condition += "AND tsd.parent = %(parent)s " query = query.where(tsd.parent == parent)
if from_time and to_time: if from_time and to_time:
condition += "AND CAST(tsd.from_time as DATE) BETWEEN %(from_time)s AND %(to_time)s" query = query.where(Date(tsd.from_time).between(from_time, to_time))
query = f""" return query.orderby(tsd.from_time).run(as_dict=1)
SELECT
tsd.name as name,
tsd.parent as time_sheet,
tsd.from_time as from_time,
tsd.to_time as to_time,
tsd.billing_hours as billing_hours,
tsd.billing_amount as billing_amount,
tsd.activity_type as activity_type,
tsd.description as description,
ts.currency as currency,
tsd.project_name as project_name
FROM `tabTimesheet Detail` tsd
INNER JOIN `tabTimesheet` ts
ON ts.name = tsd.parent
WHERE
tsd.parenttype = 'Timesheet'
AND tsd.docstatus = 1
AND tsd.is_billable = 1
AND tsd.sales_invoice is NULL
{condition}
ORDER BY tsd.from_time ASC
"""
filters = {"project": project, "parent": parent, "from_time": from_time, "to_time": to_time}
return frappe.db.sql(query, filters, as_dict=1)
@frappe.whitelist() @frappe.whitelist()
def get_timesheet_detail_rate(timelog, currency): def get_timesheet_detail_rate(timelog, currency):
allowed_timesheets = frappe.get_list("Timesheet", pluck="name")
if not allowed_timesheets:
return 0.0
ts = frappe.qb.DocType("Timesheet") ts = frappe.qb.DocType("Timesheet")
ts_detail = frappe.qb.DocType("Timesheet Detail") ts_detail = frappe.qb.DocType("Timesheet Detail")
@@ -349,10 +363,20 @@ def get_timesheet_detail_rate(timelog, currency):
frappe.qb.from_(ts_detail) frappe.qb.from_(ts_detail)
.inner_join(ts) .inner_join(ts)
.on(ts.name == ts_detail.parent) .on(ts.name == ts_detail.parent)
.select(ts_detail.billing_amount.as_("billing_amount"), ts.currency.as_("currency")) .select(
.where(ts_detail.name == timelog) ts_detail.billing_amount.as_("billing_amount"),
ts.currency.as_("currency"),
ts.name.as_("timesheet"),
)
.where((ts_detail.name == timelog) & ts_detail.parent.isin(allowed_timesheets))
.limit(1)
.run(as_dict=1) .run(as_dict=1)
)[0] )
if not timelog_detail:
return 0.0
timelog_detail = timelog_detail[0]
if timelog_detail.currency: if timelog_detail.currency:
exchange_rate = get_exchange_rate(timelog_detail.currency, currency) exchange_rate = get_exchange_rate(timelog_detail.currency, currency)
@@ -367,33 +391,42 @@ def get_timesheet(doctype, txt, searchfield, start, page_len, filters):
if not filters: if not filters:
filters = {} filters = {}
condition = "" allowed_timesheets = frappe.get_list("Timesheet", pluck="name")
if filters.get("project"):
condition = "and tsd.project = %(project)s"
return frappe.db.sql( if not allowed_timesheets:
f"""select distinct tsd.parent from `tabTimesheet Detail` tsd, return []
`tabTimesheet` ts where
ts.status in ('Submitted', 'Payslip') and tsd.parent = ts.name and tsd = frappe.qb.DocType("Timesheet Detail")
tsd.docstatus = 1 and ts.total_billable_amount > 0 ts = frappe.qb.DocType("Timesheet")
and tsd.parent LIKE %(txt)s {condition}
order by tsd.parent limit %(page_len)s offset %(start)s""", query = (
{ frappe.qb.from_(tsd)
"txt": "%" + txt + "%", .inner_join(ts)
"start": start, .on(tsd.parent == ts.name)
"page_len": page_len, .select(tsd.parent)
"project": filters.get("project"), .distinct()
}, .where(
ts.status.isin(["Submitted", "Payslip"])
& (tsd.docstatus == 1)
& (ts.total_billable_amount > 0)
& tsd.parent.like(f"%{txt}%")
& tsd.parent.isin(allowed_timesheets)
)
) )
if filters.get("project"):
query = query.where(tsd.project == filters.get("project"))
return query.orderby(tsd.parent).limit(page_len).offset(start).run()
@frappe.whitelist() @frappe.whitelist()
def get_timesheet_data(name, project): def get_timesheet_data(name, project=None):
data = None data = None
if project and project != "": if project:
data = get_projectwise_timesheet_data(project, name) data = get_projectwise_timesheet_data(project, name)
else: else:
data = frappe.get_all( data = frappe.get_list(
"Timesheet", "Timesheet",
fields=[ fields=[
{"SUB": ["total_billable_amount", "total_billed_amount"], "as": "billing_amt"}, {"SUB": ["total_billable_amount", "total_billed_amount"], "as": "billing_amt"},