diff --git a/src/dolibarr_mcp/dolibarr_client.py b/src/dolibarr_mcp/dolibarr_client.py index 09fac6d..d5ee24a 100644 --- a/src/dolibarr_mcp/dolibarr_client.py +++ b/src/dolibarr_mcp/dolibarr_client.py @@ -405,6 +405,15 @@ class DolibarrClient: if "customer_id" in payload and "socid" not in payload: payload["socid"] = payload.pop("customer_id") + # Fix: Map product_id to fk_product in lines + if "lines" in payload and isinstance(payload["lines"], list): + for line in payload["lines"]: + if "product_id" in line: + line["fk_product"] = line.pop("product_id") + # Ensure product_type is passed if present (0=Product, 1=Service) + if "product_type" in line: + line["product_type"] = line["product_type"] + result = await self.request("POST", "invoices", data=payload) return self._extract_identifier(result) diff --git a/src/dolibarr_mcp/dolibarr_mcp_server.py b/src/dolibarr_mcp/dolibarr_mcp_server.py index e992d11..a1989b4 100644 --- a/src/dolibarr_mcp/dolibarr_mcp_server.py +++ b/src/dolibarr_mcp/dolibarr_mcp_server.py @@ -364,7 +364,9 @@ async def handle_list_tools(): "subprice": {"type": "number", "description": "Unit price"}, "total_ht": {"type": "number", "description": "Total excluding tax"}, "total_ttc": {"type": "number", "description": "Total including tax"}, - "vat": {"type": "number", "description": "VAT rate"} + "vat": {"type": "number", "description": "VAT rate"}, + "product_id": {"type": "integer", "description": "Product ID to link (optional)"}, + "product_type": {"type": "integer", "description": "Type of line (0=Product, 1=Service)"} }, "required": ["desc", "qty", "subprice"] } diff --git a/tests/test_crud_operations.py b/tests/test_crud_operations.py index fde7a5a..7724087 100644 --- a/tests/test_crud_operations.py +++ b/tests/test_crud_operations.py @@ -140,6 +140,52 @@ class TestCRUDOperations: mock_request.return_value = {"success": True} result = await client.delete_invoice(100) assert result["success"] is True + + @pytest.mark.asyncio + async def test_invoice_creation_with_product_lines(self, client): + """Test invoice creation with linked product lines.""" + with patch.object(client, 'request') as mock_request: + mock_request.return_value = {"id": 101} + + # Create invoice with product link and service type + await client.create_invoice({ + "socid": 1, + "lines": [ + { + "desc": "Linked Product", + "qty": 1, + "subprice": 50, + "product_id": 10, + "product_type": 0 + }, + { + "desc": "Service Line", + "qty": 2, + "subprice": 100, + "product_type": 1 + } + ] + }) + + # Verify payload transformation + call_args = mock_request.call_args + assert call_args is not None + method, endpoint = call_args[0] + kwargs = call_args[1] + + assert method == "POST" + assert endpoint == "invoices" + + payload = kwargs["data"] + lines = payload["lines"] + + # Check first line (Product) + assert lines[0]["fk_product"] == 10 + assert "product_id" not in lines[0] + assert lines[0]["product_type"] == 0 + + # Check second line (Service) + assert lines[1]["product_type"] == 1 # Order CRUD Tests