diff --git a/app/utils/pdf_export.py b/app/utils/pdf_export.py index 57a2943..e7549cd 100644 --- a/app/utils/pdf_export.py +++ b/app/utils/pdf_export.py @@ -351,6 +351,7 @@ def _form_fields_section(form_fields, form_data, static_folder): if ft in _ALWAYS_SHOW_PRE: return True if ft == 'rating' and str(v).isdigit() and int(v) > 0: return True + if ft == 'pass_fail' and v: return True if ft == 'checkbox' and v in ('yes','true'): return True if ft == 'checkbox_group'and isinstance(v, list) and v: return True if ft == 'image' and v and os.path.exists(os.path.join(static_folder, v)): return True @@ -398,14 +399,48 @@ def _form_fields_section(form_fields, form_data, static_folder): # Build one Table row with cells sized by colSpan. # Fields with no value are skipped unless they are text/textarea/label — # those always appear so free-text notes are never suppressed. - cells = [] - col_widths = [] + # current_col tracks horizontal position (1-based) so gaps between fields + # are filled with invisible placeholder cells that preserve column alignment. + cells = [] + col_widths = [] + has_content = False + current_col = 1 + + # Returns True if the field should be hidden from the PDF. + def _skip(ftype, val): + if ftype == 'rating': + return not str(val).isdigit() or int(val) == 0 + if ftype == 'image': + img_path = os.path.join(static_folder, val) if val else '' + return not val or not os.path.exists(img_path) + if ftype == 'signature': + return not val or not str(val).startswith('data:') + if ftype == 'checkbox': + # stored as 'true'/'false' by _collect_form_responses + return val not in ('yes', 'true') + if ftype == 'pass_fail': + return not val + if ftype == 'checkbox_group': + return not val or not isinstance(val, list) or len(val) == 0 + if ftype == 'table': + return not val or not isinstance(val, list) or len(val) == 0 + # All remaining input types (text, textarea, number, date, + # email, radio, select, and any future types) + return not val for field in fields_in_row: ftype = field.get('type', '') + col = field.get('col', current_col) col_span = field.get('colSpan', 1) cell_w = UNIT * col_span + # Fill horizontal gap before this field with an invisible spacer + if col > current_col: + gap_w = UNIT * (col - current_col) + cells.append(Paragraph('', STYLES['FieldValue'])) + col_widths.append(gap_w) + current_col = col + col_span + # ── Inline label (free-standing text element) ───────────────────── if ftype == 'label': fs_map = {'small': 8, 'normal': 9, 'large': 11, 'x-large': 13} @@ -418,6 +453,7 @@ def _form_fields_section(form_fields, form_data, static_folder): ) cells.append(p) col_widths.append(cell_w) + has_content = True continue fid = str(field.get('id', '')) @@ -425,29 +461,10 @@ def _form_fields_section(form_fields, form_data, static_folder): lbl = field.get('label', '') # ── Skip fields with no meaningful value ────────────────────────── - # Returns True if the field should be hidden from the PDF. - def _skip(ftype, val): - if ftype == 'rating': - return not str(val).isdigit() or int(val) == 0 - if ftype == 'image': - img_path = os.path.join(static_folder, val) if val else '' - return not val or not os.path.exists(img_path) - if ftype == 'signature': - return not val or not str(val).startswith('data:') - if ftype == 'checkbox': - # stored as 'true'/'false' by _collect_form_responses - return val not in ('yes', 'true') - if ftype == 'pass_fail': - return not val - if ftype == 'checkbox_group': - return not val or not isinstance(val, list) or len(val) == 0 - if ftype == 'table': - return not val or not isinstance(val, list) or len(val) == 0 - # All remaining input types (text, textarea, number, date, - # email, radio, select, and any future types) - return not val - if _skip(ftype, val): + # Keep a placeholder so subsequent fields stay in column position + cells.append(Paragraph('', STYLES['FieldValue'])) + col_widths.append(cell_w) continue lbl_p = Paragraph(lbl, STYLES['FieldLabel']) @@ -479,7 +496,9 @@ def _form_fields_section(form_fields, form_data, static_folder): height=1.0 * inch, kind='proportional') except Exception: - # Could not load image — skip this cell entirely + # Could not load image — insert placeholder to preserve column position + cells.append(Paragraph('', STYLES['FieldValue'])) + col_widths.append(cell_w) continue elif ftype == 'signature': @@ -536,9 +555,10 @@ def _form_fields_section(form_fields, form_data, static_folder): cells.append(box) col_widths.append(cell_w) + has_content = True - # Skip the entire row if nothing made it through the filter - if not cells: + # Skip the entire row if no field had meaningful content + if not has_content: continue row_tbl = Table([cells], colWidths=col_widths, hAlign='LEFT')