wercore / pages /3_Termite_Control_Report.py
rawshal's picture
Update pages/3_Termite_Control_Report.py
0c2e0b8 verified
Raw History Blame Contribute Delete
12 kB
import streamlit as st
import io, math, os, tempfile, subprocess
from PIL import Image, ImageDraw, ImageFont, ImageOps
from docxtpl import DocxTemplate
from pypdf import PdfWriter, PdfReader
from datetime import date
from dateutil.relativedelta import relativedelta
# ── 1. CONFIG & ASSETS ────────────────────────────────────────────────────────
st.set_page_config(page_title="Wercore | Anti Termite Engine", page_icon="🐜", layout="wide")
st.markdown("""
<style>
[data-testid="stLogo"] { height: 6rem !important; object-fit: contain !important; }
</style>
""", unsafe_allow_html=True)
st.logo("WERCORE logo.png")
if not st.session_state.get("authenticated", False):
st.error("πŸ”’ **Access Denied.** Please log in through the main Wercore Portal.")
st.stop()
TPL_TERMITE = "Inspection_Template_Termite.docx"
TPL_WARRANTY = "Warranty_Template_Termite.docx"
def convert_to_pdf(docx_path, output_dir):
subprocess.run(["libreoffice", "--headless", "--convert-to", "pdf", docx_path, "--outdir", output_dir], check=True)
def load_font(size):
try: return ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", size)
except: return ImageFont.load_default()
def build_photo_grid_pages(all_photos, photos_per_page=4):
n = math.ceil(len(all_photos) / photos_per_page)
return [all_photos[i * photos_per_page:(i + 1) * photos_per_page] for i in range(n)]
def photo_grid_to_pdf_bytes(page_photos, page_num, total_pages, client_name, date_str):
PAGE_W, PAGE_H = 1240, 1754
PADDING, HEADER_H, FOOTER_H, CELL_GAP = 40, 70, 40, 20
canvas = Image.new("RGB", (PAGE_W, PAGE_H), (255, 255, 255))
draw = ImageDraw.Draw(canvas)
font_header, font_footer, font_wm = load_font(28), load_font(20), load_font(24)
draw.rectangle([(0, 0), (PAGE_W, HEADER_H)], fill=(0, 84, 166))
draw.text((PADDING, 18), f"Photo Documentation β€” {client_name} | {date_str}", font=font_header, fill=(255, 255, 255))
grid_top = HEADER_H + PADDING
grid_bot = PAGE_H - FOOTER_H - PADDING
cell_w = (PAGE_W - 2 * PADDING - CELL_GAP) // 2
cell_h = (grid_bot - grid_top - CELL_GAP) // 2
for idx, photo in enumerate(page_photos):
col, row = idx % 2, idx // 2
x0 = PADDING + col * (cell_w + CELL_GAP)
y0 = grid_top + row * (cell_h + CELL_GAP)
img = Image.open(io.BytesIO(photo)).convert("RGB")
img = ImageOps.exif_transpose(img)
img = ImageOps.fit(img, (cell_w, cell_h), Image.LANCZOS)
canvas.paste(img, (x0, y0))
wm_margin = 15
bbox = draw.textbbox((0, 0), date_str, font=font_wm)
wm_x = x0 + cell_w - (bbox[2] - bbox[0]) - wm_margin
wm_y = y0 + cell_h - (bbox[3] - bbox[1]) - wm_margin
draw.text((wm_x + 2, wm_y + 2), date_str, font=font_wm, fill=(0, 0, 0, 180))
draw.text((wm_x, wm_y), date_str, font=font_wm, fill=(255, 255, 255, 230))
draw.text((PADDING, PAGE_H - FOOTER_H + 8), "CONFIDENTIAL β€” Anti Termite Compliance Report", font=font_footer, fill=(150, 150, 150))
draw.text((PAGE_W - 180, PAGE_H - FOOTER_H + 8), f"Page {page_num} of {total_pages}", font=font_footer, fill=(150, 150, 150))
buf = io.BytesIO()
canvas.save(buf, format="PDF")
return buf.getvalue()
# ── 2. UI: SIDEBAR ────────────────────────────────────────────────────────────
with st.sidebar:
st.markdown("### Project & Warranty Details")
client_name = st.text_input("🏒 Client Name*", placeholder="e.g. AL GHURAIR PROPERTIES")
project_location = st.text_input("πŸ“ Project Location*", placeholder="e.g. Muteena 4428, Deira, Dubai")
quotation_ref = st.text_input("πŸ“„ Quotation Reference*", placeholder="e.g. QTN/WRCR/TRM/09/2025/2813")
st.markdown("---")
service_date = st.date_input("πŸ“… Date of Service*", value=date.today(), format="DD/MM/YYYY")
date_str = service_date.strftime("%d-%m-%Y")
# Auto-calculate standard 5-year warranty
default_warranty_end = service_date + relativedelta(years=5) - relativedelta(days=1)
override_warranty = st.checkbox("Custom Warranty Period?")
if override_warranty:
warranty_end_date = st.date_input("πŸ“… Warranty End Date", value=default_warranty_end, format="DD/MM/YYYY")
else:
st.info(f"5-Year Warranty Expiry: **{default_warranty_end.strftime('%d-%m-%Y')}**")
warranty_end_date = default_warranty_end
warranty_end_str = warranty_end_date.strftime("%d-%m-%Y")
st.markdown("---")
handwritten_files = st.file_uploader("Upload physical reports (PDFs only - Max 5MB)*", type=["pdf", "PDF"], accept_multiple_files=True)
# ── 3. UI: DATA ENTRY ─────────────────────────────────────────────────────────
st.header("Anti Termite Report Maker")
st.caption("Compliance, Inspection & Warranty Documentation")
st.markdown("### Treatment Details")
col_t1, col_t2 = st.columns(2)
with col_t1:
app_method = st.text_input("Application Method", value="Drilling, injection")
spray_internal = st.text_area("Spray Treatment (Internal)", value="Targeted spray application of termiticide on wall areas")
drill_injection = st.text_area("Drilling and Injection", value="Drill-Fill-Seal techniques in immediate external perimeter of the building")
with col_t2:
cov_internal = st.text_input("Internal Coverage Area", placeholder="e.g. Basement parking area")
cov_external = st.text_input("External Coverage Area", value="Immediate surrounding of entire building")
product_details = st.text_area("Product Details", value="IMIFORCE 20SC (Imidacloprid 20% W/V%, Other Ingredients 80%)\nApproved by Dubai Municipality")
st.markdown("### Observations & Recommendations")
col_o1, col_o2 = st.columns(2)
with col_o1:
infestation_level = st.text_input("Infestation Level", placeholder="e.g. Severe infestation in basement parking area")
obs_external = st.text_area("External Observations", value="No visible signs of external infestation or termite activity around the building outer walls were noticed at the time of service.")
with col_o2:
recommendations = st.text_area("Recommendations", value="Due to the severe level of infestation, we strongly recommend a repeat service after 5 years in the same areas to ensure continued protection and prevent recurrence of subterranean termite activity.", height=110)
st.markdown("### πŸ“Έ Site Photographs")
uploaded_photos = st.file_uploader("Upload up to 20+ images (Max 2MB each)", type=["jpg","jpeg","png"], accept_multiple_files=True)
st.markdown("---")
# ── 4. ENGINE: MASTER PDF ASSEMBLY ────────────────────────────────────────────
notification_zone = st.empty()
if st.button("πŸš€ Generate Anti Termite Dossier", type="primary", use_container_width=True):
errors = []
if not client_name: errors.append("Client Name is missing.")
if not project_location: errors.append("Project Location is missing.")
if not quotation_ref: errors.append("Quotation Reference is missing.")
if not cov_internal: errors.append("Internal Coverage Area is missing.")
if not infestation_level: errors.append("Infestation Level is missing.")
if not handwritten_files: errors.append("At least one Physical Report PDF must be uploaded.")
if handwritten_files:
for f in handwritten_files:
if f.size > 5 * 1024 * 1024: errors.append(f"Physical report '{f.name}' exceeds the 5MB limit.")
if uploaded_photos:
for f in uploaded_photos:
if f.size > 2 * 1024 * 1024: errors.append(f"Photo '{f.name}' exceeds 2MB.")
if errors:
with notification_zone.container():
st.error("⚠️ **Cannot generate report. Please fix the following issues:**")
for e in errors: st.error(f"❌ {e}")
st.stop()
with notification_zone.container():
with st.spinner("βš™οΈ Assembling Termite Dossier & Warranty... Please wait."):
with tempfile.TemporaryDirectory() as tmp_dir:
doc_context = {
"client_name": client_name,
"project_location": project_location,
"quotation_ref": quotation_ref,
"date_str": date_str,
"app_method": app_method,
"spray_internal": spray_internal,
"drill_injection": drill_injection,
"cov_internal": cov_internal,
"cov_external": cov_external,
"product_details": product_details,
"infestation_level": infestation_level,
"obs_external": obs_external,
"recommendations": recommendations,
"warranty_start": date_str,
"warranty_end": warranty_end_str
}
# 1. Render Main Report
if os.path.exists(TPL_TERMITE):
path_report = os.path.join(tmp_dir, "termite_report.docx")
doc = DocxTemplate(TPL_TERMITE)
doc.render(doc_context)
doc.save(path_report)
convert_to_pdf(path_report, tmp_dir)
else:
st.error(f"Template '{TPL_TERMITE}' not found in repository.")
st.stop()
# 2. Render Warranty Certificate
if os.path.exists(TPL_WARRANTY):
path_warr = os.path.join(tmp_dir, "termite_warranty.docx")
doc_warr = DocxTemplate(TPL_WARRANTY)
doc_warr.render(doc_context)
doc_warr.save(path_warr)
convert_to_pdf(path_warr, tmp_dir)
else:
st.error(f"Template '{TPL_WARRANTY}' not found in repository.")
st.stop()
# 3. Process Photos into Grid
grid_pdfs = []
if uploaded_photos:
photo_bytes = [f.read() for f in uploaded_photos]
photo_pages = build_photo_grid_pages(photo_bytes, 4)
for idx, pg in enumerate(photo_pages):
grid_pdfs.append(photo_grid_to_pdf_bytes(pg, idx+1, len(photo_pages), client_name, date_str))
# 4. Staple Everything Together
writer = PdfWriter()
def staple_pdf(file_source):
try:
reader = PdfReader(file_source)
for page in reader.pages: writer.add_page(page)
except Exception: pass
# Document Assembly Order
staple_pdf(os.path.join(tmp_dir, "termite_report.pdf")) # Main Report
for pdf_bytes in grid_pdfs: staple_pdf(io.BytesIO(pdf_bytes)) # Photos
for hw_pdf in handwritten_files:
hw_pdf.seek(0)
staple_pdf(hw_pdf) # Scanned Service Sheets
staple_pdf(os.path.join(tmp_dir, "termite_warranty.pdf")) # Warranty Certificate at the very end
output = io.BytesIO()
writer.write(output)
st.success("βœ… Anti Termite Report & Warranty generated successfully!")
st.download_button("⬇️ Download Master PDF", output.getvalue(), f"Anti_Termite_{client_name.replace(' ', '_')}.pdf", "application/pdf", type="primary", use_container_width=True)