Merge pull request #2 from Eledio/feature/obtainPDFInvoice

Obtain PDF version of the invoice
This commit is contained in:
Richard Kubíček
2025-03-04 14:56:18 +01:00
committed by GitHub
11 changed files with 398 additions and 130 deletions

View File

@@ -4,31 +4,31 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html # https://www.sphinx-doc.org/en/master/usage/configuration.html
import os import os
import sys import sys
sys.path.insert(0, os.path.abspath('../..'))
sys.path.insert(0, os.path.abspath("../.."))
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'SuperFaktura API client' project = "SuperFaktura API client"
copyright = '2025, Richard Kubíček, Eledio s.r.o.' copyright = "2025, Richard Kubíček, Eledio s.r.o."
author = 'Richard Kubíček' author = "Richard Kubíček"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [ extensions = [
'sphinx.ext.autodoc', "sphinx.ext.autodoc",
'sphinx.ext.napoleon', "sphinx.ext.napoleon",
'sphinx.ext.autosummary', "sphinx.ext.autosummary",
] ]
templates_path = ['_templates'] templates_path = ["_templates"]
exclude_patterns = [] exclude_patterns = []
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_rtd_theme' html_theme = "sphinx_rtd_theme"
html_static_path = ['_static'] html_static_path = ["_static"]

19
docs/source/examples.rst Normal file
View File

@@ -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:

View File

@@ -6,3 +6,4 @@ Welcome to SuperFaktura Client's documentation
superfaktura superfaktura
installation installation
examples

87
examples/add_invoice.py Normal file
View File

@@ -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()

View File

@@ -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()

5
ruff.toml Normal file
View File

@@ -0,0 +1,5 @@
[lint]
ignore = [
# Use a single `if` statement instead of nested `if` statements
"SIM102",
]

View File

@@ -1,15 +1,12 @@
from setuptools import setup, find_namespace_packages from setuptools import setup, find_namespace_packages
setup( setup(
name='superfaktura_client', name="superfaktura_client",
version='0.1.0', version="0.1.0",
packages=find_namespace_packages(), packages=find_namespace_packages(),
url='https://github.com/eledio-helpers/superfaktura-client', url="https://github.com/eledio-helpers/superfaktura-client",
license='GNU GPLv3', license="MIT",
author='Richard Kubíček', author="Richard Kubíček",
author_email='kubicekr@eledio.com', author_email="kubicekr@eledio.com",
install_requires=[ install_requires=["requests", "python-dotenv"],
"requests",
"python-dotenv"
],
) )

View File

@@ -15,20 +15,16 @@ Functions:
- (none) - (none)
Usage: Usage:
import superfaktura.bank_account >>> import superfaktura.bank_account
>>> # Create an instance of BankAccount
# Create an instance of BankAccount >>> bank = superfaktura.bank_account.BankAccount()
bank = superfaktura.bank_account.BankAccount() >>> # Retrieve a list of bank accounts
>>> accounts = bank.list()
# Retrieve a list of bank accounts >>> # Get the default bank account
accounts = bank.list() >>> default_account = bank.default()
>>> # Create or update a bank account
# Get the default bank account >>> data = {"account": "1234567890", "bank_code": "1234567890", "default": True}
default_account = bank.default() >>> bank.post(data)
# Create or update a bank account
data = {"account": "1234567890", "bank_code": "1234567890", "default": True}
bank.post(data)
""" """
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
@@ -83,11 +79,11 @@ class BankAccount(SuperFakturaAPI):
- post: Creates or updates a bank account. - post: Creates or updates a bank account.
Usage: Usage:
bank = BankAccount() >>> bank = BankAccount()
accounts = bank.list() >>> accounts = bank.list()
default_account = bank.default() >>> default_account = bank.default()
data = {"account": "1234567890", "bank_code": "1234567890", "default": True} >>> data = {"account": "1234567890", "bank_code": "1234567890", "default": True}
bank.post(data) >>> bank.post(data)
""" """
def __init__(self): def __init__(self):

View File

@@ -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"

View File

@@ -16,49 +16,45 @@ Functions:
- (none) - (none)
Usage: Usage:
import superfaktura.invoice >>> import superfaktura.invoice
>>> # Create an instance of Invoice
# Create an instance of Invoice >>> invoice = superfaktura.invoice.Invoice()
invoice = superfaktura.invoice.Invoice() >>> # Create an invoice
>>> invoice.add(
# Create an invoice invoice_model=InvoiceModel(
invoice.add( type=InvoiceType.INVOICE,
invoice=superfaktura.invoice.InvoiceModel( name="My First Invoice",
type=superfaktura.invoice.InvoiceType.PROFORMA, due=Date("2025-04-01"),
name="Invoice 3", invoice_currency=Currencies.EUR,
due=superfaktura.invoice.Date("2025-02-01"),
invoice_currency=superfaktura.invoice.Currencies.CZK,
header_comment="We invoice you for services", header_comment="We invoice you for services",
bank_accounts=[bank.default().as_dict()], bank_accounts=[bank.default().as_dict()],
), ),
items=[ items=[
superfaktura.invoice.InvoiceItem(name="Services", unit_price=100, quantity=1, InvoiceItem(
unit="ks", tax=21), name="Website Development", unit_price=1000.0, quantity=1, tax=20
superfaktura.invoice.InvoiceItem(name="SIM card", unit_price=50, quantity=1, ),
tax=21, unit="ks"), InvoiceItem(
superfaktura.invoice.InvoiceItem( name="Hosting Service (1 year)", unit_price=500.0, quantity=1, tax=20
name="SIM card 2", unit_price=75, quantity=1, tax=21, unit="ks"
), ),
], ],
contact=superfaktura.client_contacts.ClientContactModel( contact=ClientContactModel(
name="Richard Kubíček", name="John Doe",
email="kubicekr@eledio.com", email="john.doe@examle.com",
phone="+420 123 456 789", phone="+1 555-1234",
address="Jaroslava Foglara 861/1", address="123 Main Street, New York",
ico="123", ico="987654321",
update=True, update=True,
country_id=57, country_id=225,
), ),
) )
""" """
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from typing import Optional, List from typing import Optional, List, IO
import json import json
from superfaktura.bank_account import BankAccount
from superfaktura.client_contacts import ClientContactModel 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.superfaktura_api import SuperFakturaAPI
from superfaktura.utils.data_types import Date, DateEncoder from superfaktura.utils.data_types import Date, DateEncoder
@@ -147,8 +143,51 @@ class InvoiceItem:
return data 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: class InvoiceType:
""" " """
Invoice Type Enumeration. Invoice Type Enumeration.
This enumeration represents the different types of invoices that can be created. This enumeration represents the different types of invoices that can be created.
@@ -175,31 +214,32 @@ class Invoice(SuperFakturaAPI):
- update: Updates an existing invoice. - update: Updates an existing invoice.
Usage: Usage:
invoice = Invoice() >>> invoice = Invoice()
invoice.add( >>> invoice.add(
invoice=InvoiceModel( invoice_model=InvoiceModel(
type=InvoiceType.PROFORMA, type=InvoiceType.INVOICE,
name="Invoice 3", name="My First Invoice",
due=Date("2025-02-01"), due=Date("2025-04-01"),
invoice_currency=Currencies.CZK, invoice_currency=Currencies.EUR,
header_comment="We invoice you for services", header_comment="We invoice you for services",
bank_accounts=[bank.default().as_dict()], bank_accounts=[bank.default().as_dict()],
), ),
items=[ 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( 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( contact=ClientContactModel(
name="Richard Kubíček", name="John Doe",
email="kubicekr@eledio.com", email="john.doe@example.com",
phone="+420 123 456 789", phone="+1 555-1234",
address="Jaroslava Foglara 861/1", address="123 Main Street, New York",
ico="123", ico="987654321",
update=True, update=True,
country_id=57, country_id=225,
), ),
) )
""" """
@@ -212,44 +252,53 @@ class Invoice(SuperFakturaAPI):
invoice_model: InvoiceModel, invoice_model: InvoiceModel,
items: List[InvoiceItem], items: List[InvoiceItem],
contact: ClientContactModel, contact: ClientContactModel,
): invoice_settings: Optional[InvoiceSettings] = None,
"""Creates a new invoice.""" ) -> 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 = { data = {
"Invoice": invoice_model.as_dict(), "Invoice": invoice_model.as_dict(),
"InvoiceItem": [item.as_dict() for item in items], "InvoiceItem": [item.as_dict() for item in items],
"Client": contact.as_dict(), "Client": contact.as_dict(),
"InvoiceSetting": invoice_settings.as_dict() if invoice_settings else {},
} }
url = "invoices/create" url = "invoices/create"
resp = self.post(endpoint=url, data=json.dumps(data, cls=DateEncoder)) 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__": Args:
invoice = Invoice() invoice (InvoiceRespModel): The response model for the invoice.
bank = BankAccount() descriptor (IO[bytes]): The descriptor to write the PDF data to.
invoice.add( language (str): The language for the PDF.
invoice_model=InvoiceModel(
type=InvoiceType.PROFORMA, Returns:
name="Invoice 3", None
due=Date("2025-02-01"), """
invoice_currency=Currencies.CZK, url = f"{language}/invoices/pdf/{invoice.invoice_id}/token:{invoice.invoice_token}"
header_comment="We invoice you for services", self.download(url, descriptor)
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,
),
)

View File

@@ -14,20 +14,17 @@ Functions:
- post: Creates or updates data in the SuperFaktura API. - post: Creates or updates data in the SuperFaktura API.
Usage: Usage:
import superfaktura.superfaktura_api >>> import superfaktura.superfaktura_api
>>> # Create an instance of SuperFakturaAPI
# Create an instance of SuperFakturaAPI >>> api = superfaktura.superfaktura_api.SuperFakturaAPI()
api = superfaktura.superfaktura_api.SuperFakturaAPI() >>> # Retrieve data from the SuperFaktura API
>>> incoming_data = api.get('endpoint')
# Retrieve data from the SuperFaktura API >>> # Create or update data in the SuperFaktura API
data = api.get('endpoint') >>> api.post('endpoint', outgoing_data)
# Create or update data in the SuperFaktura API
api.post('endpoint', data)
""" """
import os import os
from typing import Dict from typing import Dict, IO
import requests import requests
from dotenv import load_dotenv # type: ignore from dotenv import load_dotenv # type: ignore
@@ -90,11 +87,56 @@ class SuperFakturaAPI:
url = f"{self._api_url}/{endpoint}" url = f"{self._api_url}/{endpoint}"
req = requests.get(url=url, headers=self._auth_header, timeout=timeout) req = requests.get(url=url, headers=self._auth_header, timeout=timeout)
if req.status_code == 200: 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( 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: def post(self, endpoint: str, data: str, timeout: int = 5) -> Dict:
""" """
Creates or updates data in the SuperFaktura API. Creates or updates data in the SuperFaktura API.