import io
import os
import time
from reportlab.graphics.barcode import code39, code128, qr # type: ignore
from reportlab.lib import colors # type: ignore
from reportlab.lib.colors import black, blue, gray, red, white # type: ignore
from reportlab.lib.units import cm, inch, mm # type: ignore
from reportlab.pdfbase.pdfmetrics import stringWidth # type: ignore
from reportlab.pdfgen import canvas # type: ignore
from reportlab.platypus import Table, TableStyle # type: ignore
UNIT_OBJECTS = {"cm": cm, "mm": mm, "inch": inch}
COLOR_MAP = {
"Black": black,
"Gray": gray,
"Blue": blue,
"Red": red,
"White": white,
"None": None,
}
DEFAULT_FONT_SIZE = 10
DEFAULT_FONT_NAME = "Helvetica"
# Entity properties support (copied from compute.ts)
ENTITY_PROPERTIES = [
"CREATED_AT",
"CREATED_BY",
"EDITORS",
"ID",
"INDEX",
"LAST_EDITED_AT",
"LAST_EDITED_BY",
"TEMPLATE_INDEX",
"TITLE",
"TYPE_INDEX",
"SUBMITTED_FROM",
"CREATED_FROM",
]
def is_entity_property(property_name):
"""Check if a property name is an entity property (case-insensitive)"""
return property_name.upper() in ENTITY_PROPERTIES
def get_field_or_property_value(entity, column_name):
"""Get value from entity field or property (case-insensitive for properties)"""
if is_entity_property(column_name):
property_name = column_name.upper()
if property_name == "TITLE":
return entity.get("title", "")
elif property_name == "ID":
return entity.get("id", "")
elif property_name == "CREATED_AT":
return entity.get("createdAt", "")
elif property_name == "CREATED_BY":
return entity.get("createdBy", "")
elif property_name == "EDITORS":
return entity.get("editors", "")
elif property_name == "INDEX":
return entity.get("index", "")
elif property_name == "LAST_EDITED_AT":
return entity.get("lastEditedAt", "")
elif property_name == "LAST_EDITED_BY":
return entity.get("lastEditedBy", "")
elif property_name == "TEMPLATE_INDEX":
return entity.get("templateIndex", "")
elif property_name == "TYPE_INDEX":
return entity.get("typeIndex", "")
elif property_name == "SUBMITTED_FROM":
return entity.get("submittedFrom", "")
elif property_name == "CREATED_FROM":
return entity.get("createdFrom", "")
else:
# hard to maintain let's fallback to lowercase
return entity.get(column_name.lower(), "")
else:
# Handle field values using existing function
fields = entity.get("fields", {})
return extract_field_value(fields, column_name, "")
def extract_field_value(field_dict, field_name, default_value):
"""Extract field value handling SELECT arrays and whitespace"""
field_value = field_dict.get(field_name, {}).get("value", default_value)
if isinstance(field_value, list) and field_value:
result = field_value[0]
elif isinstance(field_value, list):
result = default_value
else:
result = field_value if field_value is not None else default_value
return result
def collect_all_entity_references(entity_data, config):
"""Collect all entity references from REFERENCE and INSTANCE_SUBMISSION fields to batch fetch them"""
entity_refs_to_fetch = set()
# Look through all entity data for entity references
for field_name, field_value in entity_data.items():
if isinstance(field_value, list):
for item in field_value:
if isinstance(item, dict) and "id" in item:
entity_refs_to_fetch.add(item["id"])
return list(entity_refs_to_fetch)
def collect_entity_references_from_entity(entity):
"""Collect entity references from an entity's REFERENCE and INSTANCE_SUBMISSION fields"""
entity_refs_to_fetch = set()
fields = entity.get("fields", {})
for field_name, field_info in fields.items():
field_type = field_info.get("type", "")
field_value = field_info.get("value", "")
# Look for REFERENCE and INSTANCE_SUBMISSION fields
if field_type in ["REFERENCE", "INSTANCE_SUBMISSION"] and isinstance(
field_value, list
):
for ref in field_value:
if isinstance(ref, dict) and "id" in ref:
entity_refs_to_fetch.add(ref["id"])
return list(entity_refs_to_fetch)
def batch_fetch_entities(entity_ids, entity_cache):
"""Fetch multiple entities and add to cache"""
if not entity_ids:
return entity_cache
for entity_id in entity_ids:
if entity_id not in entity_cache:
try:
entity = seal.get_entity(entity_id=entity_id)
entity_cache[entity_id] = entity
entity_title = entity.get("title", "Untitled")
print(f"Fetched entity: {entity_title}")
except Exception as e:
print(f"Failed to fetch entity {entity_id}: {e}")
entity_cache[entity_id] = None
return entity_cache
def find_config_for_template(config_template_id, target_template_id):
"""Find configuration instance(s) that support the target template ID"""
if not config_template_id:
return {"matches": [], "error": "No configuration template ID provided"}
# Search for instances created from the configuration template
search_query = {"filters": {"kind": ["INSTANCE"], "template": [config_template_id]}}
try:
config_instances = seal.search_entities(search_query)
matching_configs = []
for instance in config_instances:
try:
instance_entity = seal.get_entity(entity_id=instance["id"])
instance_fields = instance_entity.get("fields", {})
# Check the Templates reference field
templates_refs = instance_fields.get("Templates", {}).get("value", [])
if templates_refs and isinstance(templates_refs, list):
supported_template_ids = [
ref.get("id", "") for ref in templates_refs
]
if target_template_id in supported_template_ids:
config_title = instance_entity.get(
"title", f"Untitled Config ({instance_entity['id']})"
)
matching_configs.append(
{"id": instance_entity["id"], "title": config_title}
)
except Exception as e:
print(f"Failed to process config instance {instance['id']}: {str(e)}")
continue
if len(matching_configs) == 0:
return {
"matches": [],
"error": f"No configuration found for template '{target_template_id}'",
}
elif len(matching_configs) == 1:
print(
f"Found configuration '{matching_configs[0]['title']}' for template '{target_template_id}'"
)
return {"matches": matching_configs, "error": None}
else:
# Multiple matches - this is an error condition
config_titles = [config["title"] for config in matching_configs]
config_list = ", ".join(config_titles)
print(
f"WARNING: Found {len(matching_configs)} configurations for template "
f"'{target_template_id}': {config_list}"
)
error_message = (
f"Multiple configurations found for template '{target_template_id}': "
f"{config_list}. Please delete unused configurations to avoid ambiguity."
)
return {
"matches": matching_configs,
"error": error_message,
}
except Exception as e:
return {
"matches": [],
"error": f"Failed to search for configuration instances: {str(e)}",
}
def main():
"""Main entry point with tri-mode detection"""
try:
config = load_script_configuration()
try:
trigger_info = seal.get_trigger_info()
print("Running in TRIGGER MODE")
run_trigger_mode(config, trigger_info.triggered_by_entity_id)
except Exception as e:
if "not called as a trigger" in str(e):
# Determine if Preview or Manual mode based on containing entity
mode_info = config.get("_mode_info", {})
if mode_info.get("mode") == "preview":
print("Running in PREVIEW MODE")
run_preview_mode(config)
elif mode_info.get("mode") == "manual":
print("Running in MANUAL MODE")
run_manual_mode(config, mode_info.get("containing_entity_id"))
else:
raise Exception("Could not determine script mode")
else:
raise e
except Exception as e:
print(f"Script execution failed: {str(e)}")
raise
def load_script_configuration():
"""Load configuration from referenced configuration entity"""
script_entity_id = seal.entity_id
script_entity = seal.get_entity(entity_id=script_entity_id)
script_fields = script_entity.get("fields", {})
# Determine which configuration entity to use
config_entity = None
config_field_name = None
try:
# Try to get trigger info to determine the triggering entity template
trigger_info = seal.get_trigger_info()
triggering_entity_id = trigger_info.triggered_by_entity_id
triggering_entity = seal.get_entity(entity_id=triggering_entity_id)
triggering_template_ref = triggering_entity.get("sourceInfo", {}).get(
"template", {}
)
triggering_template_id = triggering_template_ref.get("id", "")
if not triggering_template_id:
raise Exception("Triggering entity has no template ID")
# Get the Label Configuration Template reference from script entity
script_entity_id = seal.entity_id
script_entity = seal.get_entity(entity_id=script_entity_id)
script_fields = script_entity.get("fields", {})
config_template_refs = script_fields.get(
"Label Configuration Template", {}
).get("value", [])
if (
not config_template_refs
or not isinstance(config_template_refs, list)
or len(config_template_refs) == 0
):
raise Exception(
"No Label Configuration Template found in script. Please add a "
"'Label Configuration Template' reference field to the script entity."
)
config_template_id = config_template_refs[0].get("id", "")
if not config_template_id:
raise Exception("Label Configuration Template reference is invalid")
# Search for configuration instance that supports this template
config_result = find_config_for_template(
config_template_id, triggering_template_id
)
if config_result["error"]:
error_details = config_result["error"]
raise Exception(
f"Configuration lookup failed for template '{triggering_template_id}': "
f"{error_details}\n"
f"Please ensure you have exactly one configuration instance from the "
f"Label Configuration Template with '{triggering_template_id}' in the "
f"'Templates' field."
)
config_entity_id = config_result["matches"][0]["id"]
config_field_name = f"Trigger Mode (template {triggering_template_id})"
config_entity = seal.get_entity(entity_id=config_entity_id)
except Exception as e:
try:
containing_entity = seal.get_containing_entity()
containing_fields = containing_entity.get("fields", {})
# Check if containing entity has "Label Elements" field to determine mode
if "Label Elements" in containing_fields:
# Preview mode: containing entity is a configuration entity
config_entity = containing_entity
config_field_name = "Preview Mode"
mode = "preview"
else:
# Manual mode: containing entity is a regular entity that wants a label
config_entity = None # Will be determined from Label Configuration Template instances
config_field_name = "Manual Mode"
mode = "manual"
containing_entity_id = containing_entity["id"]
if mode == "manual":
# Manual mode: find configuration using containing entity's template
containing_template_ref = containing_entity.get("sourceInfo", {}).get(
"template", {}
)
containing_template_id = containing_template_ref.get("id", "")
if not containing_template_id:
raise Exception("Manual mode: Containing entity has no template ID")
# Get Label Configuration Template from script entity to find matching config
script_entity_id = seal.entity_id
script_entity = seal.get_entity(entity_id=script_entity_id)
script_fields = script_entity.get("fields", {})
config_template_refs = script_fields.get(
"Label Configuration Template", {}
).get("value", [])
if (
not config_template_refs
or not isinstance(config_template_refs, list)
or len(config_template_refs) == 0
):
raise Exception(
"Manual mode failed: No 'Label Configuration Template' reference field found in script entity. "
"Please add a 'Label Configuration Template' reference field to the script."
)
config_template_id = config_template_refs[0].get("id", "")
if not config_template_id:
raise Exception(
"Manual mode failed: Label Configuration Template reference is invalid"
)
# Find configuration instance that supports containing entity's template
config_result = find_config_for_template(
config_template_id, containing_template_id
)
if config_result["error"]:
error_details = config_result["error"]
raise Exception(
f"Manual mode failed: Configuration lookup failed for template "
f"'{containing_template_id}': {error_details}\n"
f"Please ensure you have exactly one configuration instance from the "
f"Label Configuration Template with '{containing_template_id}' in the "
f"'Templates' field."
)
config_entity_id = config_result["matches"][0]["id"]
config_entity = seal.get_entity(entity_id=config_entity_id)
config_field_name = f"Manual Mode (template {containing_template_id})"
print(
f"Manual mode: Found configuration for template '{containing_template_id}'"
)
# Validate that the script has a properly configured Label Configuration Template (Preview mode only)
if mode == "preview":
preview_entities_refs = (
config_entity.get("fields", {})
.get("Preview Entities", {})
.get("value", [])
)
if preview_entities_refs:
# Check if script entity has Label Configuration Template field
script_entity_id = seal.entity_id
script_entity = seal.get_entity(entity_id=script_entity_id)
script_fields = script_entity.get("fields", {})
config_template_refs = script_fields.get(
"Label Configuration Template", {}
).get("value", [])
if (
not config_template_refs
or not isinstance(config_template_refs, list)
or len(config_template_refs) == 0
):
print(
"INFO: No 'Label Configuration Template' reference field found in script entity. "
"Preview will work, but this script won't trigger in trigger. "
"To enable trigger mode, add a 'Label Configuration Template' "
"reference field to the script entity."
)
config_field_name = "Preview Mode (no trigger configuration)"
# Skip further validation since there's no Label Configuration Template
preview_validation_complete = True
else:
config_template_id = config_template_refs[0].get("id", "")
preview_validation_complete = False
# Check that preview entities' templates are covered in Label Configuration Template
if not preview_validation_complete:
first_preview_entity = seal.get_entity(
entity_id=preview_entities_refs[0]["id"]
)
preview_template_ref = first_preview_entity.get(
"sourceInfo", {}
).get("template", {})
preview_template_id = preview_template_ref.get("id", "")
if preview_template_id:
# Check if Label Configuration Template instances support this template
config_result = find_config_for_template(
config_template_id, preview_template_id
)
if config_result["error"]:
error_details = config_result["error"]
print(
f"WARNING: Configuration lookup failed for preview entity "
f"template '{preview_template_id}': {error_details}. "
f"This preview will generate successfully, but entities with this "
f"template will NOT trigger the script in trigger mode. To enable "
f"trigger mode, ensure you have exactly one configuration instance "
f"from the Label Configuration Template with '{preview_template_id}' "
f"in the 'Templates' field."
)
config_field_name = (
"Preview Mode (template not configured for trigger)"
)
elif (
config_result["matches"][0]["id"] != config_entity["id"]
):
found_config_id = config_result["matches"][0]["id"]
found_config_title = config_result["matches"][0][
"title"
]
raise Exception(
f"Script validation failed: Label Configuration Template instances "
f"contain template '{preview_template_id}' but point to config "
f"'{found_config_title}' (ID: {found_config_id}) instead of this "
f"configuration entity '{config_entity['id']}'. Please update the "
f"configuration instance."
)
else:
config_field_name = "Preview Mode (validated Label Configuration Template)"
else:
print(
"Preview entities found but first entity has no template - "
"skipping script validation"
)
else:
print("No preview entities configured - skipping script validation")
config_field_name = "Preview Mode"
except Exception as preview_error:
raise Exception(
f"Cannot determine configuration entity.\n"
f"Trigger mode failed: {str(e)}\n"
f"Embedded mode failed: {str(preview_error)}"
)
if not config_entity:
raise Exception("Could not load configuration entity")
print(f"Using configuration: {config_field_name} (ID: {config_entity['id']})")
fields = config_entity.get("fields", {})
units = extract_field_value(fields, "Units", "cm")
label_width = extract_field_value(fields, "Label Width", 10.0)
label_height = extract_field_value(fields, "Label Height", 5.0)
print(f"Label configuration: {label_width} x {label_height} {units}")
config = {
"Label Width": label_width,
"Label Height": label_height,
"Default Font": extract_field_value(fields, "Default Font", DEFAULT_FONT_NAME),
"Default Font Size": extract_field_value(
fields, "Default Font Size", DEFAULT_FONT_SIZE
),
"Units": units,
"Preview Entities": fields.get("Preview Entities", {}).get("value", []),
"Show Grid Lines": False, # Default to False, will be overridden in preview mode if needed
}
# Load layout elements from Label Elements reference field
layout_elements_field = fields.get("Label Elements", {})
layout_elements = []
if layout_elements_field and "value" in layout_elements_field:
element_refs = layout_elements_field["value"]
if not element_refs:
print("Label Elements field is empty - no elements will be rendered")
for element_ref in element_refs:
try:
element_entity = seal.get_entity(entity_id=element_ref["id"])
element_fields = element_entity.get("fields", {})
# Extract element configuration with defaults
element = {
"Element Type": extract_field_value(
element_fields, "Element Type", "Text"
),
"Text": extract_field_value(
element_fields, "Text", ""
), # For Text elements
"Field Or Property Name": extract_field_value(
element_fields, "Field Or Property Name", ""
), # For all other elements
"Column Names": extract_field_value(
element_fields, "Column Names", ""
), # For Table elements - comma-separated text field
"Prefix Text": extract_field_value(
element_fields, "Prefix Text", ""
),
"X Position": extract_field_value(element_fields, "X Position", 0),
"Y Position": extract_field_value(element_fields, "Y Position", 0),
"Width": extract_field_value(element_fields, "Width", 2.0),
"Height": extract_field_value(element_fields, "Height", 1.0),
"Font Name": extract_field_value(
element_fields, "Font Name (optional)", DEFAULT_FONT_NAME
),
"Font Size": extract_field_value(
element_fields, "Font Size (optional)", None
), # None = use default
"Alignment": extract_field_value(
element_fields, "Alignment", "Top-Left"
), # Universal alignment
"Rotation (°)": extract_field_value(
element_fields, "Rotation (°)", 0
), # Rotation in degrees
"Line Width": extract_field_value(element_fields, "Line Width", 1),
"Stroke Color": extract_field_value(
element_fields, "Stroke Color", "Black"
),
}
layout_elements.append(element)
except Exception as e:
print(f"Failed to load layout element {element_ref['id']}: {str(e)}")
continue
config["Label Elements"] = layout_elements
config["Configuration Entity ID"] = config_entity["id"]
# Add mode information for tri-mode detection
if "mode" in locals():
config["_mode_info"] = {
"mode": mode,
"containing_entity_id": containing_entity_id if mode == "manual" else None,
}
# In preview mode, check for "Show Gridlines" field
if mode == "preview":
show_gridlines_field = fields.get(
"Show Gridlines (only shown in preview mode)", {"value": False}
)
config["Show Grid Lines"] = show_gridlines_field.get("value")
if config["Show Grid Lines"]:
print("Grid lines enabled via 'Show Gridlines' field")
print(
f"Loaded {len(layout_elements)} label elements using config '{config_field_name}'"
)
return config
def convert_coordinates(x, y, canvas_obj):
"""
Convert from top-left coordinate system to ReportLab's bottom-left system
User coordinates: (0,0) = top-left, Y increases downward
ReportLab: (0,0) = bottom-left, Y increases upward
"""
height = canvas_obj._label_height
return x, height - y
def calculate_aligned_position(x, y, width, height, alignment):
"""
Calculate actual drawing position based on alignment anchor point
Args:
x, y: User-specified coordinates
width, height: Element dimensions (in same units as x, y)
alignment: "Top-Left", "Center", "Top-Right", "Bottom-Left", "Bottom-Right" (case-insensitive)
Returns:
(adjusted_x, adjusted_y): Actual position for drawing
"""
# Make alignment detection case-insensitive
alignment_upper = alignment.upper()
if alignment_upper == "CENTER" or alignment_upper == "CENTRE":
return x - width / 2, y - height / 2
elif alignment_upper == "TOP-RIGHT":
return x - width, y
elif alignment_upper == "BOTTOM-LEFT":
return x, y - height
elif alignment_upper == "BOTTOM-RIGHT":
return x - width, y - height
else: # "TOP-LEFT" or any other value defaults to top-left
return x, y
def apply_rotation_to_canvas(canvas_obj, rotation_degrees, anchor_x, anchor_y):
"""
Apply rotation around a specific anchor point
Args:
canvas_obj: ReportLab canvas object
rotation_degrees: Rotation angle in degrees (clockwise)
anchor_x, anchor_y: Point to rotate around (in ReportLab coordinates)
"""
if rotation_degrees != 0:
# Save current state
canvas_obj.saveState()
# Translate to anchor point, rotate, then translate back
canvas_obj.translate(anchor_x, anchor_y)
canvas_obj.rotate(rotation_degrees)
canvas_obj.translate(-anchor_x, -anchor_y)
return True # Indicates rotation was applied
return False # No rotation applied
def render_element_with_rotation(canvas_obj, element, x, y, unit_obj, render_callback):
"""
Base render function that handles common rotation setup/teardown for all element types
Args:
canvas_obj: ReportLab canvas object
element: Element configuration dict
x, y: User coordinates
unit_obj: ReportLab unit object (cm, mm, inch)
render_callback: Function that performs the actual rendering
Signature: render_callback(canvas_obj, element, x, y, unit_obj, anchor_coords)
where anchor_coords is (anchor_x_rl, anchor_y_rl)
"""
rotation = element.get("Rotation (°)", 0)
if isinstance(rotation, list) and rotation:
rotation = rotation[0]
if not isinstance(rotation, (int, float)):
rotation = 0
anchor_x = x * unit_obj
anchor_y = y * unit_obj
anchor_x_rl, anchor_y_rl = convert_coordinates(anchor_x, anchor_y, canvas_obj)
rotation_applied = apply_rotation_to_canvas(
canvas_obj, rotation, anchor_x_rl, anchor_y_rl
)
try:
render_callback(canvas_obj, element, x, y, unit_obj, (anchor_x_rl, anchor_y_rl))
finally:
if rotation_applied:
canvas_obj.restoreState()
def check_page_break(y_position, canvas_obj, min_space=50):
"""
Check if we need a page break and create one if necessary
Returns the new Y position (reset to top if page break occurred)
"""
if y_position < min_space: # Less than min_space points from bottom
canvas_obj.showPage()
# After page break, we're back to top-left coordinates
return canvas_obj._label_height - 20 # Start 20 points from top
return y_position
def draw_grid_lines(canvas_obj, label_width, label_height, units, unit_obj):
"""
Draw grid lines to help with element positioning (preview mode only)
Args:
canvas_obj: ReportLab canvas object
label_width: Label width in user units
label_height: Label height in user units
units: Unit type ("cm", "mm", "inch")
unit_obj: ReportLab unit object
"""
# Grid colors (subtle so they don't interfere with content)
major_grid_color = (0.7, 0.7, 0.7) # Light gray for major lines
minor_grid_color = (0.9, 0.9, 0.9) # Very light gray for minor lines
# Define grid spacing based on units
if units == "cm":
major_spacing = 1.0 # Every 1 cm
minor_spacing = 0.5 # Every 0.5 cm
elif units == "mm":
major_spacing = 10.0 # Every 10 mm
minor_spacing = 5.0 # Every 5 mm
elif units == "inch":
major_spacing = 1.0 # Every 1 inch
minor_spacing = 0.25 # Every 0.25 inch
else:
major_spacing = 1.0
minor_spacing = 0.5
# Convert to points
width_points = label_width * unit_obj
height_points = label_height * unit_obj
canvas_obj.saveState()
# Draw minor grid lines first (so they appear behind major lines)
canvas_obj.setStrokeColorRGB(*minor_grid_color)
canvas_obj.setLineWidth(0.25)
# Vertical minor lines
x = minor_spacing
while x < label_width:
x_points = x * unit_obj
canvas_obj.line(x_points, 0, x_points, height_points)
x += minor_spacing
# Horizontal minor lines
y = minor_spacing
while y < label_height:
y_points = height_points - (y * unit_obj) # Convert to ReportLab coordinates
canvas_obj.line(0, y_points, width_points, y_points)
y += minor_spacing
# Draw major grid lines
canvas_obj.setStrokeColorRGB(*major_grid_color)
canvas_obj.setLineWidth(0.5)
# Vertical major lines with labels
x = major_spacing
while x <= label_width:
x_points = x * unit_obj
canvas_obj.line(x_points, 0, x_points, height_points)
# Add coordinate labels at the top
canvas_obj.setFont("Helvetica", 6)
canvas_obj.setFillColorRGB(*major_grid_color)
label_text = f"{int(x)}" if x == int(x) else f"{x:.1f}"
canvas_obj.drawCentredString(x_points, height_points - 10, label_text)
x += major_spacing
# Horizontal major lines with labels
y = major_spacing
while y <= label_height:
y_points = height_points - (y * unit_obj) # Convert to ReportLab coordinates
canvas_obj.line(0, y_points, width_points, y_points)
# Add coordinate labels on the left
canvas_obj.setFont("Helvetica", 6)
canvas_obj.setFillColorRGB(*major_grid_color)
label_text = f"{int(y)}" if y == int(y) else f"{y:.1f}"
canvas_obj.drawString(3, y_points - 2, label_text)
y += major_spacing
# Draw border
canvas_obj.setStrokeColorRGB(*major_grid_color)
canvas_obj.setLineWidth(1.0)
canvas_obj.rect(0, 0, width_points, height_points, stroke=1, fill=0)
# Add unit labels in corners
canvas_obj.setFont("Helvetica", 8)
canvas_obj.setFillColorRGB(*major_grid_color)
canvas_obj.drawString(5, height_points - 15, f"Preview Mode Grid: {units}")
canvas_obj.drawRightString(
width_points - 5, 5, f"Size: {label_width} x {label_height} {units}"
)
canvas_obj.restoreState()
def create_label_pdf(config, entity_data):
"""
SHARED label generation core used by both modes
Args:
config: Dict of configuration values (width, height, label elements, etc.)
entity_data: Dict of entity field values
Returns:
PDF buffer ready for upload
"""
print("Starting PDF generation...")
# Initialize entity cache for Table elements
if "_entity_cache" not in config:
config["_entity_cache"] = {}
# Pre-populate cache with entities from reference fields
source_entity = entity_data.get("_source_entity", {})
entity_refs_to_fetch = collect_entity_references_from_entity(source_entity)
if entity_refs_to_fetch:
config["_entity_cache"] = batch_fetch_entities(
entity_refs_to_fetch, config["_entity_cache"]
)
units = config.get("Units", "cm")
unit_obj = UNIT_OBJECTS[units]
width = config.get("Label Width", 10.0) * unit_obj
height = config.get("Label Height", 5.0) * unit_obj
pdf_buffer = io.BytesIO()
canvas_obj = canvas.Canvas(pdf_buffer, pagesize=(width, height))
# Store dimensions for coordinate conversion
canvas_obj._label_width = width
canvas_obj._label_height = height
# Draw grid lines in preview mode if enabled
if config.get("Show Grid Lines", False):
draw_grid_lines(
canvas_obj,
config.get("Label Width", 10.0),
config.get("Label Height", 5.0),
units,
unit_obj,
)
layout_elements = config.get("Label Elements", [])
print(f"Processing {len(layout_elements)} label elements")
for i, element in enumerate(layout_elements):
try:
render_layout_element(canvas_obj, element, entity_data, unit_obj, config)
except Exception as e:
print(f"Failed to render layout element {i + 1}: {str(e)}")
continue
canvas_obj.save()
pdf_buffer.seek(0)
return pdf_buffer
def render_layout_element(canvas_obj, element, entity_data, unit_obj, config):
"""Render a single layout element on the canvas"""
element_type = element.get("Element Type", "").strip()
# Handle Table element type
if element_type == "Table":
x_pos = element.get("X Position", 0)
y_pos = element.get("Y Position", 0)
alignment = element.get("Alignment", "Top-Left")
if isinstance(alignment, list) and alignment:
alignment = alignment[0].strip()
elif isinstance(alignment, str):
alignment = alignment.strip()
else:
alignment = "Top-Left"
render_table_element(
canvas_obj, element, entity_data, x_pos, y_pos, unit_obj, config, alignment
)
return
if element_type == "Text":
value_or_field_name = element.get("Text", "").strip()
else:
value_or_field_name = element.get("Field Or Property Name", "").strip()
x_pos = element.get("X Position", 0)
y_pos = element.get("Y Position", 0)
alignment = element.get("Alignment", "Top-Left")
if isinstance(alignment, list) and alignment:
alignment = alignment[0].strip()
elif isinstance(alignment, str):
alignment = alignment.strip()
else:
alignment = "Top-Left"
x = x_pos * unit_obj
y = y_pos * unit_obj
# Convert from top-left to bottom-left coordinates
x_final, y_final = convert_coordinates(x, y, canvas_obj)
if element_type == "Page Break":
canvas_obj.showPage()
return
prefix_text = element.get("Prefix Text", "")
if element_type == "Text":
base_value = value_or_field_name
elif element_type == "Field":
# Handle both entity properties and field values
if is_entity_property(value_or_field_name):
# Get from entity properties (need the full entity)
source_entity = entity_data.get("_source_entity", {})
base_value = get_field_or_property_value(source_entity, value_or_field_name)
else:
# Get from entity fields using utility (supports case-insensitive properties)
source_entity = entity_data.get("_source_entity", {})
base_value = get_field_or_property_value(source_entity, value_or_field_name)
if not base_value and value_or_field_name:
print(
f"Field or property '{value_or_field_name}' not found in entity data, skipping element"
)
return
elif element_type in ["Barcode Code128", "QR Code", "Barcode Code39"]:
# Get from entity properties (need the full entity)
source_entity = entity_data.get("_source_entity", {})
base_value = get_field_or_property_value(source_entity, value_or_field_name)
if not base_value and value_or_field_name:
print(
f"Field or property '{value_or_field_name}' not found in entity data, skipping element"
)
return
# For barcodes, use base value without prefix (barcodes need clean data)
final_value = base_value
elif element_type == "Image":
source_entity = entity_data.get("_source_entity", {})
image_refs = get_field_or_property_value(source_entity, value_or_field_name)
if not image_refs:
print(
f"Image reference field or property '{value_or_field_name}' not found, skipping element"
)
return
final_value = image_refs
elif element_type == "Border":
base_value = ""
final_value = ""
else:
print(f"Unknown element type: {element_type}")
return
if element_type in ["Text", "Field"]:
final_value = f"{prefix_text}{base_value}" if prefix_text else base_value
# Render the appropriate element
if element_type in ["Text", "Field"]:
render_text_element(
canvas_obj, element, final_value, x_pos, y_pos, unit_obj, config, alignment
)
elif element_type in ["Barcode Code128", "QR Code", "Barcode Code39"]:
render_barcode_element(
canvas_obj, element, final_value, x_pos, y_pos, unit_obj, alignment
)
elif element_type == "Image":
render_image_element(
canvas_obj, element, final_value, x_pos, y_pos, unit_obj, alignment
)
elif element_type == "Border":
render_rectangle_element(canvas_obj, element, x_pos, y_pos, unit_obj, alignment)
def render_text_element(
canvas_obj, element, field_value, x, y, unit_obj, config, alignment
):
"""Render text element with universal alignment and rotation support"""
def render_text_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
"""Specific text rendering logic"""
font_name = element.get("Font Name", None)
if font_name is None:
font_name = config.get("Default Font", DEFAULT_FONT_NAME)
font_size = element.get("Font Size", None)
if font_size is None:
font_size = config.get("Default Font Size", DEFAULT_FONT_SIZE)
stroke_color = COLOR_MAP.get(element.get("Stroke Color", "Black"), black)
alignment_upper = alignment.upper()
if "RIGHT" in alignment_upper:
text_alignment = "Right"
elif "CENTER" in alignment_upper or "CENTRE" in alignment_upper:
text_alignment = "Center"
else:
text_alignment = "Left"
text_str = str(field_value)
# Split text by newlines for multiline support
text_lines = text_str.split("\n")
# Calculate dimensions for all lines
max_text_width = 0
for line in text_lines:
line_width = stringWidth(line, font_name, font_size)
max_text_width = max(max_text_width, line_width)
# Calculate total text height (font size + line spacing for each line)
line_spacing = font_size * 0.2 # 20% of font size for line spacing
total_text_height = (
len(text_lines) * font_size + (len(text_lines) - 1) * line_spacing
)
# Use the same alignment approach as images - calculate aligned position and stick with it
aligned_x, aligned_y = calculate_aligned_position(
x, y, max_text_width / unit_obj, total_text_height / unit_obj, alignment
)
# Convert aligned position to ReportLab coordinates
aligned_x_final = aligned_x * unit_obj
aligned_y_final = aligned_y * unit_obj
aligned_x_rl, aligned_y_rl = convert_coordinates(
aligned_x_final, aligned_y_final, canvas_obj
)
canvas_obj.setFont(font_name, font_size)
if stroke_color:
canvas_obj.setFillColor(stroke_color)
# For center alignment, we need the original coordinates (where user wants the center)
if text_alignment == "Center":
original_x_rl = x * unit_obj # Original x coordinate in ReportLab units
# Draw each line separately
for line_index, line in enumerate(text_lines):
# Calculate Y position for this line (ReportLab draws from baseline)
line_y = (
aligned_y_rl
- font_size * 0.8
- (line_index * (font_size + line_spacing))
)
if text_alignment == "Center":
# For center: use the original coordinates as the center point (avoid double-centering)
canvas_obj.drawCentredString(original_x_rl, line_y, line)
elif text_alignment == "Right":
# For right: use the right edge of the aligned text area
right_x = aligned_x_rl + max_text_width
canvas_obj.drawRightString(right_x, line_y, line)
else:
# For left: use the left edge of the aligned text area
canvas_obj.drawString(aligned_x_rl, line_y, line)
render_element_with_rotation(
canvas_obj, element, x, y, unit_obj, render_text_callback
)
def render_barcode_element(canvas_obj, element, field_value, x, y, unit_obj, alignment):
"""Render barcode element with universal alignment and rotation support"""
def render_barcode_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
"""Specific barcode rendering logic"""
element_type = element.get("Element Type")
desired_width = element.get("Width", 2.0) * unit_obj
height = element.get("Height", 1.0) * unit_obj
try:
canvas_obj.saveState()
canvas_obj.setStrokeColor(black)
canvas_obj.setFillColor(black)
if element_type == "Barcode Code128":
# Calculate appropriate barWidth for desired total width
# For Code128, typical character width is about 11 units, so estimate total units needed
estimated_units = (
len(str(field_value)) * 11 + 35
) # 35 for start/stop/checksum
target_bar_width = desired_width / estimated_units
# Ensure minimum bar width for readability (at least 0.5 points)
min_bar_width = 0.5
bar_width = max(target_bar_width, min_bar_width)
barcode = code128.Code128(
value=str(field_value),
barHeight=height,
barWidth=bar_width,
humanReadable=True,
quiet=0, # Disable quiet zones for precise alignment
lquiet=0,
rquiet=0,
)
actual_barcode_width = barcode.width
draw_x, draw_y = calculate_aligned_position(
x, y, actual_barcode_width / unit_obj, height / unit_obj, alignment
)
draw_x_final = draw_x * unit_obj
draw_y_final = draw_y * unit_obj
draw_x_rl, draw_y_rl = convert_coordinates(
draw_x_final, draw_y_final, canvas_obj
)
# Adjust Y coordinate: ReportLab draws barcodes from bottom-left, we want top-left positioning
actual_barcode_y = draw_y_rl - height
barcode.drawOn(canvas_obj, draw_x_rl, actual_barcode_y)
elif element_type == "Barcode Code39":
# Calculate appropriate barWidth for desired total width
# For Code39, each character is about 13 units wide
estimated_units = len(str(field_value)) * 13 + 25 # 25 for start/stop
target_bar_width = desired_width / estimated_units
# Ensure minimum bar width for readability
min_bar_width = 0.5
bar_width = max(target_bar_width, min_bar_width)
barcode = code39.Standard39(
value=str(field_value),
barHeight=height,
barWidth=bar_width,
humanReadable=True,
quiet=0, # Disable quiet zones for precise alignment
lquiet=0,
rquiet=0,
)
actual_barcode_width = barcode.width
draw_x, draw_y = calculate_aligned_position(
x, y, actual_barcode_width / unit_obj, height / unit_obj, alignment
)
draw_x_final = draw_x * unit_obj
draw_y_final = draw_y * unit_obj
draw_x_rl, draw_y_rl = convert_coordinates(
draw_x_final, draw_y_final, canvas_obj
)
# Adjust Y coordinate: ReportLab draws barcodes from bottom-left, we want top-left positioning
actual_barcode_y = draw_y_rl - height
barcode.drawOn(canvas_obj, draw_x_rl, actual_barcode_y)
elif element_type == "QR Code":
# QR codes use the width and height directly
draw_x, draw_y = calculate_aligned_position(
x, y, desired_width / unit_obj, height / unit_obj, alignment
)
draw_x_final = draw_x * unit_obj
draw_y_final = draw_y * unit_obj
draw_x_rl, draw_y_rl = convert_coordinates(
draw_x_final, draw_y_final, canvas_obj
)
qr_code = qr.QrCodeWidget(str(field_value))
qr_code.barWidth = desired_width
qr_code.barHeight = height
from reportlab.graphics import renderPDF # type: ignore
from reportlab.graphics.shapes import Drawing # type: ignore
drawing = Drawing(desired_width, height)
drawing.add(qr_code)
# Adjust Y coordinate: ReportLab draws from bottom-left, we want top-left positioning
qr_y = draw_y_rl - height
renderPDF.draw(drawing, canvas_obj, draw_x_rl, qr_y)
canvas_obj.restoreState()
except Exception as e:
print(f"Failed to render barcode {element_type}: {str(e)}")
canvas_obj.restoreState()
fallback_x, fallback_y = calculate_aligned_position(
x, y, 100 / unit_obj, 8 / unit_obj, alignment
)
fallback_x_final = fallback_x * unit_obj
fallback_y_final = fallback_y * unit_obj
fallback_x_rl, fallback_y_rl = convert_coordinates(
fallback_x_final, fallback_y_final, canvas_obj
)
canvas_obj.setFont("Helvetica", 8)
fallback_text_y = fallback_y_rl - 8 * 0.8
canvas_obj.drawString(
fallback_x_rl, fallback_text_y, f"Barcode: {field_value}"
)
render_element_with_rotation(
canvas_obj, element, x, y, unit_obj, render_barcode_callback
)
def render_rectangle_element(canvas_obj, element, x, y, unit_obj, alignment):
"""Render border element with universal alignment and rotation support"""
def render_rectangle_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
"""Specific rectangle/border rendering logic"""
width = element.get("Width", 2.0) * unit_obj
height = element.get("Height", 1.0) * unit_obj
line_width = element.get("Line Width", 1)
stroke_color = COLOR_MAP.get(element.get("Stroke Color", "Black"))
aligned_x, aligned_y = calculate_aligned_position(
x, y, width / unit_obj, height / unit_obj, alignment
)
x_final = aligned_x * unit_obj
y_final = aligned_y * unit_obj
x_rl, y_rl = convert_coordinates(x_final, y_final, canvas_obj)
canvas_obj.setLineWidth(line_width)
if stroke_color:
canvas_obj.setStrokeColor(stroke_color)
# Adjust Y coordinate: ReportLab draws rectangles from bottom-left, we want top-left positioning
rect_y = y_rl - height
stroke_flag = 1 if stroke_color else 0
canvas_obj.rect(x_rl, rect_y, width, height, stroke=stroke_flag, fill=0)
render_element_with_rotation(
canvas_obj, element, x, y, unit_obj, render_rectangle_callback
)
def render_table_element(
canvas_obj, element, entity_data, x, y, unit_obj, config, alignment
):
"""Render table using ReportLab's Table class"""
def render_table_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
"""Specific table rendering logic"""
# Get table configuration
field_name = element.get("Field Or Property Name", "").strip()
column_names_text = element.get("Column Names", "").strip()
print(f"Column Names: {column_names_text}")
# Parse comma-separated column names into a list
if column_names_text:
column_names = [
name.strip() for name in column_names_text.split(",") if name.strip()
]
else:
column_names = []
if not field_name or not column_names:
return
# Get entity references from the specified field
# For table elements, we need the FULL array of references, not just the first one
source_entity = entity_data.get("_source_entity", {})
if is_entity_property(field_name):
entity_refs = get_field_or_property_value(source_entity, field_name)
else:
fields = source_entity.get("fields", {})
entity_refs = fields.get(field_name, {}).get("value", [])
if not entity_refs:
print(f"DEBUG: No entity references found in field '{field_name}'")
return
entity_cache = config.get("_entity_cache", {})
entity_ids_to_fetch = []
for ref in entity_refs:
if isinstance(ref, dict) and "id" in ref:
entity_id = ref["id"]
if entity_id not in entity_cache:
entity_ids_to_fetch.append(entity_id)
# Batch fetch any missing entities
if entity_ids_to_fetch:
entity_cache = batch_fetch_entities(entity_ids_to_fetch, entity_cache)
config["_entity_cache"] = entity_cache
# Build table data
table_data = []
# Header row - use column names as headers
headers = column_names[:] # Copy the list
table_data.append(headers)
# Data rows
for ref in entity_refs:
if isinstance(ref, dict) and "id" in ref:
entity_id = ref["id"]
if entity_id in entity_cache and entity_cache[entity_id]:
entity = entity_cache[entity_id]
row = []
for column_name in column_names:
value = get_field_or_property_value(entity, column_name)
row.append(str(value) if value else "")
table_data.append(row)
if len(table_data) <= 1: # Only headers
return
# Get font size for the table - use element font size or fall back to global default
table_font_size = element.get("Font Size")
if table_font_size is None:
table_font_size = config.get("Default Font Size", DEFAULT_FONT_SIZE)
if isinstance(table_font_size, list) and table_font_size:
table_font_size = table_font_size[0]
if not isinstance(table_font_size, (int, float)) or table_font_size <= 0:
table_font_size = config.get("Default Font Size", DEFAULT_FONT_SIZE)
# Get font name for the table - use element font name or fall back to global default
table_font_name = element.get("Font Name")
if table_font_name is None:
table_font_name = config.get("Default Font", DEFAULT_FONT_NAME)
# Get bold version of the font for headers
if table_font_name == "Helvetica":
table_font_bold = "Helvetica-Bold"
elif table_font_name == "Times-Roman":
table_font_bold = "Times-Bold"
elif table_font_name == "Courier":
table_font_bold = "Courier-Bold"
else:
# For unknown fonts, try adding -Bold or fall back to the same font
table_font_bold = (
f"{table_font_name}-Bold"
if not table_font_name.endswith("-Bold")
else table_font_name
)
# Calculate dimensions first - needed for column width and row height calculation
cell_padding = table_font_size * 0.3
border_width = table_font_size * 0.05 # 5% of font size for border thickness
row_height = (
table_font_size * 1.8
) # 180% of font size for row height (extra space for vertical centering)
min_col_widths = []
for col_index in range(len(column_names)):
max_width = 0
for row in table_data:
if col_index < len(row):
cell_text = str(row[col_index])
# Use header font for header row, data font for data rows
font_name = (
table_font_bold if row == table_data[0] else table_font_name
)
text_width = stringWidth(cell_text, font_name, table_font_size)
max_width = max(max_width, text_width)
# Add padding (both sides) plus some extra margin for large fonts
padding_multiplier = 0.5 # More padding for large fonts
min_width = (
max_width + (cell_padding * 2) + (table_font_size * padding_multiplier)
)
min_col_widths.append(min_width)
# Create explicit row heights for proper vertical alignment
row_heights = [row_height] * len(table_data) # Same height for all rows
# Create ReportLab Table with calculated column widths AND explicit row heights
table = Table(table_data, colWidths=min_col_widths, rowHeights=row_heights)
# Build complete style commands list including row heights
style_commands = [
# Header row styling - background and font first
("BACKGROUND", (0, 0), (-1, 0), colors.gray),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), table_font_bold),
("FONTSIZE", (0, 0), (-1, 0), table_font_size),
# Data rows styling - background and font
("BACKGROUND", (0, 1), (-1, -1), colors.white),
("TEXTCOLOR", (0, 1), (-1, -1), colors.black),
("FONTNAME", (0, 1), (-1, -1), table_font_name),
("FONTSIZE", (0, 1), (-1, -1), table_font_size),
# Row backgrounds for readability - alternating white and light gray
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, (0.9, 0.9, 0.9)]),
# Grid and borders - thickness relative to font size
("GRID", (0, 0), (-1, -1), border_width, colors.black),
# Alignment commands LAST - so they take precedence
("VALIGN", (0, 0), (-1, -1), "MIDDLE"), # Vertical center
("ALIGN", (0, 0), (-1, 0), "CENTER"), # Header row center
("ALIGN", (0, 1), (-1, -1), "CENTER"), # Data rows center
]
# Note: Row heights are now set in Table constructor, not via MINHEIGHT style commands
# This ensures proper vertical alignment with VALIGN
table_style = TableStyle(style_commands)
table.setStyle(table_style)
# CRITICAL: Wrap the table to calculate natural dimensions (ReportLab requirement)
canvas_width = canvas_obj._label_width
canvas_height = canvas_obj._label_height
try:
actual_table_width, actual_table_height = table.wrap(
canvas_width, canvas_height
)
except (ValueError, TypeError):
# Fallback: calculate dimensions from our explicit column widths and row count
actual_table_width = sum(min_col_widths)
actual_table_height = len(table_data) * row_height
# Calculate position with alignment using ACTUAL table dimensions
aligned_x, aligned_y = calculate_aligned_position(
x,
y,
actual_table_width / unit_obj,
actual_table_height / unit_obj,
alignment,
)
final_x = aligned_x * unit_obj
final_y = aligned_y * unit_obj
final_x_rl, final_y_rl = convert_coordinates(final_x, final_y, canvas_obj)
# Draw table (ReportLab tables draw from bottom-left)
table_y = final_y_rl - actual_table_height
table.drawOn(canvas_obj, final_x_rl, table_y)
# Use existing rotation framework
# Note: render_table_callback accesses entity_data and config from closure
render_element_with_rotation(
canvas_obj, element, x, y, unit_obj, render_table_callback
)
def render_image_element(canvas_obj, element, image_refs, x, y, unit_obj, alignment):
"""Render image element with universal alignment and rotation support"""
def render_image_callback(canvas_obj, element, x, y, unit_obj, anchor_coords):
"""Specific image rendering logic"""
width = element.get("Width", 2.0) * unit_obj
height = element.get("Height", 1.0) * unit_obj
aligned_x, aligned_y = calculate_aligned_position(
x, y, width / unit_obj, height / unit_obj, alignment
)
x_final = aligned_x * unit_obj
y_final = aligned_y * unit_obj
x_rl, y_rl = convert_coordinates(x_final, y_final, canvas_obj)
try:
if isinstance(image_refs, list) and image_refs:
image_ref = image_refs[0]
elif isinstance(image_refs, dict):
image_ref = image_refs
else:
print(f"Invalid image reference format: {image_refs}")
return
image_entity_id = image_ref.get("id", "")
if not image_entity_id:
print("No image entity ID found in reference")
return
if image_ref.get("version"):
temp_path = seal.download_file(
file_entity_id=image_entity_id, version=image_ref["version"]
)
else:
temp_path = seal.download_file(file_entity_id=image_entity_id)
try:
# Adjust Y coordinate: ReportLab draws images from bottom-left, we want top-left positioning
image_y = y_rl - height
canvas_obj.drawImage(
temp_path, x_rl, image_y, width=width, height=height, mask="auto"
)
finally:
try:
os.unlink(temp_path)
except Exception as cleanup_error:
print(f"Failed to clean up downloaded file: {cleanup_error}")
except Exception as e:
print(f"Failed to render image: {str(e)}")
canvas_obj.setFont("Helvetica", 8)
fallback_y = y_rl - 8 * 0.8
canvas_obj.drawString(x_rl, fallback_y, "[Image Error]")
render_element_with_rotation(
canvas_obj, element, x, y, unit_obj, render_image_callback
)
def extract_entity_fields(entity):
"""Extract all field values from an entity for label generation"""
entity_data = {}
fields = entity.get("fields", {})
for field_name, field_info in fields.items():
field_value = field_info.get("value", "")
field_data_type = field_info.get("dataType", "")
if isinstance(field_value, list):
if (
field_value
and isinstance(field_value[0], dict)
and "id" in field_value[0]
):
if field_data_type in ["ENTITY", "INSTANCE_SUBMISSION", "SCRIPT"]:
entity_data[field_name] = field_value
else:
entity_data[field_name] = ", ".join(
[ref.get("id", "") for ref in field_value]
)
elif field_value:
entity_data[field_name] = ", ".join([str(item) for item in field_value])
else:
entity_data[field_name] = ""
elif isinstance(field_value, dict):
entity_data[field_name] = str(field_value.get("value", field_value))
else:
entity_data[field_name] = (
str(field_value) if field_value is not None else ""
)
# Store source entity for property access
entity_data["_source_entity"] = entity
return entity_data
def run_preview_mode(config):
"""Generate Labels for testing"""
preview_entities_refs = config.get("Preview Entities", [])
if not preview_entities_refs:
print("No preview entities configured")
return
preview_labels = []
for entity_ref in preview_entities_refs:
try:
print(f"Generating preview for entity: {entity_ref['id']}")
entity = seal.get_entity(entity_id=entity_ref["id"])
label_file = generate_and_upload_label(entity, config, mode="Preview")
preview_labels.append(label_file)
print(f"Preview label created successfully: {label_file['id']}")
except Exception as e:
print(f"Failed to generate preview for entity {entity_ref['id']}: {str(e)}")
continue
update_preview_references(preview_labels, config)
print(f"Generated {len(preview_labels)} Labels")
def run_trigger_mode(config, source_entity_id):
"""Generate label for triggering entity"""
try:
entity = seal.get_entity(entity_id=source_entity_id)
entity_data = extract_entity_fields(entity)
pdf_buffer = create_label_pdf(config, entity_data)
create_or_update_trigger_label(pdf_buffer, entity, config)
print(f"Label generated for entity: {entity.get('title')}")
except Exception as e:
print(f"Failed to generate trigger label: {str(e)}")
raise
def run_manual_mode(config, containing_entity_id):
"""Generate label for containing entity and try to update Labels field"""
try:
entity = seal.get_entity(entity_id=containing_entity_id)
label_file = generate_and_upload_label(entity, config, mode="Manual")
# Try to update Labels field on the containing entity
try:
update_manual_labels_preview(containing_entity_id, label_file)
print(
f"Manual label generated and added to Labels for entity: {entity.get('title')}"
)
except Exception as field_error:
print(
f"Manual label generated successfully, but failed to update Labels field: {str(field_error)}"
)
print(f"Label file created with ID: {label_file['id']}")
except Exception as e:
print(f"Failed to generate manual label: {str(e)}")
raise
def generate_and_upload_label(entity, config, mode="Preview"):
"""Shared logic to generate PDF and upload label for an entity"""
entity_data = extract_entity_fields(entity)
pdf_buffer = create_label_pdf(config, entity_data)
return upload_pdf_as_label(pdf_buffer, entity, config, mode=mode)
def upload_pdf_as_preview(pdf_buffer, source_entity, config):
"""Upload PDF as preview label file entity"""
return upload_pdf_as_label(pdf_buffer, source_entity, config, mode="Preview")
def upload_pdf_as_label(pdf_buffer, source_entity, config, mode="Preview"):
"""Upload PDF as label file entity with specified mode prefix"""
timestamp = str(int(time.time()))
title_field_value = get_field_or_property_value(source_entity, "title")
if isinstance(title_field_value, list):
entity_title = str(title_field_value[0]) if title_field_value else mode
elif isinstance(title_field_value, dict):
entity_title = str(title_field_value.get("value", mode))
elif title_field_value:
entity_title = str(title_field_value)
else:
entity_title = source_entity.get("title", mode)
filename = f"{mode}_{entity_title}_{timestamp}.pdf"
temp_filename = f"temp_{mode.lower()}_{timestamp}.pdf"
with open(temp_filename, "wb") as f:
f.write(pdf_buffer.read())
try:
file_entity = seal.upload_file(
file_path=temp_filename, file_name=filename, type_title="Label"
)
return file_entity
finally:
if os.path.exists(temp_filename):
os.remove(temp_filename)
def update_manual_labels_preview(entity_id, label_file):
"""Update the Labels field on an entity with the generated label"""
try:
# Get current entity to check if it's editable
entity = seal.get_entity(entity_id=entity_id)
# Make entity editable if it's not already
if entity["status"] != "EDITABLE":
seal.make_entity_editable(entity_id)
# Create reference to the label file
label_ref = {"id": label_file["id"], "version": None}
# Get current Labels field value
current_entity = seal.get_entity(entity_id=entity_id)
current_labels = (
current_entity.get("fields", {}).get("Labels", {}).get("value", [])
)
# Add new label to the list (or create new list if field doesn't exist)
if isinstance(current_labels, list):
updated_labels = current_labels + [label_ref]
else:
updated_labels = [label_ref]
# Update the field
seal.update_field_value_in_entity(
entity_id=entity_id,
field_name="Labels",
field_value=updated_labels,
)
print(f"Successfully updated Labels field with {len(updated_labels)} labels")
except Exception as e:
raise Exception(f"Failed to update Labels field: {str(e)}")
def create_or_update_trigger_label(pdf_buffer, source_entity, config):
"""Create or update trigger label (adapted from fixedGenerationScript.py)"""
source_entity_id = source_entity["id"]
title_field_value = get_field_or_property_value(source_entity, "title")
if isinstance(title_field_value, list):
label_title = str(title_field_value[0]) if title_field_value else "Label"
elif isinstance(title_field_value, dict):
label_title = str(title_field_value.get("value", "Label"))
elif title_field_value:
label_title = str(title_field_value)
else:
label_title = source_entity.get("title", "Label")
timestamp = str(int(time.time()))
temp_filename = f"label_{source_entity_id}_{timestamp}.pdf"
with open(temp_filename, "wb") as f:
f.write(pdf_buffer.read())
try:
search_query = {
"filters": {
"fieldValue": [
{
"name": "Label Source",
"operator": "=",
"value": f"ref({source_entity_id})",
}
]
}
}
existing_labels = seal.search_entities(search_query)
# Use the first matching label by Label Source
existing_label_id = existing_labels[0]["id"] if existing_labels else None
if existing_label_id:
print(f"Updating existing label: {existing_label_id}")
update_existing_label(existing_label_id, temp_filename, label_title, config)
else:
print(f"Creating new label for entity: {source_entity_id}")
create_new_label(temp_filename, label_title, source_entity_id, config)
finally:
if os.path.exists(temp_filename):
os.remove(temp_filename)
def update_existing_label(label_id, temp_filename, label_title, config):
"""Update existing label with new content"""
existing_label = seal.get_entity(entity_id=label_id)
if existing_label["status"] != "EDITABLE":
seal.make_entity_editable(label_id)
timestamp = str(int(time.time()))
unique_filename = f"{label_title}_label_{timestamp}.pdf"
temp_file_entity = seal.upload_file(
file_path=temp_filename, file_name=unique_filename, type_title="Label"
)
new_file_id = temp_file_entity["content"]["value"]["fileId"]
new_content = {"type": "FILE", "value": {"fileId": new_file_id}}
seal._request(
url=f"entities/{label_id}/content",
method="PATCH",
json={"content": new_content},
)
current_title = existing_label.get("title")
if current_title != label_title:
seal.update_entity_title(label_id, label_title)
seal.archive_entity(temp_file_entity["id"], True)
print(f"Label updated successfully: {label_id}")
def create_new_label(temp_filename, label_title, source_entity_id, config):
"""Create new label file entity"""
# Upload the PDF file
file_entity = seal.upload_file(
file_path=temp_filename, file_name=f"{label_title}.pdf", type_title="Label"
)
# Update title
seal.update_entity_title(file_entity["id"], label_title)
# Set reference fields
label_entity_id = file_entity["id"]
# Label Source reference
seal.update_field_value_in_entity(
entity_id=label_entity_id,
field_name="Label Source",
field_value=[{"id": source_entity_id, "version": None}],
)
print(f"New label created successfully: {label_entity_id}")
def update_preview_references(preview_labels, config):
"""Update the Labels reference field in configuration entity"""
if not preview_labels:
return
config_entity_id = config.get("Configuration Entity ID")
if not config_entity_id:
print("No configuration entity ID found, cannot update Labels")
return
preview_refs = [{"id": label["id"], "version": None} for label in preview_labels]
try:
seal.update_field_value_in_entity(
entity_id=config_entity_id,
field_name="Labels",
field_value=preview_refs,
)
print(
f"Updated Labels on configuration entity with {len(preview_refs)} references"
)
except Exception as e:
print(f"Failed to update Labels field: {str(e)}")
def create_label(seal_instance):
global seal
seal = seal_instance
return main()
# Only run main() when script is executed directly, not when imported
if __name__ == "__main__":
main()