feat(invoice): implement atomic invoice tools (REFACTOR-3)

- Add create_invoice_draft, add_invoice_line, update_invoice_line, delete_invoice_line, set_invoice_project, validate_invoice tools
- Update DolibarrClient with corresponding methods
- Add tests for atomic invoice operations
- Update development docs with venv instructions
This commit is contained in:
Benju1
2025-12-05 06:08:10 +01:00
parent 2035da88ce
commit b059d9c40e
4 changed files with 341 additions and 5 deletions

View File

@@ -430,6 +430,44 @@ class DolibarrClient:
async def delete_invoice(self, invoice_id: int) -> Dict[str, Any]:
"""Delete an invoice."""
return await self.request("DELETE", f"invoices/{invoice_id}")
async def add_invoice_line(
self,
invoice_id: int,
data: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Dict[str, Any]:
"""Add a line to an invoice."""
payload = self._merge_payload(data, **kwargs)
# Map product_id to fk_product if present
if "product_id" in payload:
payload["fk_product"] = payload.pop("product_id")
return await self.request("POST", f"invoices/{invoice_id}/lines", data=payload)
async def update_invoice_line(
self,
invoice_id: int,
line_id: int,
data: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Dict[str, Any]:
"""Update a line in an invoice."""
payload = self._merge_payload(data, **kwargs)
return await self.request("PUT", f"invoices/{invoice_id}/lines/{line_id}", data=payload)
async def delete_invoice_line(self, invoice_id: int, line_id: int) -> Dict[str, Any]:
"""Delete a line from an invoice."""
return await self.request("DELETE", f"invoices/{invoice_id}/lines/{line_id}")
async def validate_invoice(self, invoice_id: int, warehouse_id: int = 0, not_trigger: int = 0) -> Dict[str, Any]:
"""Validate an invoice."""
payload = {
"idwarehouse": warehouse_id,
"not_trigger": not_trigger
}
return await self.request("POST", f"invoices/{invoice_id}/validate", data=payload)
# ============================================================================
# ORDER MANAGEMENT

View File

@@ -596,6 +596,167 @@ async def handle_list_tools():
},
),
Tool(
name="create_invoice_draft",
description=(
"Create a new invoice draft (header only). "
"Use this to start a new invoice, then use add_invoice_line to add items. "
"Returns the new invoice_id."
),
inputSchema={
"type": "object",
"properties": {
"customer_id": {
"type": "integer",
"description": "Customer ID (Dolibarr socid)",
},
"date": {
"type": "string",
"description": "Invoice date (YYYY-MM-DD)",
},
"project_id": {
"type": "integer",
"description": "Linked project ID (optional)",
},
},
"required": ["customer_id", "date"],
"additionalProperties": False,
},
),
Tool(
name="add_invoice_line",
description="Add a line item to an existing draft invoice.",
inputSchema={
"type": "object",
"properties": {
"invoice_id": {
"type": "integer",
"description": "Invoice ID",
},
"desc": {
"type": "string",
"description": "Line description",
},
"qty": {
"type": "number",
"description": "Quantity",
},
"subprice": {
"type": "number",
"description": "Unit price (net)",
},
"product_id": {
"type": "integer",
"description": "Product ID (optional)",
},
"product_type": {
"type": "integer",
"description": "Type (0=Product, 1=Service)",
"default": 0,
},
"vat": {
"type": "number",
"description": "VAT rate (optional)",
},
},
"required": ["invoice_id", "desc", "qty", "subprice"],
"additionalProperties": False,
},
),
Tool(
name="update_invoice_line",
description="Update an existing line in a draft invoice.",
inputSchema={
"type": "object",
"properties": {
"invoice_id": {
"type": "integer",
"description": "Invoice ID",
},
"line_id": {
"type": "integer",
"description": "Line ID to update",
},
"desc": {
"type": "string",
"description": "New description",
},
"qty": {
"type": "number",
"description": "New quantity",
},
"subprice": {
"type": "number",
"description": "New unit price",
},
"vat": {
"type": "number",
"description": "New VAT rate",
},
},
"required": ["invoice_id", "line_id"],
"additionalProperties": False,
},
),
Tool(
name="delete_invoice_line",
description="Delete a line from a draft invoice.",
inputSchema={
"type": "object",
"properties": {
"invoice_id": {
"type": "integer",
"description": "Invoice ID",
},
"line_id": {
"type": "integer",
"description": "Line ID to delete",
},
},
"required": ["invoice_id", "line_id"],
"additionalProperties": False,
},
),
Tool(
name="set_invoice_project",
description="Link an invoice to a project.",
inputSchema={
"type": "object",
"properties": {
"invoice_id": {
"type": "integer",
"description": "Invoice ID",
},
"project_id": {
"type": "integer",
"description": "Project ID",
},
},
"required": ["invoice_id", "project_id"],
"additionalProperties": False,
},
),
Tool(
name="validate_invoice",
description="Validate a draft invoice (change status to unpaid).",
inputSchema={
"type": "object",
"properties": {
"invoice_id": {
"type": "integer",
"description": "Invoice ID",
},
"warehouse_id": {
"type": "integer",
"description": "Warehouse ID for stock decrease (optional)",
"default": 0,
},
},
"required": ["invoice_id"],
"additionalProperties": False,
},
),
# Order Management CRUD
Tool(
name="get_orders",
@@ -1099,6 +1260,40 @@ async def handle_call_tool(name: str, arguments: dict):
elif name == "delete_invoice":
result = await client.delete_invoice(arguments['invoice_id'])
elif name == "create_invoice_draft":
# Map customer_id to socid for the API
if "customer_id" in arguments:
arguments["socid"] = arguments.pop("customer_id")
# Map project_id to fk_project if present
if "project_id" in arguments:
arguments["fk_project"] = arguments.pop("project_id")
result = await client.create_invoice(**arguments)
elif name == "add_invoice_line":
invoice_id = arguments.pop("invoice_id")
result = await client.add_invoice_line(invoice_id, **arguments)
elif name == "update_invoice_line":
invoice_id = arguments.pop("invoice_id")
line_id = arguments.pop("line_id")
result = await client.update_invoice_line(invoice_id, line_id, **arguments)
elif name == "delete_invoice_line":
invoice_id = arguments.pop("invoice_id")
line_id = arguments.pop("line_id")
result = await client.delete_invoice_line(invoice_id, line_id)
elif name == "set_invoice_project":
invoice_id = arguments.pop("invoice_id")
project_id = arguments.pop("project_id")
result = await client.update_invoice(invoice_id, fk_project=project_id)
elif name == "validate_invoice":
invoice_id = arguments.pop("invoice_id")
result = await client.validate_invoice(invoice_id, **arguments)
# Order Management
elif name == "get_orders":