222 lines
7.5 KiB
Python
222 lines
7.5 KiB
Python
"""
|
|
Template service: fill Word (.docx) templates and generate output files.
|
|
|
|
Variables available in templates (use {{ variable_name }} syntax):
|
|
|
|
User:
|
|
{{ user_name }} Full name (or [MASKED] after PII erasure)
|
|
{{ user_email }}
|
|
{{ user_phone }}
|
|
{{ user_department }}
|
|
{{ user_job_title }}
|
|
{{ user_location }}
|
|
{{ user_windows_id }} Always present — survives masking
|
|
{{ user_employee_id }} Same as windows_id (alias)
|
|
|
|
Asset:
|
|
{{ asset_serial }} Serial number
|
|
{{ asset_service_tag }}
|
|
{{ asset_tag }} Internal asset tag
|
|
{{ asset_brand }}
|
|
{{ asset_model }}
|
|
{{ asset_type }} e.g. Laptop / Desktop
|
|
{{ asset_os }}
|
|
{{ asset_warranty_expiry }}
|
|
{{ asset_location }}
|
|
|
|
Assignment:
|
|
{{ assignment_date }}
|
|
{{ assignment_id }}
|
|
{{ return_date }}
|
|
|
|
Document / company:
|
|
{{ document_date }} Date of generation (dd/mm/yyyy)
|
|
{{ document_number }} Paperwork record ID
|
|
{{ company_name }}
|
|
{{ company_address }}
|
|
{{ admin_name }} Logged-in admin who generated the doc
|
|
|
|
PII masking:
|
|
When User.mask() is called, all Paperwork records that were generated
|
|
from a template have their merge_vars updated (PII keys replaced with
|
|
[MASKED]) and the .docx/.pdf files are regenerated.
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
from datetime import datetime
|
|
|
|
from docxtpl import DocxTemplate
|
|
from flask import current_app
|
|
|
|
# PII variable keys — these are blanked out on user mask
|
|
PII_VARS = {'user_name', 'user_email', 'user_phone'}
|
|
|
|
# Variables that survive masking (non-PII)
|
|
SAFE_VARS = {
|
|
'user_department', 'user_job_title', 'user_location',
|
|
'user_windows_id', 'user_employee_id',
|
|
}
|
|
|
|
|
|
def build_context(user, asset=None, assignment=None, paperwork=None, app=None):
|
|
"""
|
|
Build the Jinja2 context dict from ORM objects.
|
|
Used both at generation time and when re-rendering after masking.
|
|
"""
|
|
if app is None:
|
|
app = current_app._get_current_object()
|
|
|
|
ctx = {
|
|
# User
|
|
'user_name': user.display_name,
|
|
'user_email': user.display_email,
|
|
'user_phone': user.display_phone,
|
|
'user_department': user.department or '',
|
|
'user_job_title': user.job_title or '',
|
|
'user_location': user.location or '',
|
|
'user_windows_id': user.windows_id or '',
|
|
'user_employee_id': user.windows_id or '',
|
|
|
|
# Asset
|
|
'asset_serial': '',
|
|
'asset_service_tag': '',
|
|
'asset_tag': '',
|
|
'asset_brand': '',
|
|
'asset_model': '',
|
|
'asset_type': '',
|
|
'asset_os': '',
|
|
'asset_warranty_expiry': '',
|
|
'asset_location': '',
|
|
|
|
# Assignment
|
|
'assignment_date': '',
|
|
'assignment_id': '',
|
|
'return_date': '',
|
|
|
|
# Document/company
|
|
'document_date': datetime.utcnow().strftime('%d/%m/%Y'),
|
|
'document_number': str(paperwork.id) if paperwork else '',
|
|
'company_name': app.config.get('COMPANY_NAME', ''),
|
|
'company_address': app.config.get('COMPANY_ADDRESS', ''),
|
|
'admin_name': '',
|
|
}
|
|
|
|
if asset:
|
|
ctx.update({
|
|
'asset_serial': asset.serial_number or '',
|
|
'asset_service_tag': asset.service_tag or '',
|
|
'asset_tag': asset.asset_tag or '',
|
|
'asset_brand': asset.brand or '',
|
|
'asset_model': asset.model or '',
|
|
'asset_type': asset.asset_type or '',
|
|
'asset_os': asset.operating_system or '',
|
|
'asset_warranty_expiry': (asset.warranty_expiry.strftime('%d/%m/%Y')
|
|
if asset.warranty_expiry else ''),
|
|
'asset_location': asset.location or '',
|
|
})
|
|
|
|
if assignment:
|
|
ctx['assignment_date'] = (assignment.assigned_date.strftime('%d/%m/%Y')
|
|
if assignment.assigned_date else '')
|
|
ctx['assignment_id'] = str(assignment.id)
|
|
ctx['return_date'] = (assignment.returned_date.strftime('%d/%m/%Y')
|
|
if assignment.returned_date else '')
|
|
|
|
return ctx
|
|
|
|
|
|
def _docx_path(app, filename):
|
|
folder = os.path.join(app.root_path, '..', app.config.get('DOCX_FOLDER', 'docx_output'))
|
|
os.makedirs(folder, exist_ok=True)
|
|
return os.path.join(folder, filename)
|
|
|
|
|
|
def _template_path(app, filename):
|
|
folder = os.path.join(app.root_path, '..', app.config.get('TEMPLATE_FOLDER', 'doc_templates'))
|
|
return os.path.join(folder, filename)
|
|
|
|
|
|
def extract_variables(template_path):
|
|
"""
|
|
Parse a .docx file and return all unique Jinja2 variable names found
|
|
in the document text ({{ var_name }} syntax).
|
|
"""
|
|
try:
|
|
tpl = DocxTemplate(template_path)
|
|
return sorted(tpl.get_undeclared_template_variables())
|
|
except Exception:
|
|
# Fallback: open as zip and scan XML for {{ ... }}
|
|
import zipfile
|
|
vars_found = set()
|
|
try:
|
|
with zipfile.ZipFile(template_path, 'r') as z:
|
|
for name in z.namelist():
|
|
if name.endswith('.xml'):
|
|
content = z.read(name).decode('utf-8', errors='ignore')
|
|
vars_found.update(re.findall(r'\{\{\s*(\w+)\s*\}\}', content))
|
|
except Exception:
|
|
pass
|
|
return sorted(vars_found)
|
|
|
|
|
|
def render_template_to_docx(template_filepath, context, output_filename):
|
|
"""
|
|
Fill a .docx template with context values and save to DOCX_FOLDER.
|
|
Returns the saved filename.
|
|
"""
|
|
app = current_app._get_current_object()
|
|
tpl = DocxTemplate(template_filepath)
|
|
tpl.render(context)
|
|
out_path = _docx_path(app, output_filename)
|
|
tpl.save(out_path)
|
|
return output_filename
|
|
|
|
|
|
def regenerate_for_paperwork(paperwork, app=None):
|
|
"""
|
|
Re-render the .docx for an existing Paperwork record using its stored
|
|
merge_vars. Called after PII masking to overwrite files with sanitised data.
|
|
|
|
If the record was generated from a template, regenerates .docx.
|
|
Also regenerates the PDF via pdf_service if a PDF exists.
|
|
|
|
Returns (docx_filename, pdf_filename) — either may be None.
|
|
"""
|
|
from app.services.pdf_service import generate_paperwork_pdf
|
|
|
|
if app is None:
|
|
app = current_app._get_current_object()
|
|
|
|
docx_out = None
|
|
pdf_out = None
|
|
|
|
if paperwork.template_id and paperwork.template:
|
|
tpl_file = _template_path(app, paperwork.template.filename)
|
|
if os.path.exists(tpl_file):
|
|
ctx = paperwork.get_merge_vars()
|
|
out_name = paperwork.docx_filename or f'doc_{paperwork.id}.docx'
|
|
try:
|
|
render_template_to_docx(tpl_file, ctx, out_name)
|
|
docx_out = out_name
|
|
except Exception as exc:
|
|
app.logger.error('Template re-render failed for paperwork %s: %s', paperwork.id, exc)
|
|
|
|
# Always regenerate the PDF (uses pdf_service, reads from Paperwork + User model)
|
|
if paperwork.pdf_filename:
|
|
try:
|
|
pdf_out = generate_paperwork_pdf(paperwork, app)
|
|
except Exception as exc:
|
|
app.logger.error('PDF re-render failed for paperwork %s: %s', paperwork.id, exc)
|
|
|
|
return docx_out, pdf_out
|
|
|
|
|
|
def mask_variables(merge_vars: dict) -> dict:
|
|
"""Return a copy of merge_vars with PII values replaced by [MASKED]."""
|
|
masked = dict(merge_vars)
|
|
for key in PII_VARS:
|
|
if key in masked:
|
|
masked[key] = '[MASKED]'
|
|
return masked
|