I am trying to generate an invoice from a firebase payload in a gcp function using borb. The error I am getting is that the rectangle must have non-negative height.
The gcp function is:
from borb.pdf.canvas.layout.page_layout.multi_column_layout import SingleColumnLayout
from borb.pdf.canvas.layout.page_layout.page_layout import PageLayout
from borb.pdf.canvas.layout.text.paragraph import Paragraph
from borb.pdf.document.document import Document
from borb.pdf.page.page import Page
from borb.pdf.pdf import PDF
from borb.pdf.canvas.layout.page_layout.multi_column_layout import SingleColumnLayout
from decimal import Decimal
from borb.pdf.canvas.layout.table.fixed_column_width_table import (
FixedColumnWidthTable as Table,
)
from borb.pdf.canvas.layout.layout_element import Alignment
from borb.pdf.canvas.layout.image.image import Image
from datetime import datetime
import random
from borb.pdf.pdf import PDF
from borb.pdf.canvas.color.color import HexColor, X11Color
from borb.pdf.canvas.layout.table.fixed_column_width_table import (
FixedColumnWidthTable as Table,
)
from borb.pdf.canvas.layout.table.table import TableCell
from borb.pdf import Page
import json
from google.cloud import storage
from datetime import datetime
import firebase_admin
from firebase_admin import credentials
from firebase_admin import firestore
import logging
# Pendiente de enviar email al cliente final, guardar dentro de invoices capreta por mes en cloud storage y luego emitiar factura excel contasol y de poner en el nombre el id del comprador y de acbar de poner tablas de precios etc.
# Global Variables
export_time = datetime.now().strftime("%d_%m_%Y")
invoice_date_month = datetime.now().strftime("%m_%Y")
# Use the application default credentials.
cred = credentials.ApplicationDefault()
firebase_admin.initialize_app(cred)
db = firestore.client()
logging.basicConfig(format='%(message)s')
# Function
def generate_pdf_invoice(data, context):
""" Triggered by a change to a Firestore document.
Args:
data (dict): The event payload.
context (google.cloud.functions.Context): Metadata for the event.
"""
logging.warning(data)
logging.warning(context)
# Increase the padding for the cells to ensure data fits comfortably
padding_value = Decimal(5) # Increase as needed
# Build Table
table_001 = Table(number_of_rows=5, number_of_columns=4)
table_001.add(
Paragraph(
"Raó Social", font="Helvetica-Bold", horizontal_alignment=Alignment.LEFT
)
)
table_001.add(
Paragraph(
data["value"]["fields"]["seller_details"]["mapValue"]["fields"][
"seller_name"
]["stringValue"]
)
)
table_001.add(
Paragraph("Data", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT)
)
now = datetime.now()
table_001.add(Paragraph("%d/%d/%d" % (now.day, now.month, now.year)))
table_001.add(
Paragraph("Carrer", font="Helvetica-Bold", horizontal_alignment=Alignment.LEFT)
)
table_001.add(
Paragraph(
data["value"]["fields"]["seller_details"]["mapValue"]["fields"]["seller_address_street"]["stringValue"]
)
)
table_001.add(
Paragraph(
"Factura #", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT,
)
)
table_001.add(
Paragraph(str(data["value"]["fields"]["invoice_number"]["integerValue"]))
)
table_001.add(
Paragraph(
"Població", font="Helvetica-Bold", horizontal_alignment=Alignment.LEFT
)
)
table_001.add(
Paragraph(
data["value"]["fields"]["seller_details"]["mapValue"]["fields"]["seller_address_city_postal_code_country"]["stringValue"]
)
)
table_001.add(
Paragraph(
"Telèfon", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT
)
)
table_001.add(
Paragraph(
data["value"]["fields"]["seller_details"]["mapValue"]["fields"][
"seller_phone"
]["stringValue"]
)
)
table_001.add(
Paragraph(
"Venciment", font="Helvetica-Bold", horizontal_alignment=Alignment.LEFT
)
)
table_001.add(Paragraph("%d/%d/%d" % (now.day+4, now.month, now.year)))
table_001.add(
Paragraph("Email", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT)
)
table_001.add(
Paragraph(
data["value"]["fields"]["seller_details"]["mapValue"]["fields"][
"seller_email"
]["stringValue"]
)
)
table_001.add(
Paragraph(
"BIC", font="Helvetica-Bold", horizontal_alignment=Alignment.LEFT
)
)
table_001.add(Paragraph(data["value"]["fields"]["seller_details"]["mapValue"]["fields"]["seller_BIC"]["stringValue"]))
small_font_size = 8 # Change as per your requirement
table_001.add(
Paragraph(
"IBAN", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT
)
)
table_001.add(Paragraph(data["value"]["fields"]["seller_details"]["mapValue"]["fields"]["seller_IBAN"]["stringValue"], font_size=small_font_size))
table_001.set_padding_on_all_cells(padding_value, padding_value, padding_value, padding_value)
table_001.no_borders()
#Billing and shipping information
table_002 = Table(number_of_rows=6, number_of_columns=1)
table_002.add(
Paragraph(
"Facturat a",
background_color=HexColor("263238"),
font_color=X11Color("White"),
)
)
# BILLING
table_002.add(
Paragraph(data["value"]["fields"]["buyer_details"]["mapValue"]["fields"]["buyer_name"]["stringValue"])
) # BILLING
table_002.add(
Paragraph(data["value"]["fields"]["buyer_details"]["mapValue"]["fields"]["buyer_address_street"]["stringValue"])
) # BILLING
table_002.add(
Paragraph(
data["value"]["fields"]["buyer_details"]["mapValue"]["fields"]["buyer_address_city_postal_code_country"]["stringValue"]
)
) # BILLING
table_002.add(
Paragraph(data["value"]["fields"]["buyer_details"]["mapValue"]["fields"]["buyer_phone"]["stringValue"])
) # BILLING
table_002.add(
Paragraph(data["value"]["fields"]["buyer_details"]["mapValue"]["fields"]["buyer_email"]["stringValue"])
) # BILLING
table_002.set_padding_on_all_cells(padding_value, padding_value, padding_value, padding_value)
table_002.no_borders()
# Build_itemized_description_table
table_003 = Table(number_of_rows=7, number_of_columns=4)
for h in ["Descripció", "Quantitat", "Preu per unitat", "Import"]:
table_003.add(
TableCell(
Paragraph(h, font_color=X11Color("White")),
background_color=HexColor("000000"),
)
)
odd_color = HexColor("BBBBBB")
even_color = HexColor("FFFFFF")
for row_number, item in enumerate(
[
(
data["value"]["fields"]["product"]["mapValue"]["fields"]["description"]["stringValue"],
data["value"]["fields"]["product"]["mapValue"]["fields"]["quantity"]["doubleValue"],
data["value"]["fields"]["product"]["mapValue"]["fields"]["amount"]["doubleValue"],
list(data["value"]["fields"]["product"]["mapValue"]["fields"]["amount"].values())[0]*data["value"]["fields"]["product"]["mapValue"]["fields"]["quantity"]["doubleValue"]
),
]
):
c = even_color if row_number % 2 == 0 else odd_color
table_003.add(TableCell(Paragraph(item[0]), background_color=c))
table_003.add(TableCell(Paragraph(str(item[1])), background_color=c))
table_003.add(TableCell(Paragraph("€ " + str(item[2])), background_color=c))
table_003.add(TableCell(Paragraph("€ " + str(item[3])), background_color=c))
# Optionally add some empty rows to have a fixed number of rows for styling purposes
for row_number in range(3, 5):
c = even_color if row_number % 2 == 0 else odd_color
for _ in range(0, 4):
table_003.add(TableCell(Paragraph(" "), background_color=c))
table_003.add(
TableCell(
Paragraph(
"Subtotal", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT,
),
col_span=3,
)
)
table_003.add(
TableCell(
Paragraph(
str(data["value"]["fields"]["product"]["mapValue"]["fields"]["amount"]["doubleValue"]),
horizontal_alignment=Alignment.RIGHT,
)
)
)
table_003.add(
TableCell(
Paragraph(
"IGI", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT
),
col_span=3,
)
)
table_003.add(
TableCell(
Paragraph(
"{0:.2f}".format(
data["value"]["fields"]["product"]["mapValue"]["fields"]["amount"]["doubleValue"]* (4.5 / 100)
),
horizontal_alignment=Alignment.RIGHT,
)
)
)
table_003.add(
TableCell(
Paragraph(
"Total", font="Helvetica-Bold", horizontal_alignment=Alignment.RIGHT
),
col_span=3,
)
)
table_003.add(
TableCell(
Paragraph(
"{0:.2f}".format(
data["value"]["fields"]["product"]["mapValue"]["fields"]["amount"]["doubleValue"]* (1 + 4.5 / 100)
),
horizontal_alignment=Alignment.RIGHT,
)
)
)
table_003.set_padding_on_all_cells(padding_value, padding_value, padding_value, padding_value)
table_003.no_borders()
# Create document
pdf = Document()
# Add page
page = Page()
pdf.add_page(page)
page_layout = SingleColumnLayout(page)
logging.warning("Page height:")
logging.warning(page.get_page_info().get_height())
logging.warning("Table")
logging.warning(table_001)
page_layout.vertical_margin = page.get_page_info().get_height() * Decimal(0.02)
page_layout.add(
Image(
"https://firebasestorage.googleapis.com/v0/b/my-app.appspot.com/o/stores%2F2322e310-1743-11ed-87c2-9f7ebec4c7dc?alt=media&token=71ebb1be-a9ae-4f49-b3e1-b46619cdfb4a",
width=Decimal(128),
height=Decimal(128),
)
)
# Add more logging for debugging
logging.warning("Adding Table 001...")
logging.warning(table_001)
page_layout.add(table_001)
logging.warning("Table 001 added successfully.")
# Empty paragraph for spacing
page_layout.add(Paragraph(" "))
logging.warning("Adding Table 002...")
logging.warning(table_002)
page_layout.add(table_002)
logging.warning("Table 002 added successfully.")
logging.warning("Adding Table 003...")
logging.warning(table_003)
page_layout.add(table_003)
logging.warning("Table 003 added successfully.")
#Getting the invoice ID
# Initialize Firestore client
db = firestore.Client()
# Get reference to a Firestore document
doc_ref = db.collection('my_collection').document('my_document')
# Get the ID of the document
doc_id = doc_ref.id
# Uploading to google cloud
storage_client = storage.Client()
bucket = storage_client.bucket("my-app-prod.appspot.com")
invoice_id = doc_id
file_name = f"My_App_pagaments_Restaurant_Invoice_{export_time}_{invoice_id}.pdf"
blob = bucket.blob(f"Invoices/{invoice_date_month}/{file_name}")
with open("/tmp/output.pdf", "wb") as pdf_file_handle:
PDF.dumps(pdf_file_handle, pdf)
blob.upload_from_filename("/tmp/output.pdf")
# Making the blob public
blob.make_public()
public_url = blob.public_url
#Sending the email
# Create an initial document to update
seller_email = data["value"]["fields"]["seller_details"]["mapValue"]["fields"]["seller_email"]["stringValue"]
seller_name = data["value"]["fields"]["seller_details"]["mapValue"]["fields"]["seller_name"]["stringValue"]
mail_ref = db.collection(u'mail').document()
mail_ref.set({
u'to': [seller_email, '[email protected]'],
u'template': {
u'name': 'invoice_store_ca',
u'data': {
u'name': seller_name,
u'invoiceNumber': data["value"]["fields"]["invoice_number"]["integerValue"],
u'invoiceName': file_name,
u'invoiceUrl': [public_url],
},
}})
print(
f"Blob {blob.name} is publicly accessible at {blob.public_url}"
)
# Updating the pendingAmount in the store id
store_id = data["value"]["fields"]["seller_details"]["mapValue"]["fields"]["seller_id"]["stringValue"]
decrease_amount = float(data["value"]["fields"]["product"]["mapValue"]["fields"]["amount"]["doubleValue"])
store = db.collection('stores').document(store_id)
logging.warning(store_id)
logging.warning(store)
increaseBy = firestore.FieldValue.increment(-decrease_amount);
store.update({"pendingNetAmount": increaseBy})
And the error I get is:
restaurants_invoices57tg2dhgll0j Traceback (most recent call last): File "/layers/google.python.pip/pip/lib/python3.9/site-packages/flask/app.py", line 2529, in wsgi_app response = self.full_dispatch_request() File "/layers/google.python.pip/pip/lib/python3.9/site-packages/flask/app.py", line 1825, in full_dispatch_request rv = self.handle_user_exception(e) File "/layers/google.python.pip/pip/lib/python3.9/site-packages/flask/app.py", line 1823, in full_dispatch_request rv = self.dispatch_request() File "/layers/google.python.pip/pip/lib/python3.9/site-packages/flask/app.py", line 1799, in dispatch_request return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) File "/layers/google.python.pip/pip/lib/python3.9/site-packages/functions_framework/__init__.py", line 171, in view_func function(data, context) File "/workspace/main.py", line 302, in generate_pdf_invoice page_layout.add(table_001) File "/layers/google.python.pip/pip/lib/python3.9/site-packages/borb/pdf/canvas/layout/page_layout/multi_column_layout.py", line 201, in add layout_rect = layout_element.layout( File "/layers/google.python.pip/pip/lib/python3.9/site-packages/borb/pdf/canvas/layout/layout_element.py", line 307, in layout return self.calculate_layout_box_and_do_layout(page, bounding_box) File "/layers/google.python.pip/pip/lib/python3.9/site-packages/borb/pdf/canvas/layout/layout_element.py", line 320, in calculate_layout_box_and_do_layout layout_box = self._calculate_layout_box(page, bounding_box) File "/layers/google.python.pip/pip/lib/python3.9/site-packages/borb/pdf/canvas/layout/layout_element.py", line 230, in _calculate_layout_box returned_layout_box = self._calculate_layout_box_without_padding( File "/layers/google.python.pip/pip/lib/python3.9/site-packages/borb/pdf/canvas/layout/layout_element.py", line 258, in _calculate_layout_box_without_padding layout_rect = self._do_layout_without_padding(page, bounding_box) File "/layers/google.python.pip/pip/lib/python3.9/site-packages/borb/pdf/canvas/layout/table/fixed_column_width_table.py", line 131, in _do_layout_without_padding t.layout(page, Rectangle(x, y, w, h)) File "/layers/google.python.pip/pip/lib/python3.9/site-packages/borb/pdf/canvas/layout/table/table.py", line 145, in layout modified_layout_box: Rectangle = Rectangle( File "/layers/google.python.pip/pip/lib/python3.9/site-packages/borb/pdf/canvas/geometry/rectangle.py", line 26, in __init__ assert height >= 0, "A Rectangle must have a non-negative height." AssertionError: A Rectangle must have a non-negative height.
Example of payload:
restaurants_invoices57tg2dhgll0j {'oldValue': {}, 'updateMask': {}, 'value': {'createTime': '2023-09-28T19:59:18.390991Z', 'fields': {'buyer_details': {'mapValue': {'fields': {'buyer_address_city_postal_code_country': {'stringValue': 'Errrrrrr-Errrrrrr, NNNNNN, BC500'}, 'buyer_address_street': {'stringValue': 'Test 1,2'}, 'buyer_email': {'stringValue': '[email protected]'}, 'buyer_id': {'stringValue': 'xrr1p3s5Fh7NLzSknd1a'}, 'buyer_name': {'stringValue': 'test'}, 'buyer_phone': {'stringValue': '+235234566'}}}}, 'invoice_number': {'integerValue': '12'}, 'product': {'mapValue': {'fields': {'amount': {'doubleValue': 7.49}, 'description': {'stringValue': 'Payments genreated during the peroid'}, 'quantity': {'doubleValue': 1.0}}}}, 'seller_details': {'mapValue': {'fields': {'seller_BIC': {'stringValue': 'CDAERTEWXXX'}, 'seller_IBAN': {'stringValue': 'BC2356789954332123456778'}, 'seller_address_city_postal_code_country': {'stringValue': 'NDORWE TT TELLE BC700 bertyeq'}, 'seller_address_street': {'stringValue': ''}, 'seller_email': {'stringValue': '[email protected]'}, 'seller_id': {'stringValue': 'mOCXjlKsbwTAgUMo9uKz'}, 'seller_name': {'stringValue': 'tewqertts | dedddfe'}, 'seller_phone': {'stringValue': '+11 22 2222222'}}}}, 'status': {'stringValue': 'pending'}}, 'name': 'projects/my-app-prod/databases/(default)/documents/invoices_restaurants/7mzWObLnyZEGjsS0qAao', 'updateTime': '2023-09-28T19:59:18.390991Z'}}
I was expecting an invoice pdf generated from the seller to the buyer with the quanity and amounts needed.