diff --git a/docs/source/conf.py b/docs/source/conf.py index dd44d06..9c991a5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,31 +4,31 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html import os import sys -sys.path.insert(0, os.path.abspath('../..')) + +sys.path.insert(0, os.path.abspath("../..")) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'SuperFaktura API client' -copyright = '2025, Richard Kubíček, Eledio s.r.o.' -author = 'Richard Kubíček' +project = "SuperFaktura API client" +copyright = "2025, Richard Kubíček, Eledio s.r.o." +author = "Richard Kubíček" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.autosummary', + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.autosummary", ] -templates_path = ['_templates'] +templates_path = ["_templates"] exclude_patterns = [] - # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'sphinx_rtd_theme' -html_static_path = ['_static'] +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] diff --git a/docs/source/examples.rst b/docs/source/examples.rst new file mode 100644 index 0000000..d0c5e94 --- /dev/null +++ b/docs/source/examples.rst @@ -0,0 +1,19 @@ +Examples of SuperFaktura API client usage +==================== + + +Add Invoice +--------------------------------- + +.. automodule:: examples.add_invoice + :members: + :undoc-members: + :show-inheritance: + +Get country list +------------------------------------ + +.. automodule:: examples.get_country_list + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/index.rst b/docs/source/index.rst index f27d38e..4f2a10f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,3 +6,4 @@ Welcome to SuperFaktura Client's documentation superfaktura installation + examples diff --git a/examples/add_invoice.py b/examples/add_invoice.py new file mode 100644 index 0000000..8fb8e9a --- /dev/null +++ b/examples/add_invoice.py @@ -0,0 +1,87 @@ +""" +Main script to add an invoice and save it as a PDF using the SuperFaktura API. + +This script demonstrates how to create an invoice with multiple items, +retrieve the invoice as a PDF, and save the PDF to a file. + +Usage: + Run this script directly to create and save an invoice PDF. + +Dependencies: + - superfaktura.bank_account.BankAccount + - superfaktura.client_contacts.ClientContactModel + - superfaktura.enumerations.currency.Currencies + - superfaktura.enumerations.language.Language + - superfaktura.invoice.Invoice + - superfaktura.invoice.InvoiceModel + - superfaktura.invoice.InvoiceType + - superfaktura.invoice.InvoiceItem + - superfaktura.invoice.InvoiceSettings + - superfaktura.utils.data_types.Date +""" + +from superfaktura.bank_account import BankAccount, NoDefaultBankAccountException +from superfaktura.client_contacts import ClientContactModel +from superfaktura.enumerations.currency import Currencies +from superfaktura.enumerations.language import Language +from superfaktura.invoice import ( + Invoice, + InvoiceModel, + InvoiceType, + InvoiceItem, + InvoiceSettings, +) +from superfaktura.utils.data_types import Date + + +def main(): + """ + Main function to add Invoice and save it as a pdf using the SuperFaktura API. + """ + invoice = Invoice() + bank = BankAccount() + try: + # Get default bank account + bank_account = bank.default().as_dict() + except NoDefaultBankAccountException as e: + print(f"Error getting default bank account: {e}") + bank_account = {} + resp = invoice.add( + invoice_model=InvoiceModel( + type=InvoiceType.INVOICE, + name="My First Invoice", + due=Date("2025-04-01"), + invoice_currency=Currencies.EUR, + header_comment="We invoice you for services", + bank_accounts=[bank_account], + ), + items=[ + InvoiceItem( + name="Website Development", unit_price=1000.0, quantity=1, tax=20 + ), + InvoiceItem( + name="Hosting Service (1 year)", unit_price=500.0, quantity=1, tax=20 + ), + ], + contact=ClientContactModel( + name="John Doe", + email="john.doe@examle.com", + phone="+1 555-1234", + address="123 Main Street, New York", + ico="987654321", + update=True, + country_id=225, + ), + invoice_settings=InvoiceSettings(language=Language.English), + ) + + try: + with open("invoice.pdf", "wb") as f: + invoice.get_pdf(invoice=resp, descriptor=f, language=Language.English) + print("Invoice saved to 'invoice.pdf'") + except Exception as e: + print(f"Error generating or saving invoice as PDF: {e}") + + +if __name__ == "__main__": + main() diff --git a/examples/get_country_list.py b/examples/get_country_list.py new file mode 100644 index 0000000..208a7c8 --- /dev/null +++ b/examples/get_country_list.py @@ -0,0 +1,18 @@ +""" +This script retrieves and prints the list of countries using the SuperFaktura API. +""" + +from pprint import pprint + +from superfaktura.utils.country import country_list + + +def main(): + """ + Main function to retrieve and print the list of countries using the SuperFaktura API. + """ + pprint(country_list()) + + +if __name__ == "__main__": + main() diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..d02b9fd --- /dev/null +++ b/ruff.toml @@ -0,0 +1,5 @@ +[lint] +ignore = [ + # Use a single `if` statement instead of nested `if` statements + "SIM102", +] \ No newline at end of file diff --git a/setup.py b/setup.py index 7cf55a7..bc516cd 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,12 @@ from setuptools import setup, find_namespace_packages setup( - name='superfaktura_client', - version='0.1.0', + name="superfaktura_client", + version="0.1.0", packages=find_namespace_packages(), - url='https://github.com/eledio-helpers/superfaktura-client', - license='GNU GPLv3', - author='Richard Kubíček', - author_email='kubicekr@eledio.com', - install_requires=[ - "requests", - "python-dotenv" - ], + url="https://github.com/eledio-helpers/superfaktura-client", + license="MIT", + author="Richard Kubíček", + author_email="kubicekr@eledio.com", + install_requires=["requests", "python-dotenv"], ) diff --git a/superfaktura/bank_account.py b/superfaktura/bank_account.py index 748ba85..32546a5 100644 --- a/superfaktura/bank_account.py +++ b/superfaktura/bank_account.py @@ -15,20 +15,16 @@ Functions: - (none) Usage: - import superfaktura.bank_account - - # Create an instance of BankAccount - bank = superfaktura.bank_account.BankAccount() - - # Retrieve a list of bank accounts - accounts = bank.list() - - # Get the default bank account - default_account = bank.default() - - # Create or update a bank account - data = {"account": "1234567890", "bank_code": "1234567890", "default": True} - bank.post(data) + >>> import superfaktura.bank_account + >>> # Create an instance of BankAccount + >>> bank = superfaktura.bank_account.BankAccount() + >>> # Retrieve a list of bank accounts + >>> accounts = bank.list() + >>> # Get the default bank account + >>> default_account = bank.default() + >>> # Create or update a bank account + >>> data = {"account": "1234567890", "bank_code": "1234567890", "default": True} + >>> bank.post(data) """ from dataclasses import dataclass, asdict @@ -83,11 +79,11 @@ class BankAccount(SuperFakturaAPI): - post: Creates or updates a bank account. Usage: - bank = BankAccount() - accounts = bank.list() - default_account = bank.default() - data = {"account": "1234567890", "bank_code": "1234567890", "default": True} - bank.post(data) + >>> bank = BankAccount() + >>> accounts = bank.list() + >>> default_account = bank.default() + >>> data = {"account": "1234567890", "bank_code": "1234567890", "default": True} + >>> bank.post(data) """ def __init__(self): diff --git a/superfaktura/enumerations/language.py b/superfaktura/enumerations/language.py new file mode 100644 index 0000000..34ab924 --- /dev/null +++ b/superfaktura/enumerations/language.py @@ -0,0 +1,54 @@ +""" +Language Enumeration. + +This module provides an enumeration of languages that can be used in the SuperFaktura API. + +Classes: + - Language: Enumeration of languages. + +Usage: + from superfaktura.enumerations.language import Language + language = Language.Czech +""" + + +class Language: + """ + Language Enumeration. + + This enumeration represents the different languages that can be used in the SuperFaktura API. + + Values: + - Czech: Czech + - German: German + - English: English + - Croatian: Croatian + - Hungarian: Hungarian + - Italian: Italian + - Dutch: Dutch + - Polish: Polish + - Romanian: Romanian + - Russian: Russian + - Slovak: Slovak + - Slovene: Slovene + - Spanish: Spanish + - Ukrainian: Ukrainian + + Usage: + language = Language.Czech + """ + + Czech = "cze" + German = "deu" + English = "eng" + Croatian = "hrv" + Hungarian = "hun" + Italian = "ita" + Dutch = "nld" + Polish = "pol" + Romanian = "rom" + Russian = "rus" + Slovak = "slo" + Slovene = "slv" + Spanish = "spa" + Ukrainian = "ukr" diff --git a/superfaktura/invoice.py b/superfaktura/invoice.py index 4e5ab62..452f7f2 100644 --- a/superfaktura/invoice.py +++ b/superfaktura/invoice.py @@ -16,49 +16,45 @@ Functions: - (none) Usage: - import superfaktura.invoice - - # Create an instance of Invoice - invoice = superfaktura.invoice.Invoice() - - # Create an invoice - invoice.add( - invoice=superfaktura.invoice.InvoiceModel( - type=superfaktura.invoice.InvoiceType.PROFORMA, - name="Invoice 3", - due=superfaktura.invoice.Date("2025-02-01"), - invoice_currency=superfaktura.invoice.Currencies.CZK, + >>> import superfaktura.invoice + >>> # Create an instance of Invoice + >>> invoice = superfaktura.invoice.Invoice() + >>> # Create an invoice + >>> invoice.add( + invoice_model=InvoiceModel( + type=InvoiceType.INVOICE, + name="My First Invoice", + due=Date("2025-04-01"), + invoice_currency=Currencies.EUR, header_comment="We invoice you for services", bank_accounts=[bank.default().as_dict()], ), items=[ - superfaktura.invoice.InvoiceItem(name="Services", unit_price=100, quantity=1, - unit="ks", tax=21), - superfaktura.invoice.InvoiceItem(name="SIM card", unit_price=50, quantity=1, - tax=21, unit="ks"), - superfaktura.invoice.InvoiceItem( - name="SIM card 2", unit_price=75, quantity=1, tax=21, unit="ks" + InvoiceItem( + name="Website Development", unit_price=1000.0, quantity=1, tax=20 + ), + InvoiceItem( + name="Hosting Service (1 year)", unit_price=500.0, quantity=1, tax=20 ), ], - contact=superfaktura.client_contacts.ClientContactModel( - name="Richard Kubíček", - email="kubicekr@eledio.com", - phone="+420 123 456 789", - address="Jaroslava Foglara 861/1", - ico="123", + contact=ClientContactModel( + name="John Doe", + email="john.doe@examle.com", + phone="+1 555-1234", + address="123 Main Street, New York", + ico="987654321", update=True, - country_id=57, + country_id=225, ), ) """ from dataclasses import dataclass, asdict -from typing import Optional, List +from typing import Optional, List, IO import json -from superfaktura.bank_account import BankAccount from superfaktura.client_contacts import ClientContactModel -from superfaktura.enumerations.currency import Currencies +from superfaktura.enumerations.language import Language from superfaktura.superfaktura_api import SuperFakturaAPI from superfaktura.utils.data_types import Date, DateEncoder @@ -147,8 +143,51 @@ class InvoiceItem: return data +@dataclass +class InvoiceRespModel: + """ + This dataclass represents the response model for an invoice in the SuperFaktura API. + + Attributes: + - error (int): The error code. + - error_message (str): The error message. + - invoice_id (Optional[int]): The ID of the invoice. + - invoice_token (Optional[str]): The token of the invoice. + """ + + error: int + error_message: str + invoice_id: Optional[int] = None + invoice_token: Optional[str] = None + + +@dataclass +class InvoiceSettings: + """ + This dataclass represents the settings for an invoice in the SuperFaktura API. + """ + + language: Optional[str] = None + bysquare: Optional[bool] = None + callback_payment: Optional[str] = None + online_payment: Optional[bool] = None + payment_info: Optional[bool] = None + paypal: Optional[bool] = None + show_prices: Optional[bool] = None + signature: Optional[bool] = None + summary_bg_color: Optional[str] = None + + def as_dict(self) -> dict: + """Returns a dictionary representation of the InvoiceSettings.""" + data = asdict(self) + for key in list(data.keys()): + if data[key] is None: + del data[key] + return data + + class InvoiceType: - """ " + """ Invoice Type Enumeration. This enumeration represents the different types of invoices that can be created. @@ -175,31 +214,32 @@ class Invoice(SuperFakturaAPI): - update: Updates an existing invoice. Usage: - invoice = Invoice() - invoice.add( - invoice=InvoiceModel( - type=InvoiceType.PROFORMA, - name="Invoice 3", - due=Date("2025-02-01"), - invoice_currency=Currencies.CZK, + >>> invoice = Invoice() + >>> invoice.add( + invoice_model=InvoiceModel( + type=InvoiceType.INVOICE, + name="My First Invoice", + due=Date("2025-04-01"), + invoice_currency=Currencies.EUR, header_comment="We invoice you for services", bank_accounts=[bank.default().as_dict()], ), items=[ - InvoiceItem(name="Services", unit_price=100, quantity=1, unit="ks", tax=21), - InvoiceItem(name="SIM card", unit_price=50, quantity=1, tax=21, unit="ks"), InvoiceItem( - name="SIM card 2", unit_price=75, quantity=1, tax=21, unit="ks" + name="Website Development", unit_price=1000.0, quantity=1, tax=20 + ), + InvoiceItem( + name="Hosting Service (1 year)", unit_price=500.0, quantity=1, tax=20 ), ], contact=ClientContactModel( - name="Richard Kubíček", - email="kubicekr@eledio.com", - phone="+420 123 456 789", - address="Jaroslava Foglara 861/1", - ico="123", + name="John Doe", + email="john.doe@example.com", + phone="+1 555-1234", + address="123 Main Street, New York", + ico="987654321", update=True, - country_id=57, + country_id=225, ), ) """ @@ -212,44 +252,53 @@ class Invoice(SuperFakturaAPI): invoice_model: InvoiceModel, items: List[InvoiceItem], contact: ClientContactModel, - ): - """Creates a new invoice.""" + invoice_settings: Optional[InvoiceSettings] = None, + ) -> InvoiceRespModel: + """ + Adds a new invoice. + + Args: + invoice_model (InvoiceModel): The invoice model. + items (List[InvoiceItem]): List of invoice items. + contact (ClientContactModel): The client contact model. + invoice_settings (Optional[InvoiceSettings]): The invoice settings. + + Returns: + InvoiceRespModel: The response model for the invoice. + """ data = { "Invoice": invoice_model.as_dict(), "InvoiceItem": [item.as_dict() for item in items], "Client": contact.as_dict(), + "InvoiceSetting": invoice_settings.as_dict() if invoice_settings else {}, } url = "invoices/create" resp = self.post(endpoint=url, data=json.dumps(data, cls=DateEncoder)) - return resp + invoice_resp = InvoiceRespModel( + error=resp["error"], error_message=resp["error_message"] + ) + if "data" in resp: + if "Invoice" in resp["data"]: + invoice_resp.invoice_id = int(resp["data"]["Invoice"]["id"]) + invoice_resp.invoice_token = resp["data"]["Invoice"]["token"] + return invoice_resp + def get_pdf( + self, + invoice: InvoiceRespModel, + descriptor: IO[bytes], + language: str = Language.Czech, + ) -> None: + """ + Retrieves the PDF of the invoice. -if __name__ == "__main__": - invoice = Invoice() - bank = BankAccount() - invoice.add( - invoice_model=InvoiceModel( - type=InvoiceType.PROFORMA, - name="Invoice 3", - due=Date("2025-02-01"), - invoice_currency=Currencies.CZK, - header_comment="We invoice you for services", - bank_accounts=[bank.default().as_dict()], - ), - items=[ - InvoiceItem(name="Services", unit_price=100, quantity=1, unit="ks", tax=21), - InvoiceItem(name="SIM card", unit_price=50, quantity=1, tax=21, unit="ks"), - InvoiceItem( - name="SIM card 2", unit_price=75, quantity=1, tax=21, unit="ks" - ), - ], - contact=ClientContactModel( - name="Richard Kubíček", - email="kubicekr@eledio.com", - phone="+420 123 456 789", - address="Jaroslava Foglara 861/1", - ico="123", - update=True, - country_id=57, - ), - ) + Args: + invoice (InvoiceRespModel): The response model for the invoice. + descriptor (IO[bytes]): The descriptor to write the PDF data to. + language (str): The language for the PDF. + + Returns: + None + """ + url = f"{language}/invoices/pdf/{invoice.invoice_id}/token:{invoice.invoice_token}" + self.download(url, descriptor) diff --git a/superfaktura/superfaktura_api.py b/superfaktura/superfaktura_api.py index 9070e42..05d3adc 100644 --- a/superfaktura/superfaktura_api.py +++ b/superfaktura/superfaktura_api.py @@ -14,20 +14,17 @@ Functions: - post: Creates or updates data in the SuperFaktura API. Usage: - import superfaktura.superfaktura_api - - # Create an instance of SuperFakturaAPI - api = superfaktura.superfaktura_api.SuperFakturaAPI() - - # Retrieve data from the SuperFaktura API - data = api.get('endpoint') - - # Create or update data in the SuperFaktura API - api.post('endpoint', data) + >>> import superfaktura.superfaktura_api + >>> # Create an instance of SuperFakturaAPI + >>> api = superfaktura.superfaktura_api.SuperFakturaAPI() + >>> # Retrieve data from the SuperFaktura API + >>> incoming_data = api.get('endpoint') + >>> # Create or update data in the SuperFaktura API + >>> api.post('endpoint', outgoing_data) """ import os -from typing import Dict +from typing import Dict, IO import requests from dotenv import load_dotenv # type: ignore @@ -90,11 +87,56 @@ class SuperFakturaAPI: url = f"{self._api_url}/{endpoint}" req = requests.get(url=url, headers=self._auth_header, timeout=timeout) if req.status_code == 200: - return req.json() + try: + return req.json() + except requests.exceptions.JSONDecodeError as e: + raise SuperFakturaAPIException( + f"Unable to decode response as JSON; {req.content!r}; {e}" + ) from e raise SuperFakturaAPIException( - f"Get status code: {req.status_code}; {req.json()}" + f"Get status code: {req.status_code}; {req.content!r}" ) + def download(self, endpoint: str, descriptor: IO[bytes], timeout: int = 5) -> None: + """ + Download data stream from the SuperFaktura API. + + Download data stream from the specified endpoint in the SuperFaktura API. + + Args: + endpoint (str): The API endpoint to retrieve data from (e.g. 'invoices', 'clients', + etc.). + descriptor (IO[bytes]): The descriptor to write the data stream to. + timeout (int, optional): The timeout for the API request in seconds. Defaults to 5. + + Returns: + None + + Raises: + SuperFakturaAPIException: If the API request fails or returns an error. + + Examples: + >>> api = SuperFakturaAPI() + >>> with open("data.pdf", "wb") as f: + >>> api.download("some/endpoint", descriptor=f) + + Notes: + The available endpoints can be found in the SuperFaktura API documentation. + """ + url = f"{self._api_url}/{endpoint}" + req = requests.get(url=url, headers=self._auth_header, timeout=timeout) + if req.status_code == 200: + if descriptor.writable(): + descriptor.write(req.content) + else: + raise SuperFakturaAPIException( + f"Descriptor {descriptor} is not writable" + ) + else: + raise SuperFakturaAPIException( + f"Download status code: {req.status_code}; {req.content!r}" + ) + def post(self, endpoint: str, data: str, timeout: int = 5) -> Dict: """ Creates or updates data in the SuperFaktura API.