mirror of
https://github.com/latinogino/dolibarr-mcp.git
synced 2026-04-23 02:05:35 +02:00
Add project management tools (CRUD + Search)
This commit is contained in:
@@ -510,6 +510,43 @@ class DolibarrClient:
|
||||
"""Delete a contact."""
|
||||
return await self.request("DELETE", f"contacts/{contact_id}")
|
||||
|
||||
# ============================================================================
|
||||
# PROJECT MANAGEMENT
|
||||
# ============================================================================
|
||||
|
||||
async def get_projects(self, limit: int = 100, page: int = 1, status: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
"""Get list of projects."""
|
||||
params: Dict[str, Any] = {"limit": limit, "page": page}
|
||||
if status is not None:
|
||||
params["status"] = status
|
||||
result = await self.request("GET", "projects", params=params)
|
||||
return result if isinstance(result, list) else []
|
||||
|
||||
async def get_project_by_id(self, project_id: int) -> Dict[str, Any]:
|
||||
"""Get specific project by ID."""
|
||||
return await self.request("GET", f"projects/{project_id}")
|
||||
|
||||
async def search_projects(self, sqlfilters: str, limit: int = 20) -> List[Dict[str, Any]]:
|
||||
"""Search projects using SQL filters."""
|
||||
params = {"limit": limit, "sqlfilters": sqlfilters}
|
||||
result = await self.request("GET", "projects", params=params)
|
||||
return result if isinstance(result, list) else []
|
||||
|
||||
async def create_project(self, data: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
|
||||
"""Create a new project."""
|
||||
payload = self._merge_payload(data, **kwargs)
|
||||
result = await self.request("POST", "projects", data=payload)
|
||||
return self._extract_identifier(result)
|
||||
|
||||
async def update_project(self, project_id: int, data: Optional[Dict[str, Any]] = None, **kwargs) -> Dict[str, Any]:
|
||||
"""Update an existing project."""
|
||||
payload = self._merge_payload(data, **kwargs)
|
||||
return await self.request("PUT", f"projects/{project_id}", data=payload)
|
||||
|
||||
async def delete_project(self, project_id: int) -> Dict[str, Any]:
|
||||
"""Delete a project."""
|
||||
return await self.request("DELETE", f"projects/{project_id}")
|
||||
|
||||
# ============================================================================
|
||||
# RAW API CALL
|
||||
# ============================================================================
|
||||
|
||||
@@ -535,6 +535,89 @@ async def handle_list_tools():
|
||||
"additionalProperties": False
|
||||
}
|
||||
),
|
||||
|
||||
# Project Management CRUD
|
||||
Tool(
|
||||
name="get_projects",
|
||||
description="Get list of projects from Dolibarr",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {"type": "integer", "description": "Maximum number of projects to return (default: 100)", "default": 100},
|
||||
"page": {"type": "integer", "description": "Page number for pagination (default: 1)", "default": 1},
|
||||
"status": {"type": "integer", "description": "Project status filter (e.g. 0=draft, 1=open, 2=closed)", "default": 1}
|
||||
},
|
||||
"additionalProperties": False
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="get_project_by_id",
|
||||
description="Get specific project details by ID",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": {"type": "integer", "description": "Project ID to retrieve"}
|
||||
},
|
||||
"required": ["project_id"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="search_projects",
|
||||
description="Search projects by reference or title",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search term for project ref or title"},
|
||||
"limit": {"type": "integer", "description": "Maximum number of results", "default": 20}
|
||||
},
|
||||
"required": ["query"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="create_project",
|
||||
description="Create a new project",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ref": {"type": "string", "description": "Project reference (optional, if Dolibarr auto-generates)"},
|
||||
"title": {"type": "string", "description": "Project title"},
|
||||
"description": {"type": "string", "description": "Project description"},
|
||||
"socid": {"type": "integer", "description": "Linked customer ID (thirdparty)"},
|
||||
"status": {"type": "integer", "description": "Project status (e.g. 1=open)", "default": 1}
|
||||
},
|
||||
"required": ["title"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="update_project",
|
||||
description="Update an existing project",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": {"type": "integer", "description": "Project ID to update"},
|
||||
"title": {"type": "string", "description": "Project title"},
|
||||
"description": {"type": "string", "description": "Project description"},
|
||||
"status": {"type": "integer", "description": "Project status"}
|
||||
},
|
||||
"required": ["project_id"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="delete_project",
|
||||
description="Delete a project",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": {"type": "integer", "description": "Project ID to delete"}
|
||||
},
|
||||
"required": ["project_id"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
),
|
||||
|
||||
# Raw API Access
|
||||
Tool(
|
||||
@@ -725,6 +808,33 @@ async def handle_call_tool(name: str, arguments: dict):
|
||||
elif name == "delete_contact":
|
||||
result = await client.delete_contact(arguments['contact_id'])
|
||||
|
||||
# Project Management
|
||||
elif name == "get_projects":
|
||||
result = await client.get_projects(
|
||||
limit=arguments.get("limit", 100),
|
||||
page=arguments.get("page", 1),
|
||||
status=arguments.get("status")
|
||||
)
|
||||
|
||||
elif name == "get_project_by_id":
|
||||
result = await client.get_project_by_id(arguments["project_id"])
|
||||
|
||||
elif name == "search_projects":
|
||||
query = _escape_sqlfilter(arguments["query"])
|
||||
limit = arguments.get("limit", 20)
|
||||
sqlfilters = f"((t.ref:like:'%{query}%') OR (t.title:like:'%{query}%'))"
|
||||
result = await client.search_projects(sqlfilters=sqlfilters, limit=limit)
|
||||
|
||||
elif name == "create_project":
|
||||
result = await client.create_project(**arguments)
|
||||
|
||||
elif name == "update_project":
|
||||
project_id = arguments.pop("project_id")
|
||||
result = await client.update_project(project_id, **arguments)
|
||||
|
||||
elif name == "delete_project":
|
||||
result = await client.delete_project(arguments["project_id"])
|
||||
|
||||
# Raw API Access
|
||||
elif name == "dolibarr_raw_api":
|
||||
result = await client.dolibarr_raw_api(**arguments)
|
||||
|
||||
112
tests/test_project_operations.py
Normal file
112
tests/test_project_operations.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Project Management Integration Tests for Dolibarr MCP Server.
|
||||
|
||||
These tests verify complete CRUD operations for Dolibarr projects.
|
||||
Run with: pytest tests/test_project_operations.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock, patch, AsyncMock
|
||||
|
||||
# Add src to path for imports
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||
|
||||
from dolibarr_mcp import DolibarrClient, Config
|
||||
|
||||
|
||||
class TestProjectOperations:
|
||||
"""Test complete CRUD operations for Dolibarr projects."""
|
||||
|
||||
@pytest.fixture
|
||||
def config(self):
|
||||
"""Create a test configuration."""
|
||||
return Config(
|
||||
dolibarr_url="https://test.dolibarr.com",
|
||||
dolibarr_api_key="test_api_key",
|
||||
log_level="INFO"
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, config):
|
||||
"""Create a test client instance."""
|
||||
return DolibarrClient(config)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_project_crud_lifecycle(self, client):
|
||||
"""Test complete project CRUD lifecycle."""
|
||||
with patch.object(client, 'request') as mock_request:
|
||||
# Create
|
||||
mock_request.return_value = {"id": 200}
|
||||
project_id = await client.create_project({
|
||||
"title": "New Website",
|
||||
"description": "Website redesign project",
|
||||
"socid": 1,
|
||||
"status": 1
|
||||
})
|
||||
assert project_id == 200
|
||||
|
||||
# Read
|
||||
mock_request.return_value = {
|
||||
"id": 200,
|
||||
"ref": "PJ2401-001",
|
||||
"title": "New Website",
|
||||
"description": "Website redesign project"
|
||||
}
|
||||
project = await client.get_project_by_id(200)
|
||||
assert project["title"] == "New Website"
|
||||
assert project["ref"] == "PJ2401-001"
|
||||
|
||||
# Update
|
||||
mock_request.return_value = {"id": 200, "title": "Updated Website Project"}
|
||||
result = await client.update_project(200, {"title": "Updated Website Project"})
|
||||
assert result["title"] == "Updated Website Project"
|
||||
|
||||
# Delete
|
||||
mock_request.return_value = {"success": True}
|
||||
result = await client.delete_project(200)
|
||||
assert result["success"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_projects(self, client):
|
||||
"""Test searching projects."""
|
||||
with patch.object(client, 'request') as mock_request:
|
||||
mock_request.return_value = [
|
||||
{"id": 200, "ref": "PJ2401-001", "title": "Website Redesign"},
|
||||
{"id": 201, "ref": "PJ2401-002", "title": "Mobile App"}
|
||||
]
|
||||
|
||||
# Search by query
|
||||
results = await client.search_projects(sqlfilters="(t.title:like:'%Website%')", limit=10)
|
||||
|
||||
assert len(results) == 2
|
||||
assert results[0]["title"] == "Website Redesign"
|
||||
|
||||
# Verify call arguments
|
||||
call_args = mock_request.call_args
|
||||
assert call_args is not None
|
||||
method, endpoint = call_args[0]
|
||||
kwargs = call_args[1]
|
||||
|
||||
assert method == "GET"
|
||||
assert endpoint == "projects"
|
||||
assert kwargs["params"]["sqlfilters"] == "(t.title:like:'%Website%')"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_projects_with_filters(self, client):
|
||||
"""Test getting projects with status filter."""
|
||||
with patch.object(client, 'request') as mock_request:
|
||||
mock_request.return_value = []
|
||||
|
||||
await client.get_projects(limit=50, page=2, status=1)
|
||||
|
||||
call_args = mock_request.call_args
|
||||
kwargs = call_args[1]
|
||||
params = kwargs["params"]
|
||||
|
||||
assert params["limit"] == 50
|
||||
assert params["page"] == 2
|
||||
assert params["status"] == 1
|
||||
Reference in New Issue
Block a user