Skip to content

WIP: get current sprint tasks and add hours to the timesheet #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions src/odoo_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,36 @@ class SearchHolidaysResponse(BaseModel):
error: Optional[str] = Field(default=None, description="Error message, if any")


class Task(BaseModel):
"""Represents a task."""

id: int = Field(description="Task ID")
name: str = Field(description="Task name")
project_id: List[Union[int, str]] = Field(description="Project ID and name")
stage_id: List[Union[int, str]] = Field(description="Stage ID and name")
planned_hours: float = Field(description="Planned hours for the task")
effective_hours: float = Field(description="Effective hours spent on the task")
remaining_hours: float = Field(description="Remaining hours for the task")


class SprintTasksResponse(BaseModel):
"""Response for get_current_sprint_tasks"""

success: bool = Field(description="Indicates if the request was successful.")
tasks: Optional[List[Task]] = Field(
default=None, description="Current sprint tasks"
)
error: Optional[str] = Field(default=None, description="Error message if any.")


class AddTimesheetEntryResponse(BaseModel):
"""Response for add_timesheet_entry"""

success: bool = Field(description="Indicates if the request was successful.")
date: str = Field(description="The date for which the timesheet entry was added.")
error: Optional[str] = Field(default=None, description="Error message if any.")


# ----- MCP Tools -----


Expand Down Expand Up @@ -442,3 +472,93 @@ def search_holidays(

except Exception as e:
return SearchHolidaysResponse(success=False, error=str(e))


@mcp.tool(description="Get the tasks of the current sprint for the calling user.")
def get_current_sprint_tasks(ctx: Context) -> SprintTasksResponse:
"""
Retrieves tasks for the current sprint and the user making the call.
"""
odoo = ctx.request_context.lifespan_context.odoo
user_id = odoo.uid

if not user_id:
return SprintTasksResponse(success=False, error="User ID not found in context.")

try:
user_id_int = int(user_id)
except ValueError:
return SprintTasksResponse(success=False, error=f"Invalid user ID: {user_id}")
# Domain based on the provided example in sprint_tasks.txt
domain = [
"&",
"&",
["project_id.name", "ilike", "EN CURSO"],
["user_ids", "in", user_id_int],
"|",
"|",
["stage_id", "=", 963], # Sprint in progress
["stage_id", "=", 962], # Sprint Backlog
["stage_id", "=", 966], # Sprint blocked
]

try:
tasks = odoo.search_read(model_name="project.task", domain=domain)
parsed_tasks = [Task(**task) for task in tasks]
return SprintTasksResponse(success=True, tasks=parsed_tasks)

except Exception as e:
return SprintTasksResponse(success=False, error=str(e))


@mcp.tool(description="Add a timesheet entry to a task.")
def add_timesheet_entry(
ctx: Context, task_id: int, unit_amount: float, date: Optional[str] = None
) -> AddTimesheetEntryResponse:
"""
Adds a timesheet entry to a specified task.

Parameters:
task_id: The ID of the task to add time to.
unit_amount: Amount of time to add, in hours. Can be fractional.
date: Optional date in YYYY-MM-DD format. If not provided, defaults to the current day.

Returns:
AddTimesheetEntryResponse: Object containing success status, the date used, and error message if any.
"""
odoo = ctx.request_context.lifespan_context.odoo
model = "account.analytic.line"
method = "adjust_grid"

# Use current date if date is not provided
if date is None:
date_dt = datetime.now()
date = date_dt.strftime("%Y-%m-%d")
else:
# Validate date format
try:
date_dt = datetime.strptime(date, "%Y-%m-%d")
except ValueError:
return AddTimesheetEntryResponse(
# Return empty string for date on error
success=False,
date="",
error="Invalid date format. Use YYYY-MM-DD.",
)

# Prepare arguments for adjust_grid method
args = [
[],
[["task_id", "=", task_id]], # domain
"date",
f"{date}/{date}",
"unit_amount",
unit_amount,
]

try:
odoo.execute_method(model, method, *args)
# Return the date used
return AddTimesheetEntryResponse(success=True, date=date)
except Exception as e:
return AddTimesheetEntryResponse(success=False, date=date, error=str(e))