diff --git a/invoice_ninja/__init__.py b/invoice_ninja/__init__.py deleted file mode 100644 index 4bbfe22..0000000 --- a/invoice_ninja/__init__.py +++ /dev/null @@ -1,74 +0,0 @@ -import requests - -class InvoiceNinja(object): - API_V1 = 'api/v1' - ENDPOINT_URLS = { - 'clients': 'clients', - 'products': 'products', - 'invoices': 'invoices', - 'recurring invoices': 'recurring_invoices', - 'payments': 'payments', - 'quotes': 'quotes', - 'credits': 'credits', - 'reports': 'reports', - 'activities': 'activities', - 'charts': 'charts', - 'companies': 'companies', - 'documents': 'documents', - 'emails': 'emails', - 'expense': 'expenses', - 'export': 'export', - 'import': 'import_json', - 'ping': 'ping', - 'health check': 'health_check', - 'users': 'users' - } - - def __init__(self, - endpoint_url: str = 'https://invoicing.co', - api_token: str = str()): - self.endpoint_url = '{}/{}'.format(endpoint_url, self.API_V1) - self.api_token = api_token - self.headers = self.build_headers() - - def _get_url_for(self, endpoint: str = 'ping'): - ''' - Get complete URL for an endpoint. - - Endpoint URLs are appended to the Invoice Ninja base URL - and returned. - ''' - if endpoint in self.ENDPOINT_URLS: - return '{}/{}'.format(self.endpoint_url, - self.ENDPOINT_URLS[endpoint]) - - else: - raise KeyError('Endpoint URL not found') - - #def _get_headers(self, headers: dict = dict()): - def build_headers(self): - ''' - Build Invoice Ninja API headers for request. - - A header dictionary with the API token is returned by default. - ''' - - headers = { - 'X-API-TOKEN': self.api_token, - 'X-Requested-With': 'XMLHttpRequest'} - - return headers - - def ping(self): - ''' - Ping Invoice Ninja instance. - ''' - server_response = requests.get(url=self._get_url_for(), - headers=self.headers) - - if server_response.ok: - return True - - else: - return False - diff --git a/invoice_ninja/endpoints/__init__.py b/invoice_ninja/endpoints/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/invoice_ninja/endpoints/base_endpoint.py b/invoice_ninja/endpoints/base_endpoint.py deleted file mode 100644 index 2415307..0000000 --- a/invoice_ninja/endpoints/base_endpoint.py +++ /dev/null @@ -1,30 +0,0 @@ -from invoice_ninja import InvoiceNinja - -class BaseEndpoint(InvoiceNinja): - def bulk(self, action: str): - pass - - def archive(self): - pass - - def delete(self): - pass - - def restore(self): - pass - - def all(self): - pass - - def get(self): - pass - - def update(self): - pass - - def create(self): - pass - - def download(self): - pass - diff --git a/invoice_ninja/endpoints/clients.py b/invoice_ninja/endpoints/clients.py deleted file mode 100644 index b7d5397..0000000 --- a/invoice_ninja/endpoints/clients.py +++ /dev/null @@ -1,92 +0,0 @@ -from invoice_ninja.endpoints.base_endpoint import BaseEndpoint -from invoice_ninja.types.client import Client - -import requests - -class Clients(BaseEndpoint): - uri = '/api/v1/clients' - - def __init__(self, base_url: str = str(), api_token: str = str()): - super().__init__(base_url, api_token) - self.url = super()._get_url_for('clients') - - def __build_sort_params(self, sort: dict): - sort_params = {'sort': str()} - is_first_entry = True - for option in sort.keys(): - if is_first_entry: - sort_params['sort'] += '{}|{}'.format(option, sort[option]) - is_first_entry = False - - else: - sort_params['sort'] += ' {}|{}'.format(option, sort[option]) - - return sort_params - - def __client_from_dict(self, client: dict): - return Client(client_id=client['id'], - name=client['name'], - address=client['address1'], - city=client['city'], - state=client['state'], - postal_code=client['postal_code'], - phone=client['phone'], - email=client['contacts'][0]['email'], - pets=client['custom_value1']) - - def __clients_from_response(self, response: requests.Response): - clients = list() - for client_dict in response.json()['data']: - clients.append(self.__client_from_dict(client_dict)) - - return clients - - def show_client(self, client_id: str = None): - """ - Get client based on client id. - """ - - if client_id: - response = requests.get(url=self.url, - headers=super()._get_headers()) - - if response.ok: - return self.__client_from_dict(response.json()['data']) - - return None - - def list_clients(self, include: str = 'activities', - sort: dict = dict(), status: str = 'active', - name: str = None): - """ - Get list of clients. - """ - - request_params = dict() - - # Add sort parameters to request - if len(sort) > 0: - request_params.update(self.__build_sort_params(sort)) - - # Add include parameters to request - request_params.update({'include': include}) - - # Add status parameters to request - request_params.update({'status': status}) - - # Add name parameters to request - if name: - request_params.update({'name': name}) - - # Check is request should be sent with parameters - if len(request_params) > 0: - response = requests.get(url=self.url, - params=request_params, - headers=super()._get_headers()) - else: - response = requests.get(url=self.url, - headers=super()._get_headers()) - - if response.ok: - return self.__clients_from_response(response) - diff --git a/invoice_ninja/models/__init__.py b/invoice_ninja/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/invoice_ninja/models/client.py b/invoice_ninja/models/client.py deleted file mode 100644 index 1d7d277..0000000 --- a/invoice_ninja/models/client.py +++ /dev/null @@ -1,27 +0,0 @@ -class Client(object): - def __init__(self, client_id: int = None, name: str = None, - address: str = None, city: str = None, state: str = None, - postal_code: str = None, phone: str = None, - email: str = None, pets: str = None): - self.id = client_id - self.name = name - self.address = address - self.city = city - self.state = state - self.postal_code = postal_code - self.phone = phone - self.email = email - self.pets = pets - - def __str__(self): - return 'Client({}, {}, {}, {}, {}, {}, {}, {}, {})'.format( - self.id, - self.name, - self.address, - self.city, - self.state, - self.postal_code, - self.phone, - self.email, - self.pets) - diff --git a/invoice_ninja/models/clientContact.py b/invoice_ninja/models/clientContact.py deleted file mode 100644 index 93a7fe0..0000000 --- a/invoice_ninja/models/clientContact.py +++ /dev/null @@ -1,36 +0,0 @@ -class ClientContact(object): - def __init__(self, first_name: str = '', - last_name: str = '', - email: str = '', - phone: str = '', - send_email: bool = True, - custom_value1: str = '', - custom_value2: str = '', - custom_value3: str = '', - custom_value4: str = ''): - self.first_name = first_name - self.last_name = last_name - self.email = email - self.phone = phone - - # Flag for whether the contact will receive emails. - self.send_email = send_email - - # Custom values - self.custom_value1 = custom_value1 - self.custom_value2 = custom_value2 - self.custom_value3 = custom_value3 - self.custom_value4 = custom_value4 - - def __str__(self): - return 'ClientContact({}, {}, {}, {}, {}, {}, {}, {}, {})'.format( - self.first_name, - self.last_name, - self.email, - self.phone, - self.send_email, - self.custom_value1, - self.custom_value2, - self.custom_value3, - self.custom_value4) - diff --git a/invoice_ninja/models/clientSettings.py b/invoice_ninja/models/clientSettings.py deleted file mode 100644 index ba9fa4a..0000000 --- a/invoice_ninja/models/clientSettings.py +++ /dev/null @@ -1,38 +0,0 @@ -class ClientSettings(object): - def __init__(self, language_id: str = None, - currency_id: str = None, - payment_terms: str = None, - valid_until: str = None, - default_task_rate: float = 0, - send_reminders: bool = None): - # The language ID for the client, for the full list of languages, - # please see this resource - optional - # - # https://invoiceninja.github.io/docs/statics/#languages - self.language_id = language_id - - # The currency ID - optional - # See this resource for full list: - # - # https://invoiceninja.github.io/docs/statics/#currencies - self.currency_id = currency_id - - # The payment terms - in days - optional - self.payment_terms = payment_terms - - # The quote terms - optional - # - # How many days the quote will be valid for. - self.valid_until = valid_until - - # The task rate for this client - optional - # - # A value of 0 equates to disabled. - self.default_task_rate = default_task_rate - - # Whether the client will receive reminders - optional - # - # When left unset, this setting will rely on the company - # settings as the override/default - self.send_reminders = send_reminders - diff --git a/pyinvoiceninja/__init__.py b/pyinvoiceninja/__init__.py new file mode 100644 index 0000000..4ed83e9 --- /dev/null +++ b/pyinvoiceninja/__init__.py @@ -0,0 +1,4 @@ +from .client import InvoiceNinjaClient + +__all__ = ['InvoiceNinjaClient'] + diff --git a/pyinvoiceninja/client.py b/pyinvoiceninja/client.py new file mode 100644 index 0000000..dde749b --- /dev/null +++ b/pyinvoiceninja/client.py @@ -0,0 +1,146 @@ +from typing import Any +from typing import Dict + +# HTTP client +import requests + +# Logger +from loguru import logger + +# Local library imports +from .endpoints.clients import ClientsAPI +from .endpoints.documents import DocumentsAPI + +class InvoiceNinjaClient(object): + """ + A client for interacting with the Invoice Ninja API. + + Attributes + ---------- + base_url : str + URL to Invoice Ninja instance. + api_token : str + Token for API request access. + clients : ClientsAPI + An instance of the ClientsAPI class for client-related operations. + documents : DocumentsAPI + An instance of the DocumentsAPI class for document-related operations. + debug : bool + Flag to enable or disable debug logging. + """ + + def __init__(self, + base_url: str = 'https://invoicing.co/api/v1', + api_token: str = '', + debug: bool = False): + """ + Constructs all the necessary attributes for the InvoiceNinjaClient object + + Parameters + ---------- + base_url : str, optional + The base URL of the Invoice Ninja API (default is 'https://invoicing.co/api/v1'). + api_token : str, optional + The API token for authentication (default is an empty string). + debug : bool, optional + Flag to enable or disable debug logging (default is False). + """ + + # Check parameters for correct types + if not isinstance(base_url, str): + raise ValueError('Base URL must be a string.') + if not isinstance(api_token, str): + raise ValueError('API token must be a string.') + if not isinstance(debug, bool): + raise ValueError('Debug flag must be a boolean.') + + # Initialize paramater attributes + self._base_url = base_url + self._api_token = api_token + self._debug = debug + + # Initialize API client attributes + self._documents = DocumentsAPI(self) + self._clients = ClientsAPI(self) + + @property + def base_url(self) -> str: + """Gets the base URL of the Invoice Ninja API.""" + return self._base_url + + @base_url.setter + def base_url(self, base_url: str): + """Sets the base URL of the Invoice Ninja API.""" + if not isinstance(base_url, str): + raise ValueError('Base URL must be a string.') + self._base_url = base_url + + @property + def api_token(self) -> str: + """Gets the API token.""" + return self._api_token + + @api_token.setter + def api_token(self, api_token: str): + """Sets the API token.""" + if not isinstance(api_token, str): + raise ValueError('API token must be a string.') + self._api_token = api_token + + @property + def clients(self) -> ClientsAPI: + return self._clients + + @property + def documents(self) -> DocumentsAPI: + return self._documents + + @property + def debug(self): + return self._debug + + @debug.setter + def debug(self, debug: bool) -> None: + """Sets the debug flag.""" + if not isinstance(debug, bool): + raise ValueError('Debug flag must be a boolean.') + self._debug = debug + if self._debug: + logger.enable(__name__) + else: + logger.disable(__name__) + + @property + def headers(self) -> Dict[str, Any]: + """Gets the headers for API requests.""" + return {'X-API-TOKEN': self._api_token, + 'X-Requested-With': 'XMLHttpRequest'} + + def _log_debug(self, message: str) -> None: + """ + Logs a debug message if debugging is enabled. + + Parameters + ---------- + message : str + The debug message to log. + """ + if self._debug: + print(f'DEBUG: {message}') + + def ping(self): + """ + Ping the Invoice Ninja instance to check if it's reachable. + + Returns + ------- + bool + True if the server response is OK, False otherwise. + """ + logger.debug(f'Pinging {self.base_url}/ping with headers {self.headers}') + server_response = requests.get(url=f'{self.base_url}/ping', + headers=self.headers) + logger.debug(f'Server response: {server_response.status_code}, {server_response.text}') + + return server_response.ok + diff --git a/pyinvoiceninja/endpoints/__init__.py b/pyinvoiceninja/endpoints/__init__.py new file mode 100644 index 0000000..9a2f7da --- /dev/null +++ b/pyinvoiceninja/endpoints/__init__.py @@ -0,0 +1,5 @@ +from .clients import ClientsAPI +from .documents import DocumentsAPI + +__all__ = ['ClientsAPI', 'DocumentsAPI'] + diff --git a/pyinvoiceninja/endpoints/clients.py b/pyinvoiceninja/endpoints/clients.py new file mode 100644 index 0000000..4ce79a8 --- /dev/null +++ b/pyinvoiceninja/endpoints/clients.py @@ -0,0 +1,176 @@ +# HTTP client +import requests + +# Logger +from loguru import logger + +from typing import Dict +from typing import List +from typing import Optional +from typing import Union + +from pyinvoiceninja.models import Client + +class ClientsAPI: + """ + A client for interacting with the Invoice Ninja clients API. + + Attributes + ---------- + client : InvoiceNinjaClient + An instance of the InvoiceNinjaClient class. + clients_url : str + The URL for accessing the clients endpoint. + """ + + VALID_SORT_BY: List[str] = [ + 'id', 'name', 'balance', 'paid_to_date', 'payment_balance', + 'credit_balance', 'archived_at', 'updated_at', 'created_at', + 'display_name', 'address', 'city', 'state', 'postal_code', + 'custom_value1', 'custom_value2', 'custom_value3', 'custom_value4' + ] + VALID_SORT_ORDER: List[str] = ['asc', 'desc'] + VALID_INCLUDES: List[str] = ['activities', 'ledger', 'system_logs'] + VALID_CLIENT_STATUSES: List[str] = ['active', 'archived', 'deleted'] + VALID_BALANCE_OPERATORS: List[str] = ['lt', 'lte', 'gt', 'gte', 'eq'] + + def __init__(self, client: 'InvoiceNinjaClient') -> None: + """ + Constructs all the necessary attributes for the ClientsAPI object. + + Parameters + ---------- + client : InvoiceNinjaClient + An instance of the InvoiceNinjaClient class. + """ + self.client = client + self.clients_url = f'{self.client.base_url}/clients' + + def get_clients(self, + includes: Optional[List[str]] = None, + sort: Optional[Dict[str, str]] = None, + statuses: Optional[List[str]] = None, + name: Optional[str] = None, + email: Optional[str] = None, + balance: Optional[Dict[str, Union[str, int]]] = None, + is_deleted: Optional[bool] = None, + filter_deleted_clients: Optional[bool] = None) -> List[Client]: + """ + Retrieves a list of clients from the Invoice Ninja API. + + Parameters + ---------- + includes : Optional[List[str]] + The child relationships to include in the response. + sort : Optional[Dict[str, str]] + A dictionary containing the field by which to sort the clients and the order. + statuses : Optional[List[str]] + The statuses to filter the clients. + name : Optional[str] + The name to filter the clients. + email : Optional[str] + The email to filter the clients. + balance : Optional[Dict[str, Union[str, int]]] + The balance to filter the clients. + is_deleted : Optional[bool] + Whether to include deleted clients. + filter_deleted_clients : Optional[bool] + Whether to filter out deleted clients. + + Returns + ------- + List[Client] + A list of Client objects if the request is successful. + + Raises + ------ + ValueError + If any of the parameters are invalid. + """ + params = {} + + if includes: + if isinstance(includes, list): + for item in includes: + if item not in self.VALID_INCLUDES: + raise ValueError(f'Invalid include option: {item}. Valid options: {self.VALID_INCLUDES}') + params['include'] = ','.join(includes) + else: + raise ValueError('Includes must be a list of valid client child relationship strings.') + + if sort: + if isinstance(sort, dict): + if 'sort_by' not in sort or sort['sort_by'] not in self.VALID_SORT_BY: + raise ValueError(f'Invalid sort by option: {sort["sort_by"]}. Valid options: {self.VALID_SORT_BY}') + if 'sort_order' not in sort or sort['sort_order'] not in self.VALID_SORT_ORDER: + raise ValueError(f'Invalid sort order option: {sort["sort_order"]}. Valid options: {self.VALID_SORT_ORDER}') + params['sort'] = f'{sort["sort_by"]}|{sort["sort_order"]}' + else: + raise ValueError('Sort must be a dictionary with "sort_by" and "sort_order" keys.') + + if statuses: + if isinstance(statuses, list): + for item in statuses: + if item not in self.VALID_CLIENT_STATUSES: + raise ValueError(f'Invalid status: {item}. Valid options: {self.VALID_CLIENT_STATUSES}') + params['status'] = ','.join(statuses) + else: + raise ValueError('Statuses must be a list of valid client statuses.') + + if name: + if isinstance(name, str): + params['name'] = name + else: + raise ValueError('Name must be a string.') + + if email: + if isinstance(email, str): + params['email'] = email + else: + raise ValueError('Email parameter must be a string value.') + + # FIXME: This doesn't seem to be working properly. + if balance: + if isinstance(balance, dict): + if 'operator' in balance and 'value' in balance: + if balance['operator'] not in self.VALID_BALANCE_OPERATORS: + raise ValueError(f'Invalid balance operator: {balance["operator"]}. Valid options: {self.VALID_BALANCE_OPERATORS}') + if not isinstance(balance['value'], int): + raise ValueError('Balance value must be an integer.') + params['balance'] = f'{balance["operator"]}:{balance["value"]}' + else: + raise ValueError('Balance must be a dictionary with "operator" and "value" keys.') + else: + raise ValueError('Balance must be a dictionary with "operator" and "value" keys.') + + if is_deleted is not None: + if isinstance(is_deleted, bool): + params['is_deleted'] = is_deleted + else: + raise ValueError('Is deleted must be a boolean value.') + + if filter_deleted_clients is not None: + if isinstance(filter_deleted_clients, bool): + params['filter_deleted_clients'] = filter_deleted_clients + else: + raise ValueError('Filter deleted clients must be a boolean value.') + + request = requests.Request('GET', self.clients_url, headers=self.client.headers, params=params) + prepared_request = request.prepare() + logger.debug(f'Fetching clients from server: {prepared_request.url}') + session = requests.Session() + response = session.send(prepared_request) + logger.debug(f'Server response status: {response.status_code}') + + if response.ok: + clients = [Client.from_json(client_dict) for client_dict in response.json().get('data', [])] + if clients: + logger.success('Fetched client list from server.') + return clients + else: + logger.error('Search yielded no clients.') + return [] + else: + logger.error(f'Error fetching clients: {response.text}') + response.raise_for_status() + diff --git a/pyinvoiceninja/endpoints/documents.py b/pyinvoiceninja/endpoints/documents.py new file mode 100644 index 0000000..3888a9b --- /dev/null +++ b/pyinvoiceninja/endpoints/documents.py @@ -0,0 +1,64 @@ +import requests + +from pyinvoiceninja.models import Client +from pyinvoiceninja.models import Document + +class DocumentsAPI(object): + """ + A client for interacting with the Invoice Ninja documents API. + + Attributes + ---------- + client : InvoiceNinjaClient + An instance of the InvoiceNinjaClient class. + documents_url : str + The URL for accessing the clients endpoint. + """ + + def __init__(self, client): + """ + Constructs all the necessary attributes for the DocumentsAPI object. + + Parameters + ---------- + client : InvoiceNinjaClient + An instance of the InvoiceNinjaClient class. + """ + self.client = client + self.documents_url = f'{self.client.base_url}/documents' + + def __get_documents_from_response(self, response: requests.Response): + """ + Parses the response from the API and converts it to a list of Client objects. + + Parameters + ---------- + response : requests.Response + The response object from the API request. + + Returns + ------- + list of Client + A list of Client objects parsed from the response. + """ + documents = list() + for document_dict in response.json()['data']: + documents.append(Document.from_json(document_dict)) + return documents + + def get_documents(self): + """ + Retrieves a list of documents from the Invoice Ninja API. + + Returns + ------- + list of Documents + A list of Document objects if the request is successful. + """ + self.client._log_debug(f'Fetching documents from {self.documents_url}') + response = requests.get(self.documents_url, headers=self.client.headers) + self.client._log_debug(f'Server response: {response.status_code}, {response.text}') + + if response.ok: + return self.__get_documents_from_response(response) + diff --git a/invoice_ninja/models/README.md b/pyinvoiceninja/models/README.md similarity index 100% rename from invoice_ninja/models/README.md rename to pyinvoiceninja/models/README.md diff --git a/pyinvoiceninja/models/__init__.py b/pyinvoiceninja/models/__init__.py new file mode 100644 index 0000000..a7f900f --- /dev/null +++ b/pyinvoiceninja/models/__init__.py @@ -0,0 +1,7 @@ +from .client import Client +from .clientContact import ClientContact +from .clientSettings import ClientSettings +from .document import Document + +__all__ = ['Client', 'ClientContact', 'ClientSettings', 'Document'] + diff --git a/pyinvoiceninja/models/client.py b/pyinvoiceninja/models/client.py new file mode 100644 index 0000000..b67d5fa --- /dev/null +++ b/pyinvoiceninja/models/client.py @@ -0,0 +1,402 @@ +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Type +from typing import Union + +# JSON serializer/deserializer +import json + +# Local library imports +from pyinvoiceninja.models.clientContact import ClientContact +from pyinvoiceninja.models.clientInvoice import ClientInvoice +from pyinvoiceninja.models.clientPayment import ClientPayment +from pyinvoiceninja.models.clientSettings import ClientSettings +from pyinvoiceninja.models.document import Document + +class Client: + """ + A class to represent a client. + + Attributes: + id (int): The ID of the client. + name (str): The name of the client. + private_notes (str): Private notes about the client. + public_notes (str): Public notes about the client. + balance (int): The balance of the client. + paid_to_date (int): The amount paid to date by the client. + payment_balance (int): The payment balance of the client. + credit_balance (int): The credit balance of the client. + settings (dict): Client-specific settings. + is_deleted (bool): Indicates if the client is deleted. + updated_at (int): The timestamp when the client was last updated. + archived_at (int): The timestamp when the client was archived. + created_at (int): The timestamp when the client was created. + display_name (str): The display name of the client. + contacts (list): A list of contacts associated with the client. + documents (list): A list of documents associated with the client. + address (str): The address of the client. + city (str): The city of the client. + state (str): The state of the client. + postal_code (str): The postal code of the client. + phone (str): The phone number of the client. + custom_value1 (str): Custom field 1. + custom_value2 (str): Custom field 2. + custom_value3 (str): Custom field 3. + custom_value4 (str): Custom field 4. + """ + + def __init__(self, client_id: int = 0, + name: str = '', + private_notes: str = '', + public_notes: str = '', + balance: int = 0, + paid_to_date: int = 0, + payment_balance: int = 0, + credit_balance: int = 0, + settings: Optional[ClientSettings] = None, + is_deleted: bool = False, + updated_at: int = 0, + archived_at: int = 0, + created_at: int = 0, + display_name: str = '', + contacts: Optional[List[ClientContact]] = None, + documents: Optional[List[Document]] = None, + address: str = '', + city: str = '', + state: str = '', + postal_code: str = '', + phone: str = '', + custom_value1: str = '', + custom_value2: str = '', + custom_value3: str = '', + custom_value4: str = '', + ledger: Optional[List[Union[ClientPayment, ClientInvoice]]] = None): + self._id = client_id + self._name = name + self._private_notes = private_notes + self._public_notes = public_notes + self._balance = balance + self._paid_to_date = paid_to_date + self._payment_balance = payment_balance + self._credit_balance = credit_balance + self._settings = settings or {} + self._is_deleted = is_deleted + self._updated_at = updated_at + self._archived_at = archived_at + self._created_at = created_at + self._display_name = display_name + self._contacts = contacts or [] + self._documents = documents or [] + self._address = address + self._city = city + self._state = state + self._postal_code = postal_code + self._phone = phone + self._custom_value1 = custom_value1 + self._custom_value2 = custom_value2 + self._custom_value3 = custom_value3 + self._custom_value4 = custom_value4 + self._ledger = ledger + + @property + def id(self): + return self._id + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + + @name.deleter + def name(self): + self._name = '' + + @property + def private_notes(self): + return self._private_notes + + @private_notes.setter + def private_notes(self, note): + self._private_notes = note + + @private_notes.deleter + def private_notes(self): + self._private_notes = '' + + @property + def public_notes(self): + return self._public_notes + + @public_notes.setter + def public_notes(self, note): + self._public_notes = note + + @public_notes.deleter + def public_notes(self): + self._public_notes = '' + + @property + def balance(self): + return self._balance + + @property + def paid_to_date(self): + return self._paid_to_date + + @property + def payment_balance(self): + return self._payment_balance + + @property + def credit_balance(self): + return self._credit_balance + + @property + def settings(self): + return self._settings + + @settings.setter + def settings(self, settings): + self._settings = settings + + @settings.deleter + def settings(self): + self._settings = {} + + @property + def is_deleted(self): + return self._is_deleted + + @property + def updated_at(self): + return self._updated_at + + @property + def archived_at(self): + return self._archived_at + + @property + def created_at(self): + return self._created_at + + @property + def display_name(self): + return self._display_name + + @display_name.setter + def display_name(self, name): + self._display_name = name + + @display_name.deleter + def display_name(self): + self._display_name = '' + + @property + def contacts(self): + return self._contacts + + @contacts.setter + def contacts(self, contacts): + self._contacts = contacts + + @contacts.deleter + def contacts(self): + self._contacts = [] + + @property + def documents(self): + return self._documents + + @documents.setter + def documents(self, documents): + self._documents = documents + + @documents.deleter + def documents(self): + self._documents = [] + + @property + def address(self): + return self._address + + @address.setter + def address(self, address): + self._address = address + + @address.deleter + def address(self): + self._address = '' + + @property + def city(self): + return self._city + + @city.setter + def city(self, city): + self._city = city + + @city.deleter + def city(self): + self._city = '' + + @property + def state(self): + return self._state + + @state.setter + def state(self, state): + self._state = state + + @state.deleter + def state(self): + self._state = '' + + @property + def postal_code(self): + return self._postal_code + + @postal_code.setter + def postal_code(self, postal_code): + self._postal_code = postal_code + + @postal_code.deleter + def postal_code(self): + self._postal_code = '' + + @property + def custom_value1(self): + return self._custom_value1 + + @custom_value1.setter + def custom_value1(self, value): + self._custom_value1 = value + + @custom_value1.deleter + def custom_value1(self): + self._custom_value1 = '' + + @property + def custom_value2(self): + return self._custom_value2 + + @custom_value2.setter + def custom_value2(self, value): + self._custom_value2 = value + + @custom_value2.deleter + def custom_value2(self): + self._custom_value2 = '' + + @property + def custom_value3(self): + return self._custom_value3 + + @custom_value3.setter + def custom_value3(self, value): + self._custom_value3 = value + + @custom_value3.deleter + def custom_value3(self): + self._custom_value3 = '' + + @property + def custom_value4(self): + return self._custom_value4 + + @custom_value4.setter + def custom_value4(self, value): + self._custom_value4 = value + + @custom_value4.deleter + def custom_value4(self): + self._custom_value4 = '' + + @property + def ledger(self): + return self._ledger + + @ledger.setter + def ledger(self, ledger: List[Dict[str, Any]]): + self._ledger = ledger + + @classmethod + def from_json(cls, client: dict): + """ + Create a Client instance from a JSON-like dictionary. + """ + contacts = [ClientContact.from_json(contact) for contact in client.get('contacts', [])] + documents = [Document.from_json(document) for document in client.get('documents', [])] + ledger_processed = [] + for index, item in enumerate(client.get('ledger', [])): + if index % 2 == 0: + # Processing ClientInvoice + ledger_processed = ClientInvoice.from_json(item) + + else: + # Processing ClientPayment + ledger_processed = ClientPayment.from_json(item) + + + return cls(client_id=client['id'], + name=client['name'], + private_notes=client['private_notes'], + public_notes=client['public_notes'], + balance=client['balance'], + paid_to_date=client['paid_to_date'], + payment_balance=client['payment_balance'], + credit_balance=client['credit_balance'], + settings=ClientSettings.from_json(client['settings']), + is_deleted=client['is_deleted'], + updated_at=client['updated_at'], + archived_at=client['archived_at'], + created_at=client['created_at'], + display_name=client['display_name'], + contacts=contacts, + documents=documents, + address=client['address1'], + city=client['city'], + state=client['state'], + postal_code=client['postal_code'], + phone=client['phone'], + custom_value1=client['custom_value1'], + custom_value2=client['custom_value2'], + custom_value3=client['custom_value3'], + custom_value4=client['custom_value4'], + ledger=ledger_processed) + + def to_json(self): + """ + Convert the Client instance to a JSON-like dictionary. + """ + return json.dumps({'id': self._id, + 'name': self._name, + 'private_notes': self._private_notes, + 'public_notes': self._public_notes, + 'balance': self._balance, + 'paid_to_date': self._paid_to_date, + 'payment_balance': self._payment_balance, + 'credit_balance': self._credit_balance, + 'settings': self._settings, + 'is_deleted': self._is_deleted, + 'updated_at': self._updated_at, + 'archived_at': self._archived_at, + 'created_at': self._created_at, + 'display_name': self._display_name, + 'contacts': [contact.to_json() for contact in self._contacts], + 'documents': [document.to_json() for document in self._documents], + 'address1': self._address, + 'city': self._city, + 'state': self._state, + 'postal_code': self._postal_code, + 'phone': self._phone, + 'custom_value1': self._custom_value1, + 'custom_value2': self._custom_value2, + 'custom_value3': self._custom_value3, + 'custom_value4': self._custom_value4, + 'ledger': self._ledger}) + diff --git a/pyinvoiceninja/models/clientContact.py b/pyinvoiceninja/models/clientContact.py new file mode 100644 index 0000000..0489cdd --- /dev/null +++ b/pyinvoiceninja/models/clientContact.py @@ -0,0 +1,254 @@ +from typing import Any +from typing import Dict +from typing import List + +class ClientContact(object): + def __init__(self, contact_id: str = '', + first_name: str = '', + last_name: str = '', + email: str = '', + phone: str = '', + password: str = '', + created_at: int = 0, + updated_at: int = 0, + archived_at: int = 0, + is_primary: bool = False, + is_locked: bool = False, + send_email: bool = False, + contact_key: str = '', + last_login: int = 0, + portal_link: str = '', + custom_value1: str = '', + custom_value2: str = '', + custom_value3: str = '', + custom_value4: str = '') -> None: + self._contact_id = contact_id + self._first_name = first_name + self._last_name = last_name + self._email = email + self._phone = phone + self._password = password + self._created_at = created_at + self._updated_at = updated_at + self._archived_at = archived_at + self._is_primary = is_primary + self._is_locked = is_locked + self._contact_key = contact_key + self._last_login = last_login + self._send_email = send_email + self._portal_link = portal_link + self._custom_value1 = custom_value1 + self._custom_value2 = custom_value2 + self._custom_value3 = custom_value3 + self._custom_value4 = custom_value4 + + @property + def contact_id(self) -> str: + return self._contact_id + + @property + def first_name(self) -> str: + return self._first_name + + @first_name.setter + def first_name(self, first_name: str) -> None: + self._first_name = first_name + + @first_name.deleter + def first_name(self) -> None: + self._first_name = '' + + @property + def last_name(self) -> str: + return self._last_name + + @last_name.setter + def last_name(self, last_name: str) -> None: + self._last_name = last_name + + @last_name.deleter + def last_name(self) -> None: + self._last_name = '' + + @property + def email(self) -> str: + return self._email + + @email.setter + def email(self, email: str) -> None: + self._email = email + + @email.deleter + def email(self) -> None: + self._email = '' + + @property + def phone(self) -> str: + return self._phone + + @phone.setter + def phone(self, phone: str) -> None: + self._phone = phone + + @phone.deleter + def phone(self) -> None: + self._phone = '' + + @property + def password(self) -> str: + return self._password + + @password.setter + def password(self, password: str) -> None: + self._password = password + + @password.deleter + def password(self) -> None: + self._password = '' + + @property + def created_at(self) -> int: + return self._created_at + + @property + def updated_at(self) -> int: + return self._updated_at + + @property + def archived_at(self) -> int: + return self._archived_at + + @property + def is_primary(self) -> bool: + return self._is_primary + + @is_primary.setter + def is_primary(self, is_primary: bool) -> None: + self._is_primary = is_primary + + @property + def is_locked(self) -> bool: + return self._is_locked + + @is_locked.setter + def is_locked(self, is_locked: bool) -> None: + self._is_locked = is_locked + + @property + def contact_key(self) -> str: + return self._contact_key + + @property + def last_login(self) -> int: + return self._last_login + + @property + def send_email(self) -> bool: + return self._send_email + + @send_email.setter + def send_email(self, send_email: bool) -> None: + self._send_email = send_email + + @property + def portal_link(self) -> str: + return self._portal_link + + @property + def custom_value1(self) -> str: + return self._custom_value1 + + @custom_value1.setter + def custom_value1(self, value: str) -> None: + self._custom_value1 = value + + @custom_value1.deleter + def custom_value1(self) -> None: + self._custom_value1 = '' + + @property + def custom_value2(self) -> str: + return self._custom_value2 + + @custom_value2.setter + def custom_value2(self, value: str) -> None: + self._custom_value2 = value + + @custom_value2.deleter + def custom_value2(self) -> None: + self._custom_value2 = '' + + @property + def custom_value3(self) -> str: + return self._custom_value3 + + @custom_value3.setter + def custom_value3(self, value: str) -> None: + self._custom_value3 = value + + @custom_value3.deleter + def custom_value3(self) -> None: + self._custom_value3 = '' + + @property + def custom_value4(self) -> str: + return self._custom_value4 + + @custom_value4.setter + def custom_value4(self, value: str) -> None: + self._custom_value4 = value + + @custom_value4.deleter + def custom_value4(self) -> None: + self._custom_value4 = '' + + @classmethod + def from_json(cls, contact: Dict[str, Any]) -> 'ClientContact': + return cls(contact_id=contact['id'], + first_name=contact['first_name'], + last_name=contact['last_name'], + email=contact['email'], + created_at=contact['created_at'], + updated_at=contact['updated_at'], + archived_at=contact['archived_at'], + is_primary=contact['is_primary'], + is_locked=contact['is_locked'], + phone=contact['phone'], + custom_value1=contact['custom_value1'], + custom_value2=contact['custom_value2'], + custom_value3=contact['custom_value3'], + custom_value4=contact['custom_value4'], + contact_key=contact['contact_key'], + send_email=contact['send_email'], + last_login=contact['last_login'], + password=contact['password'], + portal_link=contact['link']) + + @classmethod + def from_json_list(cls, contacts: List[Dict[str, Any]]) -> List['ClientContact']: + return [cls.from_json(contact) for contact in contacts] + + def to_json(self) -> str: + import json + contact = { + "contact_id": self._contact_id, + "first_name": self._first_name, + "last_name": self._last_name, + "email": self._email, + "phone": self._phone, + "created_at": self._created_at, + "updated_at": self._updated_at, + "archived_at": self._archived_at, + "is_primary": self._is_primary, + "is_locked": self._is_locked, + "send_email": self._send_email, + "contact_key": self._contact_key, + "last_login": self._last_login, + "portal_link": self._portal_link, + "custom_value1": self._custom_value1, + "custom_value2": self._custom_value2, + "custom_value3": self._custom_value3, + "custom_value4": self._custom_value4 + } + return json.dumps(contact) + diff --git a/pyinvoiceninja/models/clientInvoice.py b/pyinvoiceninja/models/clientInvoice.py new file mode 100644 index 0000000..1ba680d --- /dev/null +++ b/pyinvoiceninja/models/clientInvoice.py @@ -0,0 +1,48 @@ +from typing import Any +from typing import Dict +from typing import Optional +from typing import Type + +import json + +class ClientInvoice: + def __init__(self, invoice_id: Optional[str] = None, + notes: Optional[str] = None, + balance: Optional[int] = None, + adjustment: Optional[int] = None, + activity_id: Optional[int] = None, + created_at: Optional[int] = None, + updated_at: Optional[int] = None, + archived_at: Optional[int] = None): + self.invoice_id = invoice_id + self.notes = notes + self.balance = balance + self.adjustment = adjustment + self.activity_id = activity_id + self.created_at = created_at + self.updated_at = updated_at + self.archived_at = archived_at + + @classmethod + def from_json(cls: Type['ClientInvoice'], invoice: Dict[str, Any]) -> 'ClientInvoice': + return cls(invoice_id=invoice.get('invoice_id', None), + notes=invoice.get('notes', None), + balance=invoice.get('balance', None), + adjustment=invoice.get('adjustment', None), + activity_id=invoice.get('activity_id', None), + created_at=invoice.get('created_at', None), + updated_at=invoice.get('updated_at', None), + archived_at=invoice.get('archived_at', None)) + + def __str__(self): + return f'Invoice: {self.invoice_id}, {self.balance}' + + def to_json(self) -> str: + return json.dumps({'invoice_id': self.invoice_id, + 'notes': self.notes, + 'balance': self.balance, + 'adjustment': self.adjustment, + 'activity_id': self.activity_id, + 'created_at': self.created_at, + 'archive_at': self.archived_at}) + diff --git a/pyinvoiceninja/models/clientPayment.py b/pyinvoiceninja/models/clientPayment.py new file mode 100644 index 0000000..c5c2f0d --- /dev/null +++ b/pyinvoiceninja/models/clientPayment.py @@ -0,0 +1,48 @@ +from typing import Any +from typing import Dict +from typing import Optional +from typing import Type + +import json + +class ClientPayment: + def __init__(self, payment_id: Optional[str] = None, + notes: Optional[str] = None, + balance: Optional[int] = None, + adjustment: Optional[int] = None, + activity_id: Optional[int] = None, + created_at: Optional[int] = None, + updated_at: Optional[int] = None, + archived_at: Optional[int] = None): + self.payment_id = payment_id + self.notes = notes + self.balance = balance + self.adjustment = adjustment + self.activity_id = activity_id + self.created_at = created_at + self.updated_at = updated_at + self.archived_at = archived_at + + @classmethod + def from_json(cls: Type['ClientLedger'], ledger: Dict[str, Any]) -> 'ClientLedger': + return cls(payment_id=ledger.get('payment_id', None), + notes=ledger.get('notes', None), + balance=ledger.get('balance', None), + adjustment=ledger.get('adjustment', None), + activity_id=ledger.get('activity_id', None), + created_at=ledger.get('created_at', None), + updated_at=ledger.get('updated_at', None), + archived_at=ledger.get('archived_at', None)) + + def __str__(self): + return f'Ledger: {self.payment_id}, {self.balance}' + + def to_json(self) -> str: + return json.dumps({'payment_id': self.payment_id, + 'notes': self.notes, + 'balance': self.balance, + 'adjustment': self.adjustment, + 'activity_id': self.activity_id, + 'created_at': self.created_at, + 'archive_at': self.archived_at}) + diff --git a/pyinvoiceninja/models/clientSettings.py b/pyinvoiceninja/models/clientSettings.py new file mode 100644 index 0000000..29e1554 --- /dev/null +++ b/pyinvoiceninja/models/clientSettings.py @@ -0,0 +1,42 @@ +from typing import Any +from typing import Dict +from typing import Optional +from typing import Type + +import json + +class ClientSettings: + def __init__(self, currency_id: Optional[str] = None, + language_id: Optional[str] = None, + payment_terms: Optional[int] = None, + default_task_rate: Optional[float] = None, + send_reminders: Optional[bool] = None, + valid_until: Optional[str] = None, + enable_client_portal_password: Optional[bool] = None): + self.currency_id = currency_id + self.language_id = language_id + self.payment_terms = payment_terms + self.default_task_rate = default_task_rate + self.send_reminders = send_reminders + self.valid_until = valid_until + self.enable_client_portal_password = enable_client_portal_password + + @classmethod + def from_json(cls: Type['ClientSettings'], settings: Dict[str, Any]) -> 'ClientSettings': + return cls(currency_id=settings.get('currency_id', None), + language_id=settings.get('language_id', None), + payment_terms=settings.get('payment_terms', None), + default_task_rate=settings.get('default_task_rate', None), + send_reminders=settings.get('send_reminders', None), + valid_until=settings.get('valid_until', None), + enable_client_portal_password=settings.get('enable_client_portal_password', None)) + + def to_json(self) -> str: + return json.dumps({'currency_id': self.currency_id, + 'language_id': self.language_id, + 'payment_terms': self.payment_terms, + 'default_task_rate': self.default_task_rate, + 'send_reminders': self.send_reminders, + 'valid_until': self.valid_until, + 'enable_client_portal_password': self.enable_client_portal_password}) + diff --git a/pyinvoiceninja/models/document.py b/pyinvoiceninja/models/document.py new file mode 100644 index 0000000..9aa3ac0 --- /dev/null +++ b/pyinvoiceninja/models/document.py @@ -0,0 +1,147 @@ +from typing import Any +from typing import Dict +from typing import List +import json + +class Document: + def __init__(self, document_id: str = '', + user_id: str = '', + assigned_user_id: str = '', + project_id: str = '', + vendor_id: str = '', + name: str = '', + url: str = '', + preview: str = '', + document_type: str = '', + disk: str = '', + document_hash: str = '', + is_deleted: bool = False, + is_default: bool = False, + created_at: int = 0, + updated_at: int = 0, + deleted_at: int = 0): + self._document_id = document_id + self._user_id = user_id + self._assigned_user_id = assigned_user_id + self._project_id = project_id + self._vendor_id = vendor_id + self._name = name + self._url = url + self._preview = preview + self._document_type = document_type + self._disk = disk + self._document_hash = document_hash + self._is_deleted = is_deleted + self._is_default = is_default + self._created_at = created_at + self._updated_at = updated_at + self._deleted_at = deleted_at + + @property + def document_id(self) -> str: + return self._document_id + + @property + def user_id(self) -> str: + return self._user_id + + @property + def assigned_user_id(self) -> str: + return self._assigned_user_id + + @property + def project_id(self) -> str: + return self._project_id + + @property + def vendor_id(self) -> str: + return self._vendor_id + + @property + def name(self) -> str: + return self._name + + @property + def url(self) -> str: + return self._url + + @property + def preview(self) -> str: + return self._preview + + @property + def document_type(self) -> str: + return self._document_type + + @property + def disk(self) -> str: + return self._disk + + @property + def document_hash(self) -> str: + return self._document_hash + + @property + def is_deleted(self) -> bool: + return self._is_deleted + + @property + def is_default(self) -> bool: + return self._is_default + + @property + def created_at(self) -> int: + return self._created_at + + @property + def updated_at(self) -> int: + return self._updated_at + + @property + def deleted_at(self) -> int: + return self._deleted_at + + @classmethod + def from_json(cls, document: Dict[str, Any]) -> 'Document': + return cls(document_id=document.get('id', ''), + user_id=document.get('user_id', ''), + assigned_user_id=document.get('assigned_user_id', ''), + project_id=document.get('project_id', ''), + vendor_id=document.get('vendor_id', ''), + name=document.get('name', ''), + url=document.get('url', ''), + preview=document.get('preview', ''), + document_type=document.get('type', ''), + disk=document.get('disk', ''), + document_hash=document.get('hash', ''), + is_deleted=document.get('is_deleted', False), + is_default=document.get('is_default', False), + created_at=document.get('created_at', 0), + updated_at=document.get('updated_at', 0), + deleted_at=document.get('deleted_at', 0)) + + @classmethod + def from_json_list(cls, documents: List[Dict[str, Any]]) -> List['Document']: + return [cls.from_json(document) for document in documents] + + def to_json(self) -> str: + return json.dumps({'document_id': self._document_id, + 'user_id': self._user_id, + 'assigned_user_id': self._assigned_user_id, + 'project_id': self._project_id, + 'vendor_id': self._vendor_id, + 'name': self._name, + 'url': self._url, + 'preview': self._preview, + 'document_type': self._document_type, + 'disk': self._disk, + 'document_hash': self._document_hash, + 'is_deleted': self._is_deleted, + 'is_default': self._is_default, + 'created_at': self._created_at, + 'updated_at': self._updated_at, + 'deleted_at': self._deleted_at}) + + def __str__(self): + return f'Document({self._document_id}, {self._name})' +