Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tests/test_invoices.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import time
import uuid
from datetime import date, timedelta
from dotenv import load_dotenv
load_dotenv()

from twikey.model.invoice_request import Customer, InvoiceRequest, LineItem, UpdateInvoiceRequest, DetailsRequest, \
ActionRequest, ActionType, UblUploadRequest, BulkInvoiceRequest
Expand Down Expand Up @@ -220,6 +222,11 @@ def test_feed(self):
def test_payments(self):
self._twikey.invoice.payment(MyPayments(), False)

def test_retrieve_pdf(self):
retrieved_pdf = self._twikey.invoice.retrieve_pdf(os.environ["INVOICEID"])
retrieved_pdf.save("/tmp/pdf.pdf")
self.assertIsNotNone(retrieved_pdf)


class MyFeed(twikey.InvoiceFeed):
def invoice(self, invoice:Invoice):
Expand Down
35 changes: 34 additions & 1 deletion twikey/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .model.invoice_request import InvoiceRequest, UpdateInvoiceRequest, DetailsRequest, ActionRequest, \
UblUploadRequest, BulkInvoiceRequest
from .model.invoice_response import Event, Invoice, BulkInvoiceResponse, \
BulkBatchDetailsResponse, InvoiceFeed, PaymentFeed
BulkBatchDetailsResponse, InvoiceFeed, PaymentFeed, PdfResponse

class InvoiceService(object):
def __init__(self, client) -> None:
Expand Down Expand Up @@ -433,3 +433,36 @@ def payment(self, payment_feed: PaymentFeed, start_position=False):
except requests.exceptions.RequestException as e:
raise self.client.raise_error_from_request("Payment feed", e)

def retrieve_pdf(self, invoice_id: str) -> PdfResponse:
"""
See https://www.twikey.com/api/#retrieve-invoice-pdf

retrieve the PDF of an invoice via GET request to the API

Args:
invoice_id (str): The UUID of the invoice

Returns:
PdfResponse: A structured response object representing the server’s reply.

Raises:
Exception: If the request to the PDF endpoint fails or response is invalid.
"""

url = self.client.instance_url(f"/invoice/{invoice_id}/pdf")
try:
self.client.refresh_token_if_required()
response = requests.get(
url=url, headers=self.client.headers(), timeout=15
)
if "ApiErrorCode" in response.headers:
raise self.client.raise_error("pdf", response)
filename = None
if "Content-Disposition" in response.headers:
disposition = response.headers["Content-Disposition"]
parts = disposition.split("=")
if len(parts) == 2:
filename = parts[1].strip().strip('"')
return PdfResponse(content=response.content, filename=filename)
except requests.exceptions.RequestException as e:
raise self.client.raise_error_from_request("detail", e)
16 changes: 16 additions & 0 deletions twikey/model/invoice_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,19 @@ def __init__(self, raw: list):

def __str__(self):
return "\n".join(str(item) for item in self.results)


class PdfResponse:
def __init__(self, content: bytes, filename: str = None, content_type: str = "application/pdf"):
self.content = content
self.content_type = content_type
self.filename = filename or "invoice.pdf"

def save(self, path: str = None):
path = path or self.filename
with open(path, "wb") as f:
f.write(self.content)
return path

def __str__(self):
return f"PdfResponse(filename='{self.filename}', size={len(self.content)} bytes)"