Repository: rogers-obrien-rad/ProPyCore (language: python)

Issue:
Update Direct Cost item
This task is a component of User Story #69 

### Description
[Update a direct cost item](https://developers.procore.com/reference/rest/v1/direct-costs?version=1.1#update-direct-cost-item) with a focus on the attachments since that has a real use case at RO.


Discussion / hints:

Fix patch:
diff --git a/ProPyCore/access/base.py b/ProPyCore/access/base.py
index 4adc421..1b5ee52 100644
--- a/ProPyCore/access/base.py
+++ b/ProPyCore/access/base.py
@@ -59,7 +59,7 @@ def get_request(self, api_url, additional_headers=None, params=None):
 
     def post_request(self, api_url, additional_headers=None, params=None, data=None, files=None):
         """
-        Create a HTTP Get request
+        Create a HTTP Post request
 
         Parameters
         ----------
@@ -75,7 +75,7 @@ def post_request(self, api_url, additional_headers=None, params=None, data=None,
         Returns
         -------
         response : HTTP response object
-            GET response details in json
+            POST response details in json
         """
         # Get URL
         if params is None:
@@ -94,19 +94,22 @@ def post_request(self, api_url, additional_headers=None, params=None, data=None,
             response = requests.post(
                 url,
                 headers=headers,
-                json=data # use json for folder creation
+                data=data 
             )
-        else:
+        elif data is None:
             response = requests.post(
                 url,
                 headers=headers,
-                data=data, # use data for file creation
-                files=files
+                files=files  # use files for multipart/form-data
             )
-        
+        else:
+            response = requests.request("POST", url, headers=headers, data=data, files=files)
+
         if response.ok:
             return response.json()
         else:
+            print("Response Status Code:", response.status_code)
+            print("Response Text:", response.text)
             raise_exception(response)
 
     def patch_request(self, api_url, additional_headers=None, params=None, data=None, files=False):
diff --git a/ProPyCore/access/budgets/__init__.py b/ProPyCore/access/budgets/__init__.py
index dd83aee..765f9c8 100644
--- a/ProPyCore/access/budgets/__init__.py
+++ b/ProPyCore/access/budgets/__init__.py
@@ -1,9 +1,11 @@
 from .views import BudgetViews
 from .columns import BudgetColumns
 from .rows import BudgetRows
+from .details import BudgetDetails
 
 class Budgets:
     def __init__(self, access_token, server_url):
         self.views = BudgetViews(access_token, server_url)
         self.columns = BudgetColumns(access_token, server_url)
-        self.rows = BudgetRows(access_token, server_url)
\ No newline at end of file
+        self.rows = BudgetRows(access_token, server_url)
+        self.details = BudgetDetails(access_token, server_url)
\ No newline at end of file
diff --git a/ProPyCore/access/direct_costs.py b/ProPyCore/access/direct_costs.py
index 71ba2dc..b66a805 100644
--- a/ProPyCore/access/direct_costs.py
+++ b/ProPyCore/access/direct_costs.py
@@ -1,5 +1,8 @@
+import json
+import mimetypes
+
 from .base import Base
-from ..exceptions import NotFoundItemError
+from ..exceptions import NotFoundItemError, raise_exception
 
 class DirectCosts(Base):
     """
@@ -96,8 +99,19 @@ def find(self, company_id, project_id, identifier):
         direct_cost_info : dict
             Direct Cost data
         """
+        if isinstance(identifier, int):
+            key = "id"
+        else:
+            key = "invoice_number"
+
         for direct_cost in self.get(company_id=company_id, project_id=project_id):
-            if direct_cost["id"] == identifier:
+            try:
+                dc_identifier = direct_cost[key]
+            except KeyError:
+                # raise_exception(f"Direct Cost does not have {key} parameter")
+                continue
+            
+            if dc_identifier == identifier:
                 direct_cost_info = self.show(
                     company_id=company_id,
                     project_id=project_id,
@@ -105,4 +119,139 @@ def find(self, company_id, project_id, identifier):
                 )
                 return direct_cost_info
 
-        raise NotFoundItemError(f"Could not find Direct Cost {identifier}")
\ No newline at end of file
+        raise NotFoundItemError(f"Could not find Direct Cost {identifier}")
+    
+    def create(self, company_id, project_id, direct_cost_data, line_items, attachments=[]):
+        """
+        Creates a new Direct Cost item in the specified Project.
+
+        Parameters
+        ----------
+        company_id : int
+            unique identifier for the company
+        project_id : int
+            unique identifier for the project
+        direct_cost_data : dict
+            the data for the new Direct Cost item
+        line_items : list
+            the list of line items associated with the direct cost
+        attachments : list, default []
+            list of attachment file paths
+
+        Returns
+        -------
+        response : dict
+            response from the API containing the created Direct Cost item
+        """
+        headers = {
+            "Procore-Company-Id": f"{company_id}",
+            "Accept": "application/json",
+        }
+
+        # Prepare direct cost payload without line items as form data
+        payload = {
+            f'direct_cost[{key}]': str(value)
+            for key, value in direct_cost_data.items()
+        }
+
+        # Prepare attachments as form data
+        files = []
+        for attachment in attachments:
+            mime_type, _ = mimetypes.guess_type(attachment)
+            if mime_type is None:
+                mime_type = 'application/octet-stream'
+            files.append(('attachments[]', (attachment, open(attachment, 'rb'), mime_type)))
+
+        # Make the request
+        response = self.post_request(
+            api_url=f"{self.endpoint}/{project_id}/direct_costs",
+            additional_headers=headers,
+            data=payload,
+            files=files
+        )
+
+        # Close the file objects
+        for file_tuple in files:
+            file_tuple[1][1].close()
+
+        # Handle line items sequentially by patching because we can't use form data
+        if len(line_items) > 0:
+            direct_cost_id = response.get('id')
+
+            if direct_cost_id:
+                # Use the update method to add line items to the previously created Direct Cost item
+                self.update(company_id, project_id, direct_cost_id, line_items=line_items)
+
+        return response
+    
+    def update(self, company_id, project_id, direct_cost_id, direct_cost_data={}, line_items=[], attachments=[]):
+        """
+        Creates a new Direct Cost item in the specified Project.
+
+        Parameters
+        ----------
+        company_id : int
+            unique identifier for the company
+        project_id : int
+            unique identifier for the project
+        direct_cost_id : int
+            unique identifier for the direct cost
+        direct_cost_data : dict, default {}
+            the data for the new Direct Cost item
+        line_items : list, default []
+            the list of line items associated with the direct cost
+        attachments : list, default []
+            list of attachment file paths
+
+        Returns
+        -------
+        response : dict
+            response from the API containing the created Direct Cost item
+        """
+        headers = {
+            "Procore-Company-Id": f"{company_id}",
+            "Accept": "application/json",
+        }
+
+        # Prepare payload
+        if direct_cost_data:
+            payload = {
+                f'direct_cost[{key}]': str(value)
+                for key, value in direct_cost_data.items()
+            }
+        else:
+            payload = {}
+
+        # Prepare attachments
+        files = []
+        for attachment in attachments:
+            mime_type, _ = mimetypes.guess_type(attachment)
+            if mime_type is None:
+                mime_type = 'application/octet-stream'
+            files.append(('attachments[]', (attachment, open(attachment, 'rb'), mime_type)))
+
+        # Make the request
+        response = self.patch_request(
+            api_url=f"{self.endpoint}/{project_id}/direct_costs/{direct_cost_id}",
+            additional_headers=headers,
+            data=payload,
+            files=files
+        )
+
+        # Close the file objects
+        for file_tuple in files:
+            file_tuple[1][1].close()
+
+        # Handle line items separately to avoid form data
+        payload = {
+            'direct_cost': {
+                'line_items': line_items
+            }
+        }
+        _ = self.patch_request(
+            api_url=f"{self.endpoint}/{project_id}/direct_costs/{direct_cost_id}",
+            additional_headers=headers,
+            data=payload,
+        )
+
+        return response
\ No newline at end of file
diff --git a/ProPyCore/access/directory/__init__.py b/ProPyCore/access/directory/__init__.py
new file mode 100644
index 0000000..9983460
--- /dev/null
+++ b/ProPyCore/access/directory/__init__.py
@@ -0,0 +1,9 @@
+from .trades import Trades
+from .users import Users
+from .vendors import Vendors
+
+class Directory:
+    def __init__(self, access_token, server_url):
+        self.trades = Trades(access_token, server_url)
+        self.users = Users(access_token, server_url)
+        self.vendors = Vendors(access_token, server_url)
\ No newline at end of file
diff --git a/ProPyCore/access/directory/trades.py b/ProPyCore/access/directory/trades.py
new file mode 100644
index 0000000..1df70be
--- /dev/null
+++ b/ProPyCore/access/directory/trades.py
@@ -0,0 +1,79 @@
+from ..base import Base
+from ...exceptions import NotFoundItemError
+
+class Trades(Base):
+    """Access vendor information on a Company and Project Level"""
+
+    def __init__(self, access_token, server_url) -> None:
+        super().__init__(access_token, server_url)
+
+        self.endpoint = "/rest/v1.0/companies"
+
+    def get(self, company_id, per_page=1000):
+        """
+        Gets a list of all the trades the company level
+
+        Parameters
+        ----------
+        company_id : int
+            unique identifier for the company
+        per_page : int, default 1000
+            number of companies to include
+
+        Returns
+        -------
+        users : list of dict
+            list where each value is a dict with a user's information
+        """
+        trades = []
+        n_trades = 1
+        page = 1
+        while n_trades > 0:
+            params = {
+                "page": page,
+                "per_page": per_page,
+                "company_id": company_id
+            }
+
+            headers = {
+                "Procore-Company-Id": f"{company_id}"
+            }
+
+            trades_per_page = self.get_request(
+                api_url=f"{self.endpoint}/{company_id}/trades",
+                additional_headers=headers,
+                params=params
+            )
+            n_trades = len(trades_per_page)
+
+            trades += trades_per_page
+            page += 1
+
+        return trades
+
+    def find(self, company_id, user_id):
+        """
+        Finds a trade based on the identifier
+
+        Parameters
+        ----------
+        company_id : int
+            company id that the project is under
+        user_id : int or str
+            project id number or company name
+        
+        Returns
+        -------
+        trade : dict
+            trade-specific dictionary
+        """
+        if isinstance(user_id, int):
+            key = "id"
+        else:
+            key = "name"
+
+        for trade in self.get(company_id=company_id):
+            if trade[key] == user_id:
+                return trade
+
+        raise NotFoundItemError(f"Could not find {user_id}")
diff --git a/ProPyCore/access/directory.py b/ProPyCore/access/directory/users.py
similarity index 62%
rename from ProPyCore/access/directory.py
rename to ProPyCore/access/directory/users.py
index 3e0ffa0..1a3d879 100644
--- a/ProPyCore/access/directory.py
+++ b/ProPyCore/access/directory/users.py
@@ -1,20 +1,37 @@
-from .base import Base
-from ..exceptions import NotFoundItemError
+from ..base import Base
+from ...exceptions import NotFoundItemError
 
-class Directory(Base):
-    """
-    Wrapper class for Folders and Files - should NOT instantiate directly
-    Basic functionality for working with Folders and Files
-    """
+class Users(Base):
+    """Access user information on a Company and Project Level"""
 
     def __init__(self, access_token, server_url) -> None:
         super().__init__(access_token, server_url)
 
-        self.endpoint = None # create dummy endpoint so the methods have a reference
+    def get_url(self, company_id, project_id=None):
+        """
+        Returns the url specific to Users at company or project level
+
+        Parameters
+        ----------
+        company_id : int
+            unique identifier for the company
+        project_id : int, default None
+            unique identifier for the project
+            None specifies company-level
 
-    def get(self, company_id, project_id=None, per_page=10000):
+        Returns
+        -------
+        <get_url> : str
+            url for Users request
         """
-        Gets a list of all the projects from a certain company
+        if project_id is None:
+            return f"/rest/v1.1/companies/{company_id}/users"
+        else:
+            return f"/rest/v1.0/projects/{project_id}/users"
+
+    def get(self, company_id, project_id=None, per_page=1000):
+        """
+        Gets a list of all the users from the company or project level
 
         Parameters
         ----------
@@ -45,7 +62,6 @@ def get(self, company_id, project_id=None, per_page=10000):
                 "Procore-Company-Id": f"{company_id}"
             }
 
-            # url
             url = self.get_url(
                 company_id=company_id,
                 project_id=project_id
@@ -91,35 +107,7 @@ def find(self, company_id, user_id, project_id=None,):
             if user[key] == user_id:
                 return user
 
-        raise NotFoundItemError(f"Could not find {user_id}")
-
-class Users(Directory):
-    """Access user information on a Company and Project Level"""
-
-    def __init__(self, access_token, server_url) -> None:
-        super().__init__(access_token, server_url)
-
-    def get_url(self, company_id, project_id=None):
-        """
-        Returns the url specific to Users at company or project level
-
-        Parameters
-        ----------
-        company_id : int
-            unique identifier for the company
-        project_id : int, default None
-            unique identifier for the project
-            None specifies company-level
-
-        Returns
-        -------
-        <get_url> : str
-            url for Users request
-        """
-        if project_id is None:
-            return f"/rest/v1.1/companies/{company_id}/users"
-        else:
-            return f"/rest/v1.0/projects/{project_id}/users"
+        raise NotFoundItemError(f"Could not find User {user_id}")
 
     def add(self, company_id, project_id, user_id, permission_template_id=None):
         """
@@ -136,7 +124,6 @@ def add(self, company_id, project_id, user_id, permission_template_id=None):
         permission_template_id : int, default None
             level of permissions to give the added user
         """
-        # 768372
         data = {
             "user":{
                 "permission_template_id": permission_template_id,
@@ -156,59 +143,4 @@ def add(self, company_id, project_id, user_id, permission_template_id=None):
             additional_headers=headers,
             params=params,
             data=data
-        )
-
-class Vendors(Directory):
-    """Access vendor information on a Company and Project Level"""
-
-    def __init__(self, access_token, server_url) -> None:
-        super().__init__(access_token, server_url)
-
-    def get_url(self, company_id, project_id=None):
-        """
-        Returns the url specific to Vendors at company or project level
-
-        Parameters
-        ----------
-        company_id : int
-            unique identifier for the company
-        project_id : int, default None
-            unique identifier for the project
-            None specifies company-level
-
-        Returns
-        -------
-        <get_url> : str
-            url for Vendors request
-        """
-        if project_id is None:
-            # company level is included in the query parameters for this endpoint
-            return "/rest/v1.0/vendors"
-        else:
-            return f"/rest/v1.0/projects/{project_id}/vendors"
-
-class Trades(Directory):
-    """Access trade information on a Company Level only"""
-
-    def __init__(self, access_token, server_url) -> None:
-        super().__init__(access_token, server_url)
-
-    def get_url(self, company_id, project_id=None):
-        """
-        Returns the url specific to Trades at company level
-
-        Parameters
-        ----------
-        company_id : int
-            unique identifier for the company
-        project_id : int, default None
-            unique identifier for the project
-            None specifies company-level
-
-        Returns
-        -------
-        <get_url> : str
-            url for Trades request
-        """
-        # can only access trades at the company level
-        return f"/rest/v1.0/companies/{company_id}/trades"
\ No newline at end of file
+        )
\ No newline at end of file
diff --git a/ProPyCore/access/directory/vendors.py b/ProPyCore/access/directory/vendors.py
new file mode 100644
index 0000000..12299e5
--- /dev/null
+++ b/ProPyCore/access/directory/vendors.py
@@ -0,0 +1,109 @@
+from ..base import Base
+from ...exceptions import NotFoundItemError
+
+class Vendors(Base):
+    """Access vendor information on a Company and Project Level"""
+
+    def __init__(self, access_token, server_url) -> None:
+        super().__init__(access_token, server_url)
+
+    def get_url(self, project_id=None):
+        """
+        Returns the url specific to Vendors at company or project level
+
+        Parameters
+        ----------
+        project_id : int, default None
+            unique identifier for the project
+            None specifies company-level
+
+        Returns
+        -------
+        <url> : str
+            url for Vendors request
+        """
+        if project_id is None:
+            return f"/rest/v1.0/vendors"
+        else:
+            return f"/rest/v1.1/projects/{project_id}/vendors"
+
+    def get(self, company_id, project_id=None, per_page=10000):
+        """
+        Gets a list of all vendors from a certain company
+
+        Parameters
+        ----------
+        company_id : int
+            unique identifier for the company
+        project_id : int, default None
+            unique identifier for the project
+            None specifies company-level
+        per_page : int, default 100
+            number of companies to include
+
+        Returns
+        -------
+        vendors : list of dict
+            list where each value is a dict with a vendors's information
+        """
+        vendors = []
+        n_vendors = 1
+        page = 1
+        while n_vendors > 0:
+            params = {
+                "page": page,
+                "per_page": per_page,
+                "company_id": company_id # this parameter is only used in Company Vendors, but including it for other requests does not seem to create any issues
+            }
+
+            headers = {
+                "Procore-Company-Id": f"{company_id}"
+            }
+
+            url = self.get_url(
+                project_id=project_id
+            )
+
+            vendors_per_page = self.get_request(
+                api_url=url,
+                additional_headers=headers,
+                params=params
+            )
+            n_vendors = len(vendors_per_page)
+
+            vendors += vendors_per_page
+            page += 1
+
+        return vendors
+
+    def find(self, company_id, user_id, project_id=None,):
+        """
+        Finds a Vendor based on the identifier
+
+        Parameters
+        ----------
+        company_id : int
+            company id that the project is under
+        user_id : int or str
+            project id number or company name
+        project_id : int, default None
+            unique identifier for the project
+            None specifies company-level
+        
+        Returns
+        -------
+        vendor : dict
+            project-specific dictionary
+        """
+        if isinstance(user_id, int):
+            key = "id"
+        else:
+            key = "name"
+
+        for vendor in self.get(company_id=company_id, project_id=project_id):
+            if vendor[key] == user_id:
+                return vendor
+
+        raise NotFoundItemError(f"Could not find Vendor {user_id}")
+
+    
\ No newline at end of file
diff --git a/ProPyCore/exceptions.py b/ProPyCore/exceptions.py
index 28aed06..fad57ae 100644
--- a/ProPyCore/exceptions.py
+++ b/ProPyCore/exceptions.py
@@ -54,6 +54,10 @@ class InternalServerError(ProcoreException):
     """The rest Procore errors, 500 error."""
     pass
 
+class UnprocessableContentError(ProcoreException):
+    """Non-unique field, 422 error."""
+    pass
+
 def raise_exception(response):
     """
     Raises an exception based on the provided status code
@@ -72,6 +76,9 @@ def raise_exception(response):
     elif response.status_code == 404:
         raise NotFoundClientError('Client ID doesn\'t exist', response.text)
 
+    elif response.status_code == 422:
+        raise UnprocessableContentError('A field that needs a unique value already exists', response.text)
+
     elif response.status_code == 500:
         raise InternalServerError('Internal server error', response.text)
 
diff --git a/ProPyCore/procore.py b/ProPyCore/procore.py
index 54473e3..16c97c6 100644
--- a/ProPyCore/procore.py
+++ b/ProPyCore/procore.py
@@ -54,9 +54,7 @@ def __init__(self, client_id, client_secret, redirect_uri, base_url, oauth_url)
         self.__tasks__ = tasks.Task(access_token=self.__access_token, server_url=self.__base_url)
         self.__tools__ = generic_tools.GenericTool(access_token=self.__access_token, server_url=self.__base_url)
         # People
-        self.__users__ = directory.Users(access_token=self.__access_token, server_url=self.__base_url)
-        self.__vendors__ = directory.Vendors(access_token=self.__access_token, server_url=self.__base_url)
-        self.__trades__ = directory.Trades(access_token=self.__access_token, server_url=self.__base_url)
+        self.directory = directory.Directory(access_token=self.__access_token, server_url=self.__base_url)
         # Financials
         self.budgets = budgets.Budgets(access_token=self.__access_token, server_url=self.__base_url)
         self.direct_costs = direct_costs.DirectCosts(access_token=self.__access_token, server_url=self.__base_url)
diff --git a/docs/direct_cost_post.md b/docs/direct_cost_post.md
new file mode 100644
index 0000000..85d3041
--- /dev/null
+++ b/docs/direct_cost_post.md
@@ -0,0 +1,190 @@
+# Procore Direct Cost Example POST/PATCH Request
+[Procore Documentation](https://developers.procore.com/reference/rest/v1/direct-costs?version=1.1)
+
+### Example URL
+
+https://app.procore.com/rest/v1.1/projects/2783683/direct_costs
+
+### Headers:
+
+```json
+{
+    "Authorization":"Bearer <token>",
+    "Procore-Company-Id":"8089",
+    "Accept":"application/json"
+}
+```
+### Data
+
+```json
+{
+    "direct_cost[description]":"Invoice with attachment for April 13, 2024",
+    "direct_cost[direct_cost_date]":"2024-04-13",
+    "direct_cost[employee_id]":"8780450",
+    "direct_cost[invoice_number]":"Attachment 2024-04-13",
+    "direct_cost[origin_data]":"OD-4385814523",
+    "direct_cost[origin_id]":"px-1141",
+    "direct_cost[payment_date]":"2024-01-11",
+    "direct_cost[received_date]":"2024-01-11",
+    "direct_cost[status]":"approved",
+    "direct_cost[terms]":"Net 50",
+    "direct_cost[vendor_id]":"5181441",
+    "direct_cost[direct_cost_type]":"invoice",
+}
+```
+
+### Files
+
+```json
+[
+    (
+    'attachments[]',
+        (
+            'C:\\Users\\hfritz\\OneDrive - RO\\Documents\\packages\\ProPyCore\\snippets\\dummy\\direct_costs_module.pdf',
+            <_io.BufferedReader name='C:\\Users\\hfritz\\OneDrive - RO\\Documents\\packages\\ProPyCore\\snippets\\dummy\\direct_costs_module.pdf'>,
+            'application/pdf'
+        )
+    )
+]
+```
+
+## Code Snippets
+
+### POST: `create()`
+
+```python
+def create(self, company_id, project_id, direct_cost_data, line_items, attachments=[]):
+    """
+    Creates a new Direct Cost item in the specified Project.
+
+    Parameters
+    ----------
+    company_id : int
+        unique identifier for the company
+    project_id : int
+        unique identifier for the project
+    direct_cost_data : dict
+        the data for the new Direct Cost item
+    line_items : list
+        the list of line items associated with the direct cost
+    attachments : list, default []
+        list of attachment file paths
+
+    Returns
+    -------
+    response : dict
+        response from the API containing the created Direct Cost item
+    """
+    headers = {
+        "Procore-Company-Id": f"{company_id}",
+        "Accept": "application/json",
+    }
+
+    # Prepare payload
+    payload = {
+        f'direct_cost[{key}]': str(value)
+        for key, value in direct_cost_data.items()
+    }
+
+    # Add line items to the payload
+    line_item_payload = []
+    for line_item in line_items:
+        line_item_dict = {}
+        for key, value in line_item.items():
+            line_item_dict[key] = value
+            
+        line_item_payload.append(line_item_dict)
+
+    payload['direct_cost[line_items]'] = line_item_payload
+
+    # Prepare attachments
+    files = []
+    for attachment in attachments:
+        mime_type, _ = mimetypes.guess_type(attachment)
+        if mime_type is None:
+            mime_type = 'application/octet-stream'
+        files.append(('attachments[]', (attachment, open(attachment, 'rb'), mime_type)))
+
+    # Make the request
+    response = self.post_request(
+        api_url=f"{self.endpoint}/{project_id}/direct_costs",
+        additional_headers=headers,
+        data=payload,
+        files=files
+    )
+```
+
+### PATCH: `update()`
+
+```python
+def update(self, company_id, project_id, direct_cost_id, direct_cost_data={}, line_items=[], attachments=[]):
+"""
+Creates a new Direct Cost item in the specified Project.
+
+Parameters
+----------
+company_id : int
+    unique identifier for the company
+project_id : int
+    unique identifier for the project
+direct_cost_id : int
+    unique identifier for the direct cost
+direct_cost_data : dict, default {}
+    the data for the new Direct Cost item
+line_items : list, default []
+    the list of line items associated with the direct cost
+attachments : list, default []
+    list of attachment file paths
+
+Returns
+-------
+response : dict
+    response from the API containing the created Direct Cost item
+"""
+headers = {
+    "Procore-Company-Id": f"{company_id}",
+    "Accept": "application/json",
+}
+
+# Prepare payload
+if direct_cost_data:
+    payload = {
+        f'direct_cost[{key}]': str(value)
+        for key, value in direct_cost_data.items()
+    }
+else:
+    payload = {}
+
+# Add line items to the payload
+line_item_payload = []
+for line_item in line_items:
+    line_item_dict = {}
+    for key, value in line_item.items():
+        line_item_dict[key] = value
+        
+    line_item_payload.append(line_item_dict)
+
+payload['direct_cost[line_items]'] = line_item_payload
+
+# Prepare attachments
+files = []
+for attachment in attachments:
+    mime_type, _ = mimetypes.guess_type(attachment)
+    if mime_type is None:
+        mime_type = 'application/octet-stream'
+    files.append(('attachments[]', (attachment, open(attachment, 'rb'), mime_type)))
+
+# Make the request
+response = self.patch_request(
+    api_url=f"{self.endpoint}/{project_id}/direct_costs/{direct_cost_id}",
+    additional_headers=headers,
+    data=payload,
+    files=files
+)
+
+# Close the file objects
+for file_tuple in files:
+    file_tuple[1][1].close()
+
+return response
+```
\ No newline at end of file
diff --git a/docs/direct_costs_module.md b/docs/direct_costs_module.md
new file mode 100644
index 0000000..c1cd143
--- /dev/null
+++ b/docs/direct_costs_module.md
@@ -0,0 +1,161 @@
+## DirectCosts Module
+
+This module provides access and functionalities for working with Direct Costs using the Procore API.
+
+### Class: `DirectCosts`
+
+#### Description
+
+The `DirectCosts` class allows interaction with Direct Costs in a specified project within Procore.
+
+#### Initialization
+
+```python
+DirectCosts(access_token, server_url)
+```
+
+**Parameters:**
+- `access_token` (str): Access token for Procore API.
+- `server_url` (str): Base URL for the Procore API.
+
+#### Methods
+
+##### `get`
+
+Retrieves all available Direct Costs for a specified project.
+
+```python
+def get(self, company_id, project_id, page=1, per_page=100)
+```
+
+**Parameters:**
+- `company_id` (int): Unique identifier for the company.
+- `project_id` (int): Unique identifier for the project.
+- `page` (int, default 1): Page number.
+- `per_page` (int, default 100): Number of direct costs to include per page.
+
+**Returns:**
+- `direct_costs` (dict): Dictionary containing available direct cost data.
+
+##### `show`
+
+Retrieves specific Direct Cost information.
+
+```python
+def show(self, company_id, project_id, direct_cost_id)
+```
+
+**Parameters:**
+- `company_id` (int): Unique identifier for the company.
+- `project_id` (int): Unique identifier for the project.
+- `direct_cost_id` (int): Unique identifier for the direct cost.
+
+**Returns:**
+- `direct_cost_item` (dict): Dictionary containing specific direct cost information.
+
+##### `find`
+
+Finds specified Direct Cost and returns its data using the `show` method.
+
+```python
+def find(self, company_id, project_id, identifier)
+```
+
+**Parameters:**
+- `company_id` (int): Unique identifier for the company.
+- `project_id` (int): Unique identifier for the project.
+- `identifier` (int or str): Identifier for Direct Cost which can be ID (int).
+
+**Returns:**
+- `direct_cost_info` (dict): Dictionary containing Direct Cost data.
+
+**Raises:**
+- `NotFoundItemError`: If the specified Direct Cost is not found.
+
+##### `create`
+
+Creates a new Direct Cost item in the specified project.
+
+```python
+def create(self, company_id, project_id, direct_cost_data, line_items, attachments=[])
+```
+
+**Parameters:**
+- `company_id` (int): Unique identifier for the company.
+- `project_id` (int): Unique identifier for the project.
+- `direct_cost_data` (dict): Data for the new Direct Cost item.
+- `line_items` (list): List of line items associated with the direct cost.
+- `attachments` (list, default []): List of attachment file paths.
+
+**Returns:**
+- `response` (dict): Response from the API containing the created Direct Cost item.
+
+### Example Usage
+
+```python
+from direct_costs import DirectCosts
+
+# Initialize DirectCosts instance
+direct_costs = DirectCosts(access_token="your_access_token", server_url="https://api.procore.com")
+
+# Get all Direct Costs
+direct_costs_data = direct_costs.get(company_id=123, project_id=456)
+
+# Show specific Direct Cost
+specific_direct_cost = direct_costs.show(company_id=123, project_id=456, direct_cost_id=789)
+
+# Find a Direct Cost by ID
+found_direct_cost = direct_costs.find(company_id=123, project_id=456, identifier=789)
+
+# Create a new Direct Cost item
+direct_cost_data = {
+    "description": "Invoice for April",
+    "direct_cost_date": "2024-12-14",
+    "employee_id": 8780450,
+    "invoice_number": "Invoice v1.1",
+    "origin_data": "OD-2398273424",
+    "origin_id": "px-1990",
+    "payment_date": "2025-01-10",
+    "received_date": "2025-01-08",
+    "status": "approved",
+    "terms": "Net 50",
+    "vendor_id": 5181441,
+    "direct_cost_type": "invoice"
+}
+
+line_items = [
+    {
+        "manual_amount": 1000,
+        "wbs_code_id": 1989,
+        "description": "100' of Copper Piping",
+        "direct_cost_id": 81753,
+        "origin_data": "OD-2398273424",
+        "origin_id": "px-1990",
+        "quantity": 82.0201,
+        "ref": "PQRS5678",
+        "unit_cost": 12.03,
+        "uom": "cubic feet"
+    }
+]
+
+attachments = ["path/to/attachment1.pdf", "path/to/attachment2.pdf"]
+
+created_direct_cost = direct_costs.create(
+    company_id=123,
+    project_id=456,
+    direct_cost_data=direct_cost_data,
+    line_items=line_items,
+    attachments=attachments
+)
+```
+
+### Exception Handling
+
+The following exceptions are used within this module:
+
+- `NotFoundItemError`: Raised when a specified Direct Cost is not found.
+
+### Notes
+
+- Ensure the file paths provided in the `attachments` list are correct and accessible.
+- This setup will correctly include the files in the `multipart/form-data` request and handle the JSON data as specified.
\ No newline at end of file
diff --git a/snippets/direct_costs_snippets.py b/snippets/direct_costs_snippets.py
index 4dbeaa7..e9e62a2 100644
--- a/snippets/direct_costs_snippets.py
+++ b/snippets/direct_costs_snippets.py
@@ -1,15 +1,27 @@
 import os
+import json
 import sys
+import random
 import pathlib
 sys.path.append(f"{pathlib.Path(__file__).resolve().parent.parent}")
+PATH_TO_FOLDER = pathlib.Path(__file__).resolve().parent
 
 from ProPyCore.procore import Procore
 from dotenv import load_dotenv
-import json
+from datetime import datetime, timedelta
 
 if os.getenv("CLIENT_ID") is None:
     load_dotenv()
 
+def random_date(start, end):
+    """
+    This function will return a random datetime between two datetime
+    objects.
+    """
+    return start + timedelta(
+        seconds=random.randint(0, int((end - start).total_seconds())),
+    )
+
 if __name__ == "__main__":
     connection = Procore(
         client_id=os.getenv("CLIENT_ID"),
@@ -29,7 +41,7 @@
 
     # Example 1
     # ---------
-    print("Example 1")
+    print("Example 1: Get All Direct Costs")
     dcs = connection.direct_costs.get(
         company_id=company["id"],
         project_id=project["id"]
@@ -39,16 +51,14 @@
     
     # Example 2
     # ---------
-    print("Example 2")
-    direct_cost_id=dcs[0]["id"]
-    print(direct_cost_id)
+    print("Example 2: Find Direct Cost by ID")
+    direct_cost_id = 95627654
     dc = connection.direct_costs.show(
         company_id=company["id"],
         project_id=project["id"],
         direct_cost_id=direct_cost_id
     )
 
-    print(f"Number of Direct Cost Items: {len(dcs)}")
     print(json.dumps(dc, indent=4))
 
     # Example 3
@@ -65,10 +75,86 @@
     # Example 3
     # ---------
     print("Example 3")
-    dc_not_found = connection.direct_costs.find(
+    try:
+        dc_not_found = connection.direct_costs.find(
+            company_id=company["id"],
+            project_id=project["id"],
+            identifier=1
+        )
+
+        print(json.dumps(dc_not_found, indent=4))
+    except Exception as e:
+        print(f"Error: {e}")
+
+    # Example 4
+    # ---------
+    print("Example 4: Direct Cost with Attachment")
+    # Example of creating a Direct Cost item
+    # Generate a random date between two dates
+    start_date = datetime.strptime('2024-01-01', '%Y-%m-%d')
+    end_date = datetime.strptime('2024-12-31', '%Y-%m-%d')
+    random_day = random_date(start_date, end_date).date()
+    direct_cost_data = {
+        "description": f"Invoice with attachment for {random_day.strftime('%B %d, %Y')}",
+        "direct_cost_date": random_day.strftime('%Y-%m-%d'),
+        "employee_id": 8780450,
+        "invoice_number": f"Attachment {random_day.strftime('%Y-%m-%d')}",
+        "origin_data": f"OD-{''.join(random.choices('0123456789', k=10))}",
+        "origin_id": f"px-{''.join(random.choices('0123456789', k=4))}",
+        "payment_date": f"{(start_date + timedelta(days=10)).strftime('%Y-%m-%d')}",
+        "received_date": f"{(start_date + timedelta(days=10)).strftime('%Y-%m-%d')}",
+        "status": "approved",
+        "terms": "Net 50",
+        "vendor_id": 5181441,
+        "direct_cost_type": "invoice"
+    }
+
+    line_items = [
+        {
+            "wbs_code_id": 1752073765,
+            "description": "100' of Copper Piping",
+            "quantity": 82,
+            "unit_cost": 12,
+            "uom": "cubic feet"
+        },
+        {
+            "wbs_code_id": 1728675525,
+            "description": "Project Manager",
+            "quantity": 2,
+            "unit_cost": 100000.0,
+            "uom": "ea"
+        }
+    ]
+
+    attachments = [os.path.join(PATH_TO_FOLDER, "dummy", "direct_costs_module.pdf")]
+
+    created_direct_cost = connection.direct_costs.create(
         company_id=company["id"],
         project_id=project["id"],
-        identifier=1
+        direct_cost_data=direct_cost_data,
+        line_items=line_items,
+        attachments=attachments
     )
 
-    print(json.dumps(dc_not_found, indent=4))
+    # Example 5
+    # ---------
+    print("Example 5: Find by Invoice Number")
+    invoice_number = "Invoice 2024-12-07"
+    dc_by_invoice = connection.direct_costs.find(
+        company_id=company["id"],
+        project_id=project["id"],
+        identifier=invoice_number
+    )
+    print(dc_by_invoice["id"])
+
+    # Example 6
+    # ---------
+    print("Example 6: Update direct cost with attachment")
+    attachments = [os.path.join(PATH_TO_FOLDER, "dummy", "direct_costs_module.pdf")]
+
+    created_direct_cost = connection.direct_costs.update(
+        company_id=company["id"],
+        project_id=project["id"],
+        direct_cost_id=dc_by_invoice["id"],
+        attachments=attachments
+    )
diff --git a/snippets/directory.ipynb b/snippets/directory.ipynb
new file mode 100644
index 0000000..4abcad6
--- /dev/null
+++ b/snippets/directory.ipynb
@@ -0,0 +1,317 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# ProPyCore SDK Examples: Directory\n",
+    "\n",
+    "This notebook contains snippets from the `directory.py` module."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import os\n",
+    "import dotenv\n",
+    "import json\n",
+    "\n",
+    "import ProPyCore"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Setup\n",
+    "You will need to create the connection to your Procore app and then get the details for your company and project.\n",
+    "\n",
+    "### Connection to Procore App\n",
+    "Ensure you have a `.env` file with the following information included:\n",
+    "* `CLIENT_ID`: your data connection app's client ID\n",
+    "* `CLIENT_SECRET`: your data connection app's client secret"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "dotenv.load_dotenv()\n",
+    "connection = ProPyCore.procore.Procore(\n",
+    "    client_id=os.getenv(\"CLIENT_ID\"),\n",
+    "    client_secret=os.getenv(\"CLIENT_SECRET\"),\n",
+    "    redirect_uri=\"urn:ietf:wg:oauth:2.0:oob\", # default for data connection apps\n",
+    "    oauth_url=\"https://app.procore.com\", # default for data connection apps\n",
+    "    base_url=\"https://app.procore.com\" # default for data connection apps\n",
+    ")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "### Get Company and Project\n",
+    "Use the cells below to specify your company and your project"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "company_name = \"Rogers-O`Brien Construction\"\n",
+    "project_name = \"Sandbox Test Project\""
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now use the ProPyCore `connection` variable to get the company and project details."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "8089 2783683\n"
+     ]
+    }
+   ],
+   "source": [
+    "company = connection.companies.find(identifier=company_name)\n",
+    "project = connection.projects.find(\n",
+    "    company_id=company[\"id\"],\n",
+    "    identifier=project_name\n",
+    ")\n",
+    "print(company[\"id\"], project[\"id\"])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Directory: Trades\n",
+    "Trades are only specified at the company-level which simplifies the process of accessing and creating."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### `directory.trades.get()`\n",
+    "Gets a list of all the trades the company level\n",
+    "\n",
+    "**Parameters**\n",
+    "* company_id : int\n",
+    "    * unique identifier for the company\n",
+    "* per_page : int, default 1000\n",
+    "    * number of companies to include\n",
+    "\n",
+    "**Returns**\n",
+    "* users : list of dict\n",
+    "    * list where each value is a dict with a user's information"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### `directory.trades.find()`\n",
+    "Finds a trade based on the identifier\n",
+    "\n",
+    "**Parameters**\n",
+    "* company_id : int\n",
+    "    * company id that the project is under\n",
+    "* user_id : int or str\n",
+    "    * project id number or company name\n",
+    "\n",
+    "**Returns**\n",
+    "* trade : dict\n",
+    "    * trade-specific dictionary"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Directory: Users\n",
+    "Users can be specified at the project _and_ company level. If you supply a `project_id` to any of the `users` methods, you will be accessing Users at the project-level. Otherwise, it will default to company-level. The `get_url()` method handles the endpoint definitions. "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### `directory.users.get()`\n",
+    "Gets a list of all the users from the company or project level\n",
+    "\n",
+    "**Parameters**\n",
+    "* company_id : int\n",
+    "    * unique identifier for the company\n",
+    "* project_id : int, default None\n",
+    "    * unique identifier for the project\n",
+    "    * None specifies company-level\n",
+    "* per_page : int, default 100\n",
+    "    * number of companies to include\n",
+    "\n",
+    "**Returns**\n",
+    "* users : list of dict\n",
+    "    * list where each value is a dict with a user's information"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### `directory.users.find()`\n",
+    "Finds a user based on the identifier\n",
+    "\n",
+    "**Parameters**\n",
+    "* company_id : int\n",
+    "    * company id that the project is under\n",
+    "* user_id : int or str\n",
+    "    * project id number or company name\n",
+    "* project_id : int, default None\n",
+    "    * unique identifier for the project\n",
+    "    * None specifies company-level\n",
+    "\n",
+    "**Returns**\n",
+    "* project : dict\n",
+    "    * project-specific dictionary"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### `directory.users.add()`\n",
+    "Adds a company user to a given project\n",
+    "\n",
+    "**Parameters**\n",
+    "* company_id : int\n",
+    "    * unique identifier for the company\n",
+    "* project_id : int, default None\n",
+    "    * unique identifier for the project to add the user to\n",
+    "* user_id : int\n",
+    "    * unique identifier of the user to add\n",
+    "* permission_template_id : int, default None\n",
+    "    * level of permissions to give the added user\n",
+    "\n",
+    "**Returns**\n",
+    "* project : dict\n",
+    "    * project-specific dictionary"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Directory: Vendors\n",
+    "Like Users, Vendors can be specified at the project _and_ company level. If you supply a `project_id` to any of the `users` methods, you will be accessing Users at the project-level. Otherwise, it will default to company-level. The `get_url()` method handles the endpoint definitions. "
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### `directory.vendors.get()`\n",
+    "Gets a list of all vendors from a certain company\n",
+    "\n",
+    "**Parameters**\n",
+    "* company_id : int\n",
+    "    * unique identifier for the company\n",
+    "* project_id : int, default None\n",
+    "    * unique identifier for the project\n",
+    "    * None specifies company-level\n",
+    "* per_page : int, default 100\n",
+    "    * number of companies to include\n",
+    "\n",
+    "**Returns**\n",
+    "* vendors : list of dict\n",
+    "    * list where each value is a dict with a vendors's information"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "#### `directory.vendors.find()`\n",
+    "Finds a Vendor based on the identifier\n",
+    "\n",
+    "**Parameters**\n",
+    "* company_id : int\n",
+    "    * company id that the project is under\n",
+    "* user_id : int or str\n",
+    "    * project id number or company name\n",
+    "* project_id : int, default None\n",
+    "    * unique identifier for the project\n",
+    "    * None specifies company-level\n",
+    "\n",
+    "**Returns**\n",
+    "* vendor : dict\n",
+    "    * project-specific dictionary"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "---"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": ".venv",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/snippets/dummy/direct_costs_module.pdf b/snippets/dummy/direct_costs_module.pdf
new file mode 100644
index 0000000..b7866d2
Binary files /dev/null and b/snippets/dummy/direct_costs_module.pdf differ
diff --git a/snippets/find_user.py b/snippets/find_user.py
index 35be496..d396709 100644
--- a/snippets/find_user.py
+++ b/snippets/find_user.py
@@ -3,8 +3,7 @@
 import pathlib
 sys.path.append(f"{pathlib.Path(__file__).resolve().parent.parent}")
 
-from propycore.procore import Procore
-from propycore.exceptions import NotFoundItemError
+from ProPyCore.procore import Procore
 
 from dotenv import load_dotenv
 import json
@@ -21,12 +20,12 @@
         base_url=os.getenv("BASE_URL")
     )
 
-    company = connection.__companies__.find(identifier="Rogers-O`Brien Construction")
+    company = connection.companies.find(identifier="Rogers-O`Brien Construction")
 
     # Example 1: Get company-level user
     # ---------
     print("Example 1")
-    comp_user = connection.__users__.find(
+    comp_user = connection.directory.users.find(
         company_id=company["id"],
         user_id="Hagen Fritz"
     )
@@ -39,12 +38,12 @@
     # ---------
     print("\nExample 2")
     # find project
-    project = connection.__projects__.find(
+    project = connection.projects.find(
         company_id=company["id"],
         identifier="Sandbox Test Project"
     )
 
-    proj_user = connection.__users__.find(
+    proj_user = connection.directory.users.find(
         company_id=company["id"],
         project_id=project["id"],
         user_id=8780450
diff --git a/snippets/get_vendors.py b/snippets/get_vendors.py
index 5bce648..81f9611 100644
--- a/snippets/get_vendors.py
+++ b/snippets/get_vendors.py
@@ -3,8 +3,7 @@
 import pathlib
 sys.path.append(f"{pathlib.Path(__file__).resolve().parent.parent}")
 
-from propycore.procore import Procore
-from propycore.exceptions import NotFoundItemError
+from ProPyCore.procore import Procore
 
 from dotenv import load_dotenv
 import json
@@ -21,12 +20,12 @@
         base_url=os.getenv("BASE_URL")
     )
 
-    company = connection.__companies__.find(identifier="Rogers-O`Brien Construction")
+    company = connection.companies.find(identifier="Rogers-O`Brien Construction")
 
     # Example 1: Get company-level users
     # ---------
     print("Example 1")
-    comp_vendors = connection.__vendors__.get(
+    comp_vendors = connection.directory.vendors.get(
         company_id=company["id"]
     )
 
@@ -38,12 +37,12 @@
     # ---------
     print("\nExample 2")
     # find project
-    project = connection.__projects__.find(
+    project = connection.projects.find(
         company_id=company["id"],
         identifier="Sandbox Test Project"
     )
 
-    proj_vendors = connection.__vendors__.get(
+    proj_vendors = connection.directory.vendors.get(
         company_id=company["id"],
         project_id=project["id"]
     )
@@ -56,12 +55,12 @@
     # ---------
     print("\nExample 3")
     # find project
-    project2 = connection.__projects__.find(
+    project2 = connection.projects.find(
         company_id=company["id"],
         identifier=1668030
     )
 
-    proj_vendors2 = connection.__vendors__.get(
+    proj_vendors2 = connection.directory.vendors.get(
         company_id=company["id"],
         project_id=project2["id"]
     )


Test patch:
diff --git a/tests/unit/test_direct_costs.py b/tests/unit/test_direct_costs.py
index 7d7052c..9a2e2d3 100644
--- a/tests/unit/test_direct_costs.py
+++ b/tests/unit/test_direct_costs.py
@@ -43,3 +43,82 @@ def test_find_direct_cost_by_id(direct_costs_instance, mocker):
 
     assert isinstance(direct_cost_info, dict)
     assert direct_cost_info == mock_show_response
+
+
+def test_create_direct_cost(direct_costs_instance, mocker):
+    # Mock the post_request method to return a created Direct Cost item
+    mock_response = {
+        'id': 1,
+        'description': 'Invoice for April',
+        'direct_cost_date': '2024-12-14',
+        'employee_id': 43223,
+        'invoice_number': 'Invoice # abc123',
+        'origin_data': 'OD-2398273424',
+        'origin_id': 'px-1990',
+        'payment_date': '2025-01-10',
+        'received_date': '2025-01-08',
+        'status': 'approved',
+        'terms': 'Net 50',
+        'vendor_id': 23423,
+        'direct_cost_type': 'invoice',
+        'line_items': [
+            {
+                'manual_amount': 1000,
+                'wbs_code_id': 1989,
+                'description': "100' of Copper Piping",
+                'direct_cost_id': 81753,
+                'origin_data': 'OD-2398273424',
+                'origin_id': 'px-1990',
+                'quantity': 82.0201,
+                'ref': 'PQRS5678',
+                'unit_cost': 12.03,
+                'uom': 'cubic feet'
+            }
+        ],
+        'attachments': ["path/to/attachment1.pdf", "path/to/attachment2.pdf"]
+    }
+
+    mocker.patch.object(direct_costs_instance, 'post_request', return_value=mock_response)
+
+    direct_cost_data = {
+        "description": "Invoice for April",
+        "direct_cost_date": "2024-12-14",
+        "employee_id": 43223,
+        "invoice_number": "Invoice # abc123",
+        "origin_data": "OD-2398273424",
+        "origin_id": "px-1990",
+        "payment_date": "2025-01-10",
+        "received_date": "2025-01-08",
+        "status": "approved",
+        "terms": "Net 50",
+        "vendor_id": 23423,
+        "direct_cost_type": "invoice"
+    }
+
+    line_items = [
+        {
+            "manual_amount": 1000,
+            "wbs_code_id": 1989,
+            "description": "100' of Copper Piping",
+            "direct_cost_id": 81753,
+            "origin_data": "OD-2398273424",
+            "origin_id": "px-1990",
+            "quantity": 82.0201,
+            "ref": "PQRS5678",
+            "unit_cost": 12.03,
+            "uom": "cubic feet"
+        }
+    ]
+
+    attachments = ["path/to/attachment1.pdf", "path/to/attachment2.pdf"]
+
+    response = direct_costs_instance.create(
+        company_id=123,
+        project_id=456,
+        direct_cost_data=direct_cost_data,
+        line_items=line_items,
+        attachments=attachments
+    )
+
+    assert isinstance(response, dict)
+    assert response == mock_response
\ No newline at end of file