|
import os
|
|
import re
|
|
import time
|
|
import json
|
|
import requests
|
|
import warnings
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import cv2
|
|
import easyocr
|
|
from openpyxl import load_workbook
|
|
from urllib.parse import quote
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
# ============================================================
|
|
# WARNING SUPPRESSION
|
|
# ============================================================
|
|
warnings.filterwarnings("ignore", category=UserWarning)
|
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
os.environ["PYTHONWARNINGS"] = "ignore"
|
|
|
|
# ============================================================
|
|
# CONFIGURATION
|
|
# ============================================================
|
|
USERNAME = "canofy01"
|
|
PASSWORD = "Siddhartha@2026"
|
|
|
|
# ============================================================
|
|
# BULK INPUT EXCEL
|
|
# ============================================================
|
|
|
|
EXCEL_FILE = Path(__file__).parent / "request_list.xlsx"
|
|
|
|
BASE_URL = "https://www.sezonline-ndml.co.in/User/Login.aspx"
|
|
|
|
# ============================================================
|
|
# FRAPPE CONFIGURATION
|
|
# ============================================================
|
|
|
|
# FRAPPE_URL = "https://wud.canofy.tech/"
|
|
|
|
# API_KEY = "9ed2cd4a81647e9"
|
|
# API_SECRET = "8f177698daa2370"
|
|
# ------------------------------------------eap-----------------
|
|
# FRAPPE_URL = "https://eap.canofy.tech/"
|
|
|
|
# API_KEY = "b51750272b9961b"
|
|
# API_SECRET = "ad42019b8e8a820"
|
|
# -------------------------------------------------------------
|
|
|
|
|
|
# ============================================================
|
|
# DIRECTORIES & PATHS
|
|
# ============================================================
|
|
DOWNLOAD_DIR = Path.home() / "Downloads"
|
|
SCREENSHOT_DIR = Path(__file__).parent / "screenshots"
|
|
|
|
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
|
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Initialize EasyOCR Reader
|
|
ocr_reader = easyocr.Reader(["en"], gpu=False)
|
|
|
|
|
|
# ============================================================
|
|
# SAFE FILE PATH RESOLVER (PREVENTS PERMISSIONERROR 13)
|
|
# ============================================================
|
|
def get_safe_pdf_path(target_dir: Path, req_id: str) -> Path:
|
|
"""Returns a writable file path, adding an increment suffix if the file is locked."""
|
|
base_path = target_dir / f"{req_id}.pdf"
|
|
|
|
# Check if base path can be written to
|
|
if base_path.exists():
|
|
try:
|
|
with open(base_path, "a+b"):
|
|
pass
|
|
return base_path
|
|
except (PermissionError, IOError):
|
|
pass
|
|
|
|
# If locked or unwritable, append timestamp/counter
|
|
counter = 1
|
|
while True:
|
|
alt_path = target_dir / f"{req_id}_{counter}.pdf"
|
|
if not alt_path.exists():
|
|
return alt_path
|
|
try:
|
|
with open(alt_path, "a+b"):
|
|
pass
|
|
return alt_path
|
|
except (PermissionError, IOError):
|
|
counter += 1
|
|
|
|
|
|
# ============================================================
|
|
# CAPTCHA ENGINE
|
|
# ============================================================
|
|
def resolve_captcha(image_path: Path) -> str:
|
|
print("Processing CAPTCHA image via OCR Engine...")
|
|
image = cv2.imread(str(image_path))
|
|
if image is None:
|
|
raise FileNotFoundError(f"Could not load CAPTCHA image from {image_path}")
|
|
|
|
resized = cv2.resize(image, None, fx=2.5, fy=2.5, interpolation=cv2.INTER_CUBIC)
|
|
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
|
|
filtered = cv2.bilateralFilter(gray, 9, 75, 75)
|
|
processed_img = cv2.adaptiveThreshold(
|
|
filtered, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2
|
|
)
|
|
|
|
processed_path = SCREENSHOT_DIR / "processed_captcha.png"
|
|
cv2.imwrite(str(processed_path), processed_img)
|
|
|
|
results = ocr_reader.readtext(str(processed_path), detail=0)
|
|
raw_text = "".join(results).strip()
|
|
cleaned_token = re.sub(r"[^A-Za-z0-9]", "", raw_text)
|
|
print(f"Extracted CAPTCHA Token: '{cleaned_token}'")
|
|
return cleaned_token
|
|
|
|
|
|
# ============================================================
|
|
# PERMANENT POPUP & OVERLAY IMMUNIZATION
|
|
# ============================================================
|
|
def immunize_page_against_popups(context):
|
|
"""Permanently disables driver.js and injects aggressive style blockades."""
|
|
context.add_init_script("""
|
|
// 1. Permanently neuter Driver.js constructor
|
|
window.Driver = function() {
|
|
return {
|
|
start: function() {},
|
|
reset: function() {},
|
|
destroy: function() {},
|
|
defineSteps: function() {},
|
|
showNextStep: function() {},
|
|
preventMove: function() {}
|
|
};
|
|
};
|
|
|
|
// 2. Continuous DOM guard to delete any tour artifacts instantly
|
|
const style = document.createElement('style');
|
|
style.innerHTML = `
|
|
.driver-overlay, .driver-popover, .driver-popover-tip,
|
|
.driver-popover-footer, .driver-popover-description,
|
|
.driver-popover-title, .driver-stage-no-animation,
|
|
.modal-backdrop, .overlay {
|
|
display: none !important;
|
|
visibility: hidden !important;
|
|
pointer-events: none !important;
|
|
width: 0 !important;
|
|
height: 0 !important;
|
|
opacity: 0 !important;
|
|
}
|
|
body, html {
|
|
overflow: auto !important;
|
|
pointer-events: auto !important;
|
|
}
|
|
`;
|
|
document.head ? document.head.appendChild(style) : document.addEventListener('DOMContentLoaded', () => document.head.appendChild(style));
|
|
""")
|
|
|
|
|
|
def clean_popups_now(page):
|
|
"""Runtime cleanup for elements already present in the active DOM."""
|
|
try:
|
|
page.evaluate("""
|
|
document.querySelectorAll('.driver-overlay, .driver-popover, .driver-popover-tip, .driver-popover-footer, .modal-backdrop, .overlay').forEach(el => el.remove());
|
|
if (document.body) {
|
|
document.body.classList.remove('driver-active', 'driver-fade');
|
|
document.body.style.overflow = 'auto';
|
|
document.body.style.pointerEvents = 'auto';
|
|
}
|
|
if (document.documentElement) {
|
|
document.documentElement.classList.remove('driver-active', 'driver-fade');
|
|
document.documentElement.style.overflow = 'auto';
|
|
}
|
|
""")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def safe_click(page, locator_or_selector):
|
|
clean_popups_now(page)
|
|
loc = locator_or_selector if hasattr(locator_or_selector, "click") else page.locator(locator_or_selector)
|
|
try:
|
|
loc.click(timeout=3000, force=True)
|
|
except Exception:
|
|
loc.dispatch_event("click")
|
|
|
|
|
|
# ============================================================
|
|
# LOGIN FLOW
|
|
# ============================================================
|
|
def login(page):
|
|
print()
|
|
print("=" * 60)
|
|
print("SEZ ONLINE LOGIN (AUTOMATED OCR)")
|
|
print("=" * 60)
|
|
|
|
print("Opening login page...")
|
|
page.goto(BASE_URL, wait_until="domcontentloaded")
|
|
page.wait_for_load_state("networkidle", timeout=15000)
|
|
clean_popups_now(page)
|
|
|
|
username_input = page.get_by_role("textbox", name="User Name")
|
|
password_input = page.get_by_role("textbox", name="Password")
|
|
captcha_input = page.get_by_role("textbox", name="Captcha")
|
|
login_button = page.get_by_role("button", name="Login")
|
|
captcha_image = page.locator("#captchaControlRevamp_imgCaptcha")
|
|
|
|
print("Entering username & password...")
|
|
username_input.wait_for(state="visible", timeout=30000)
|
|
username_input.fill(USERNAME)
|
|
password_input.fill(PASSWORD)
|
|
|
|
raw_captcha_path = SCREENSHOT_DIR / "raw_captcha.png"
|
|
captcha_image.wait_for(state="visible", timeout=30000)
|
|
captcha_image.screenshot(path=str(raw_captcha_path))
|
|
|
|
captcha_token = resolve_captcha(raw_captcha_path)
|
|
print("Entering extracted CAPTCHA...")
|
|
captcha_input.fill(captcha_token)
|
|
|
|
print("Submitting login form...")
|
|
clean_popups_now(page)
|
|
safe_click(page, login_button)
|
|
|
|
try:
|
|
page.wait_for_load_state("domcontentloaded", timeout=15000)
|
|
page.wait_for_load_state("networkidle", timeout=10000)
|
|
except Exception:
|
|
pass
|
|
|
|
page.wait_for_timeout(2000)
|
|
clean_popups_now(page)
|
|
print("Login submission completed. Current URL:", page.url)
|
|
|
|
# Handle ConfirmUserCredentials screen if redirected there
|
|
if "ConfirmUserCredentials.aspx" in page.url:
|
|
print("Handling ConfirmUserCredentials screen...")
|
|
try:
|
|
confirm_btn = page.locator("input[type='submit'][value*='Continue'], input[type='submit'][value*='OK'], #btnSubmit, #btnConfirm")
|
|
if confirm_btn.count() > 0:
|
|
safe_click(page, confirm_btn.first)
|
|
page.wait_for_load_state("networkidle", timeout=10000)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
# ============================================================
|
|
# NAVIGATE TO PRINT & EXPORT PDF
|
|
# ============================================================
|
|
def navigate_to_print(page, req_id: str, target_dir: Path) -> str:
|
|
print()
|
|
print("=" * 60)
|
|
print("NAVIGATE TO PRINT")
|
|
print("=" * 60)
|
|
print("Target Request ID:", req_id)
|
|
|
|
page.wait_for_load_state("networkidle", timeout=10000)
|
|
clean_popups_now(page)
|
|
|
|
# 1. Search Request Link
|
|
print("Opening Search Request...")
|
|
search_request_button = page.get_by_role("link", name="Search Request")
|
|
search_request_button.wait_for(state="attached", timeout=30000)
|
|
safe_click(page, search_request_button)
|
|
|
|
page.wait_for_load_state("networkidle", timeout=10000)
|
|
clean_popups_now(page)
|
|
|
|
# 2. Clear Date Fields via JS
|
|
print("Clearing date filters...")
|
|
from_date_input = page.locator("#ctl00_DefaultContent_DatePicker_RequestFromDate_txtMaskedDate")
|
|
to_date_input = page.locator("#ctl00_DefaultContent_DatePicker_RequestToDate_txtMaskedDate")
|
|
|
|
from_date_input.evaluate("(el) => el.value = ''")
|
|
to_date_input.evaluate("(el) => el.value = ''")
|
|
|
|
# 3. Enter Request ID
|
|
print(f"Entering Request ID: {req_id}...")
|
|
request_id_textfield = page.locator("#ctl00_DefaultContent_txtRequestID")
|
|
request_id_textfield.fill(req_id)
|
|
|
|
# 4. Search
|
|
print("Searching...")
|
|
search_button = page.get_by_role("button", name="Search")
|
|
safe_click(page, search_button)
|
|
|
|
page.wait_for_timeout(2000)
|
|
clean_popups_now(page)
|
|
|
|
# 5. Open Search Result Link
|
|
print("Opening request details...")
|
|
req_id_link = page.locator(f"xpath=//a[text()='{req_id}']")
|
|
req_id_link.wait_for(state="attached", timeout=30000)
|
|
safe_click(page, req_id_link)
|
|
|
|
page.wait_for_timeout(1500)
|
|
clean_popups_now(page)
|
|
|
|
# 6. Click View/Print Button
|
|
print("Clicking View / Print...")
|
|
view_receipt = page.locator("#ctl00_DefaultContent_btnPrintBOE")
|
|
view_receipt.wait_for(state="attached", timeout=30000)
|
|
safe_click(page, view_receipt)
|
|
|
|
# 7. Interact inside Print Dialog Iframe
|
|
print("Handling Print Modal iframe...")
|
|
print_frame = page.frame_locator("#dialog-body")
|
|
|
|
original_checkbox = print_frame.locator("#cbNo_0")
|
|
original_checkbox.wait_for(state="attached", timeout=30000)
|
|
original_checkbox.dispatch_event("click")
|
|
print("Selected 'Original' document option.")
|
|
|
|
ok_button = print_frame.locator("xpath=//input[@value='OK']")
|
|
ok_button.wait_for(state="attached", timeout=30000)
|
|
|
|
# 8. Intercept New Window Tab for PDF Export
|
|
print("Triggering print preview window...")
|
|
context = page.context
|
|
|
|
with context.expect_page(timeout=30000) as page_info:
|
|
ok_button.dispatch_event("click")
|
|
|
|
print_page = page_info.value
|
|
print("Print window intercepted successfully.")
|
|
|
|
print_page.wait_for_load_state("networkidle", timeout=30000)
|
|
print_page.wait_for_timeout(2000)
|
|
|
|
# Resolve safe destination path (avoiding file lock collisions)
|
|
resolved_pdf_path = get_safe_pdf_path(target_dir, req_id)
|
|
|
|
# 9. Save as PDF
|
|
print(f"Generating PDF at: {resolved_pdf_path}...")
|
|
print_page.pdf(
|
|
path=str(resolved_pdf_path),
|
|
print_background=True,
|
|
landscape=False
|
|
)
|
|
|
|
# 10. Verify File Creation
|
|
if not resolved_pdf_path.exists() or resolved_pdf_path.stat().st_size == 0:
|
|
raise RuntimeError(f"PDF creation failed or output file is empty: {resolved_pdf_path}")
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("PDF CREATED SUCCESSFULLY")
|
|
print("=" * 60)
|
|
print("Location:", resolved_pdf_path)
|
|
print("File Size:", resolved_pdf_path.stat().st_size, "bytes")
|
|
|
|
print_page.close()
|
|
return str(resolved_pdf_path)
|
|
|
|
# ============================================================
|
|
# SEZ PORTAL LOGOUT
|
|
# ============================================================
|
|
|
|
def logout(page):
|
|
print()
|
|
print("=" * 60)
|
|
print("LOGGING OUT FROM SEZ PORTAL")
|
|
print("=" * 60)
|
|
|
|
clean_popups_now(page)
|
|
|
|
# Exact logout control used by the original framework
|
|
logout_button = page.locator("#ctl00_LoginStatus")
|
|
|
|
logout_button.wait_for(
|
|
state="visible",
|
|
timeout=30000
|
|
)
|
|
|
|
print("Clicking Logout...")
|
|
|
|
safe_click(page, logout_button)
|
|
|
|
# Give the portal time to complete logout
|
|
try:
|
|
page.wait_for_load_state(
|
|
"domcontentloaded",
|
|
timeout=15000
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
page.wait_for_timeout(1500)
|
|
|
|
print("Successfully Logged out")
|
|
print("Current URL:", page.url)
|
|
|
|
def read_request_excel():
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("READING BULK REQUEST EXCEL")
|
|
print("=" * 60)
|
|
|
|
if not EXCEL_FILE.exists():
|
|
raise FileNotFoundError(
|
|
f"Excel file not found:\n{EXCEL_FILE}"
|
|
)
|
|
|
|
workbook = load_workbook(EXCEL_FILE)
|
|
sheet = workbook.active
|
|
|
|
# Read header row
|
|
headers = {}
|
|
|
|
for cell in sheet[1]:
|
|
if cell.value is not None:
|
|
headers[str(cell.value).strip()] = cell.column
|
|
|
|
required_columns = {
|
|
"Request ID",
|
|
"Doctype Name"
|
|
}
|
|
|
|
missing_columns = (
|
|
required_columns - set(headers.keys())
|
|
)
|
|
|
|
if missing_columns:
|
|
raise RuntimeError(
|
|
"Missing required Excel columns: "
|
|
+ ", ".join(sorted(missing_columns))
|
|
)
|
|
|
|
requests_list = []
|
|
|
|
for row_number in range(2, sheet.max_row + 1):
|
|
|
|
request_id = sheet.cell(
|
|
row=row_number,
|
|
column=headers["Request ID"]
|
|
).value
|
|
|
|
doctype_name = sheet.cell(
|
|
row=row_number,
|
|
column=headers["Doctype Name"]
|
|
).value
|
|
|
|
if request_id is None:
|
|
continue
|
|
|
|
request_id = str(request_id).strip()
|
|
|
|
if not request_id:
|
|
continue
|
|
|
|
if doctype_name is None:
|
|
raise RuntimeError(
|
|
f"Doctype Name is empty "
|
|
f"for Excel row {row_number}."
|
|
)
|
|
|
|
doctype_name = str(
|
|
doctype_name
|
|
).strip()
|
|
|
|
requests_list.append({
|
|
"row_number": row_number,
|
|
"request_id": request_id,
|
|
"doctype_name": doctype_name
|
|
})
|
|
|
|
print(
|
|
"Total requests found:",
|
|
len(requests_list)
|
|
)
|
|
|
|
for item in requests_list:
|
|
print(
|
|
f"Row {item['row_number']} | "
|
|
f"Request ID: {item['request_id']} | "
|
|
f"Doctype: {item['doctype_name']}"
|
|
)
|
|
|
|
return workbook, sheet, headers, requests_list
|
|
|
|
def update_excel_row(
|
|
sheet,
|
|
headers,
|
|
row_number,
|
|
download_status=None,
|
|
timestamp=None,
|
|
filename=None,
|
|
upload_status=None,
|
|
upload_timestamp=None,
|
|
upload_response=None
|
|
):
|
|
|
|
if download_status is not None:
|
|
sheet.cell(
|
|
row=row_number,
|
|
column=headers["Download Status"]
|
|
).value = download_status
|
|
|
|
if timestamp is not None:
|
|
sheet.cell(
|
|
row=row_number,
|
|
column=headers["Timestamp"]
|
|
).value = timestamp
|
|
|
|
if filename is not None:
|
|
sheet.cell(
|
|
row=row_number,
|
|
column=headers["Filename"]
|
|
).value = filename
|
|
|
|
if upload_status is not None:
|
|
sheet.cell(
|
|
row=row_number,
|
|
column=headers["Upload Status"]
|
|
).value = upload_status
|
|
|
|
if upload_timestamp is not None:
|
|
sheet.cell(
|
|
row=row_number,
|
|
column=headers["Upload Timestamp"]
|
|
).value = upload_timestamp
|
|
|
|
if upload_response is not None:
|
|
sheet.cell(
|
|
row=row_number,
|
|
column=headers["Upload response"]
|
|
).value = upload_response
|
|
|
|
# ============================================================
|
|
# MAIN ENTRY POINT
|
|
# ============================================================
|
|
|
|
def main():
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("SEZ PRINT AUTOMATION - BULK RUNNER")
|
|
print("=" * 60)
|
|
|
|
# ========================================================
|
|
# READ EXCEL
|
|
# ========================================================
|
|
|
|
workbook, sheet, headers, request_rows = (
|
|
read_request_excel()
|
|
)
|
|
|
|
downloaded_items = []
|
|
|
|
with sync_playwright() as p:
|
|
|
|
browser = p.chromium.launch(
|
|
headless=False,
|
|
args=[
|
|
"--start-maximized",
|
|
"--no-sandbox"
|
|
]
|
|
)
|
|
|
|
context = browser.new_context(
|
|
no_viewport=True,
|
|
user_agent=(
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
|
"AppleWebKit/537.36 "
|
|
"(KHTML, like Gecko) "
|
|
"Chrome/120.0.0.0 Safari/537.36"
|
|
)
|
|
)
|
|
|
|
# ====================================================
|
|
# POPUP PROTECTION
|
|
# ====================================================
|
|
|
|
immunize_page_against_popups(context)
|
|
|
|
def auto_accept_dialogs(dialog):
|
|
|
|
print(
|
|
f"\n[Dialog Intercepted] "
|
|
f"{dialog.message}"
|
|
)
|
|
|
|
dialog.accept()
|
|
|
|
context.on(
|
|
"dialog",
|
|
auto_accept_dialogs
|
|
)
|
|
|
|
page = context.new_page()
|
|
|
|
try:
|
|
|
|
# =================================================
|
|
# STEP 1
|
|
# LOGIN ONCE
|
|
# =================================================
|
|
|
|
login(page)
|
|
|
|
# =================================================
|
|
# STEP 2
|
|
# DOWNLOAD ALL PDFs
|
|
# =================================================
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("STARTING BULK PDF DOWNLOAD")
|
|
print("=" * 60)
|
|
|
|
for item in request_rows:
|
|
|
|
row_number = item["row_number"]
|
|
request_id = item["request_id"]
|
|
doctype_name = item["doctype_name"]
|
|
|
|
print()
|
|
print("#" * 60)
|
|
print("PROCESSING REQUEST")
|
|
print("#" * 60)
|
|
|
|
print("Excel Row :", row_number)
|
|
print("Request ID :", request_id)
|
|
print("Doctype Name :", doctype_name)
|
|
|
|
try:
|
|
|
|
saved_pdf = navigate_to_print(
|
|
page,
|
|
request_id,
|
|
DOWNLOAD_DIR
|
|
)
|
|
|
|
update_excel_row(
|
|
sheet,
|
|
headers,
|
|
row_number,
|
|
download_status="Success",
|
|
timestamp=datetime.now().strftime(
|
|
"%Y-%m-%d %H:%M:%S"
|
|
),
|
|
filename=Path(saved_pdf).name
|
|
)
|
|
|
|
workbook.save(EXCEL_FILE)
|
|
|
|
downloaded_items.append({
|
|
"row_number": row_number,
|
|
"request_id": request_id,
|
|
"doctype_name": doctype_name,
|
|
"pdf_path": saved_pdf
|
|
})
|
|
|
|
print()
|
|
print(
|
|
f"PDF download successful "
|
|
f"for {request_id}"
|
|
)
|
|
|
|
print(
|
|
"PDF:",
|
|
saved_pdf
|
|
)
|
|
|
|
except Exception as exc:
|
|
|
|
print()
|
|
print(
|
|
f"PDF DOWNLOAD FAILED "
|
|
f"for {request_id}"
|
|
)
|
|
|
|
print(
|
|
f"{type(exc).__name__}: {exc}"
|
|
)
|
|
|
|
# Download failed.
|
|
# Frappe upload is not attempted.
|
|
update_excel_row(
|
|
sheet,
|
|
headers,
|
|
row_number,
|
|
download_status="Failed",
|
|
timestamp=datetime.now().strftime(
|
|
"%Y-%m-%d %H:%M:%S"
|
|
)
|
|
)
|
|
|
|
workbook.save(EXCEL_FILE)
|
|
|
|
# Continue with next request.
|
|
continue
|
|
|
|
# =================================================
|
|
# STEP 3
|
|
# LOGOUT ONLY AFTER ALL PDFs ARE DOWNLOADED
|
|
# =================================================
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("ALL PDF DOWNLOADS COMPLETED")
|
|
print("=" * 60)
|
|
|
|
logout(page)
|
|
|
|
except Exception as exc:
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("SEZ PORTAL PIPELINE FAILED")
|
|
print("=" * 60)
|
|
|
|
print(
|
|
f"{type(exc).__name__}: {exc}"
|
|
)
|
|
|
|
failure_path = (
|
|
SCREENSHOT_DIR /
|
|
"failure.png"
|
|
)
|
|
|
|
page.screenshot(
|
|
path=str(failure_path),
|
|
full_page=True
|
|
)
|
|
|
|
print(
|
|
"Saved failure diagnostic screenshot to:",
|
|
failure_path
|
|
)
|
|
|
|
raise
|
|
|
|
finally:
|
|
|
|
context.close()
|
|
browser.close()
|
|
|
|
# ========================================================
|
|
# STEP 4
|
|
# FRAPPE API PROCESSING
|
|
#
|
|
# IMPORTANT:
|
|
# SEZ browser is already logged out and closed.
|
|
# ========================================================
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("STARTING FRAPPE BULK PROCESSING")
|
|
print("=" * 60)
|
|
|
|
for item in downloaded_items:
|
|
|
|
row_number = item["row_number"]
|
|
request_id = item["request_id"]
|
|
doctype_name = item["doctype_name"]
|
|
pdf_path = item["pdf_path"]
|
|
|
|
print()
|
|
print("#" * 60)
|
|
print("FRAPPE PROCESSING")
|
|
print("#" * 60)
|
|
|
|
print("Request ID :", request_id)
|
|
print("Doctype :", doctype_name)
|
|
print("PDF :", pdf_path)
|
|
|
|
try:
|
|
|
|
frappe_result = process_pdf_in_frappe(
|
|
request_id,
|
|
doctype_name,
|
|
pdf_path
|
|
)
|
|
|
|
# ================================================
|
|
# UPLOAD / ATTACHMENT RESULT
|
|
# ================================================
|
|
|
|
if frappe_result.get("already_attached", False):
|
|
|
|
update_excel_row(
|
|
sheet,
|
|
headers,
|
|
row_number,
|
|
upload_status="Already Attached",
|
|
upload_timestamp=datetime.now().strftime(
|
|
"%Y-%m-%d %H:%M:%S"
|
|
),
|
|
upload_response=(
|
|
f"PDF already attached | "
|
|
f"Document: "
|
|
f"{frappe_result['frappe_docname']} | "
|
|
f"System Doc No: "
|
|
f"{frappe_result['system_doc_no']} | "
|
|
f"PDF: {Path(pdf_path).name}"
|
|
)
|
|
)
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("PDF ALREADY ATTACHED")
|
|
print("=" * 60)
|
|
|
|
print(
|
|
"Request ID :",
|
|
request_id
|
|
)
|
|
|
|
print(
|
|
"Frappe Document:",
|
|
frappe_result["frappe_docname"]
|
|
)
|
|
|
|
print(
|
|
"System Doc No. :",
|
|
frappe_result["system_doc_no"]
|
|
)
|
|
|
|
print(
|
|
"Upload Status :",
|
|
"Already Attached"
|
|
)
|
|
|
|
else:
|
|
|
|
update_excel_row(
|
|
sheet,
|
|
headers,
|
|
row_number,
|
|
upload_status="Success",
|
|
upload_timestamp=datetime.now().strftime(
|
|
"%Y-%m-%d %H:%M:%S"
|
|
),
|
|
upload_response=(
|
|
f"PDF attached successfully | "
|
|
f"Document: "
|
|
f"{frappe_result['frappe_docname']} | "
|
|
f"System Doc No: "
|
|
f"{frappe_result['system_doc_no']} | "
|
|
f"PDF: {Path(pdf_path).name}"
|
|
)
|
|
)
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("PDF ATTACHED SUCCESSFULLY")
|
|
print("=" * 60)
|
|
|
|
print(
|
|
"Request ID :",
|
|
request_id
|
|
)
|
|
|
|
print(
|
|
"Frappe Document:",
|
|
frappe_result["frappe_docname"]
|
|
)
|
|
|
|
print(
|
|
"System Doc No. :",
|
|
frappe_result["system_doc_no"]
|
|
)
|
|
|
|
print(
|
|
"Upload Status :",
|
|
"Success"
|
|
)
|
|
|
|
# Save Excel after every Frappe request
|
|
workbook.save(EXCEL_FILE)
|
|
|
|
except Exception as exc:
|
|
|
|
# ================================================
|
|
# VALIDATION / UPLOAD FAILURE
|
|
# ================================================
|
|
|
|
print()
|
|
print(
|
|
f"FRAPPE PROCESSING FAILED "
|
|
f"FOR {request_id}"
|
|
)
|
|
|
|
print(
|
|
f"{type(exc).__name__}: {exc}"
|
|
)
|
|
|
|
update_excel_row(
|
|
sheet,
|
|
headers,
|
|
row_number,
|
|
upload_status="Failed – Frappe document mismatch",
|
|
upload_timestamp=datetime.now().strftime(
|
|
"%Y-%m-%d %H:%M:%S"
|
|
),
|
|
upload_response=str(exc)
|
|
)
|
|
|
|
workbook.save(EXCEL_FILE)
|
|
|
|
print(
|
|
"Upload Status:",
|
|
"Failed – Frappe document mismatch"
|
|
)
|
|
|
|
# Continue with next request.
|
|
continue
|
|
|
|
# ========================================================
|
|
# STEP 5
|
|
# SAVE UPDATED EXCEL
|
|
# ========================================================
|
|
|
|
workbook.save(EXCEL_FILE)
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("BULK PROCESSING COMPLETED")
|
|
print("=" * 60)
|
|
|
|
print(
|
|
"Updated Excel:",
|
|
EXCEL_FILE
|
|
)
|
|
# ============================================================
|
|
# FRAPPE API HEADERS
|
|
# ============================================================
|
|
|
|
def get_frappe_headers():
|
|
|
|
return {
|
|
"Authorization": f"token {API_KEY}:{API_SECRET}",
|
|
"Accept": "application/json",
|
|
}
|
|
|
|
|
|
def find_frappe_document(
|
|
request_id: str,
|
|
expected_doctype: str
|
|
):
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("FINDING FRAPPE DOCUMENT")
|
|
print("=" * 60)
|
|
|
|
request_id = str(request_id).strip()
|
|
expected_doctype = str(
|
|
expected_doctype
|
|
).strip()
|
|
|
|
if not API_KEY or not API_SECRET:
|
|
raise RuntimeError(
|
|
"Frappe API_KEY and API_SECRET are not configured."
|
|
)
|
|
|
|
print("Excel Request ID:", request_id)
|
|
print("Excel Doctype:", expected_doctype)
|
|
|
|
# --------------------------------------------------------
|
|
# SEARCH THE EXACT DOCTYPE FROM EXCEL
|
|
# --------------------------------------------------------
|
|
|
|
encoded_doctype = quote(
|
|
expected_doctype,
|
|
safe=""
|
|
)
|
|
|
|
url = (
|
|
f"{FRAPPE_URL.rstrip('/')}"
|
|
f"/api/resource/{encoded_doctype}"
|
|
)
|
|
|
|
params = {
|
|
"filters": json.dumps([
|
|
[
|
|
expected_doctype,
|
|
"request_id",
|
|
"=",
|
|
request_id
|
|
]
|
|
]),
|
|
"fields": json.dumps([
|
|
"name",
|
|
"request_id",
|
|
"docstatus"
|
|
]),
|
|
"limit_page_length": 100,
|
|
}
|
|
|
|
response = requests.get(
|
|
url,
|
|
headers=get_frappe_headers(),
|
|
params=params,
|
|
timeout=120
|
|
)
|
|
|
|
print(
|
|
"Frappe search status:",
|
|
response.status_code
|
|
)
|
|
|
|
if not response.ok:
|
|
print(response.text)
|
|
|
|
raise RuntimeError(
|
|
"Failed to search Frappe document."
|
|
)
|
|
|
|
documents = response.json().get(
|
|
"data",
|
|
[]
|
|
)
|
|
|
|
print(
|
|
"Matching documents found:",
|
|
len(documents)
|
|
)
|
|
|
|
if len(documents) == 0:
|
|
raise RuntimeError(
|
|
"Frappe document mismatch"
|
|
)
|
|
|
|
if len(documents) > 1:
|
|
raise RuntimeError(
|
|
"Frappe document mismatch"
|
|
)
|
|
|
|
document = documents[0]
|
|
|
|
frappe_docname = document.get("name")
|
|
|
|
if not frappe_docname:
|
|
raise RuntimeError(
|
|
"Frappe document mismatch"
|
|
)
|
|
|
|
# --------------------------------------------------------
|
|
# GET COMPLETE FRAPPE DOCUMENT
|
|
# --------------------------------------------------------
|
|
|
|
document_url = (
|
|
f"{FRAPPE_URL.rstrip('/')}"
|
|
f"/api/resource/{encoded_doctype}/"
|
|
f"{quote(frappe_docname, safe='')}"
|
|
)
|
|
|
|
document_response = requests.get(
|
|
document_url,
|
|
headers=get_frappe_headers(),
|
|
timeout=120
|
|
)
|
|
|
|
if not document_response.ok:
|
|
raise RuntimeError(
|
|
"Frappe document mismatch"
|
|
)
|
|
|
|
frappe_document = (
|
|
document_response
|
|
.json()
|
|
.get("data", {})
|
|
)
|
|
|
|
# ========================================================
|
|
# VALIDATION 1
|
|
# Excel Request ID == Frappe request_id
|
|
# ========================================================
|
|
|
|
excel_request_id = str(
|
|
request_id
|
|
).strip()
|
|
|
|
frappe_request_id = str(
|
|
frappe_document.get(
|
|
"request_id",
|
|
""
|
|
)
|
|
).strip()
|
|
|
|
print()
|
|
print("Request ID validation:")
|
|
print(
|
|
"Excel :",
|
|
excel_request_id
|
|
)
|
|
print(
|
|
"Frappe:",
|
|
frappe_request_id
|
|
)
|
|
|
|
if excel_request_id != frappe_request_id:
|
|
|
|
print("Request ID validation: FAILED")
|
|
|
|
raise RuntimeError(
|
|
"Frappe document mismatch"
|
|
)
|
|
|
|
print("Request ID validation: PASSED")
|
|
|
|
# ========================================================
|
|
# VALIDATION 2
|
|
# Excel Doctype == Frappe document doctype
|
|
# ========================================================
|
|
|
|
frappe_doctype = str(
|
|
frappe_document.get(
|
|
"doctype",
|
|
""
|
|
)
|
|
).strip()
|
|
|
|
print()
|
|
print("Doctype validation:")
|
|
print(
|
|
"Excel :",
|
|
expected_doctype
|
|
)
|
|
print(
|
|
"Frappe:",
|
|
frappe_doctype
|
|
)
|
|
|
|
if expected_doctype != frappe_doctype:
|
|
|
|
print("Doctype validation: FAILED")
|
|
|
|
raise RuntimeError(
|
|
"Frappe document mismatch"
|
|
)
|
|
|
|
print("Doctype validation: PASSED")
|
|
|
|
# ========================================================
|
|
# BOTH VALIDATIONS PASSED
|
|
# ========================================================
|
|
|
|
print()
|
|
print("BOTH FRAPPE VALIDATIONS PASSED")
|
|
print("Document Name:", frappe_docname)
|
|
print(
|
|
"System Doc No.:",
|
|
frappe_document.get("system_doc_no")
|
|
)
|
|
|
|
return (
|
|
frappe_docname,
|
|
frappe_document.get("system_doc_no"),
|
|
frappe_document.get("docstatus")
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# CHECK WHETHER PDF IS ALREADY ATTACHED
|
|
# ============================================================
|
|
|
|
def is_pdf_already_attached(
|
|
pdf_path: str,
|
|
frappe_docname: str,
|
|
doctype_name: str,
|
|
request_id: str
|
|
) -> bool:
|
|
|
|
pdf_path = Path(pdf_path)
|
|
|
|
# Always use the Request ID as the canonical PDF name.
|
|
# This prevents files such as 12345_1.pdf from bypassing
|
|
# the "Already Attached" check.
|
|
expected_filename = f"{str(request_id).strip()}.pdf"
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("CHECKING EXISTING FRAPPE ATTACHMENT")
|
|
print("=" * 60)
|
|
|
|
print("Doctype :", doctype_name)
|
|
print("Document :", frappe_docname)
|
|
print("Request ID :", request_id)
|
|
print("Local PDF :", pdf_path.name)
|
|
print("Expected PDF :", expected_filename)
|
|
|
|
encoded_file_doctype = quote(
|
|
"File",
|
|
safe=""
|
|
)
|
|
|
|
url = (
|
|
f"{FRAPPE_URL.rstrip('/')}"
|
|
f"/api/resource/{encoded_file_doctype}"
|
|
)
|
|
|
|
# Check using the canonical Request-ID filename,
|
|
# NOT the local filename that may contain _1, _2, etc.
|
|
params = {
|
|
"filters": json.dumps([
|
|
[
|
|
"File",
|
|
"attached_to_doctype",
|
|
"=",
|
|
doctype_name
|
|
],
|
|
[
|
|
"File",
|
|
"attached_to_name",
|
|
"=",
|
|
frappe_docname
|
|
],
|
|
[
|
|
"File",
|
|
"file_name",
|
|
"=",
|
|
expected_filename
|
|
]
|
|
]),
|
|
"fields": json.dumps([
|
|
"name",
|
|
"file_name",
|
|
"file_url",
|
|
"attached_to_doctype",
|
|
"attached_to_name"
|
|
]),
|
|
"limit_page_length": 10
|
|
}
|
|
|
|
response = requests.get(
|
|
url,
|
|
headers=get_frappe_headers(),
|
|
params=params,
|
|
timeout=120
|
|
)
|
|
|
|
print(
|
|
"Attachment check status:",
|
|
response.status_code
|
|
)
|
|
|
|
if not response.ok:
|
|
|
|
print(
|
|
"Attachment check response:",
|
|
response.text
|
|
)
|
|
|
|
raise RuntimeError(
|
|
"Failed to check existing Frappe attachment."
|
|
)
|
|
|
|
files = response.json().get(
|
|
"data",
|
|
[]
|
|
)
|
|
|
|
if files:
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("PDF ALREADY ATTACHED")
|
|
print("=" * 60)
|
|
|
|
for file_data in files:
|
|
|
|
print(
|
|
"File Name:",
|
|
file_data.get("file_name")
|
|
)
|
|
|
|
print(
|
|
"File URL:",
|
|
file_data.get("file_url")
|
|
)
|
|
|
|
return True
|
|
|
|
print("No existing attachment found.")
|
|
|
|
return False
|
|
# ============================================================
|
|
# ATTACH PDF TO FRAPPE DOCUMENT
|
|
# ============================================================
|
|
|
|
def attach_pdf_to_frappe(
|
|
pdf_path: str,
|
|
frappe_docname: str,
|
|
doctype_name: str,
|
|
request_id: str
|
|
):
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("ATTACHING PDF TO FRAPPE DOCUMENT")
|
|
print("=" * 60)
|
|
|
|
pdf_path = Path(
|
|
pdf_path
|
|
)
|
|
|
|
if not pdf_path.exists():
|
|
|
|
raise FileNotFoundError(
|
|
f"PDF file does not exist:\n{pdf_path}"
|
|
)
|
|
|
|
if pdf_path.stat().st_size == 0:
|
|
|
|
raise RuntimeError(
|
|
f"PDF file is empty:\n{pdf_path}"
|
|
)
|
|
|
|
# --------------------------------------------------------
|
|
# CHECK IF THIS EXACT PDF IS ALREADY ATTACHED
|
|
# --------------------------------------------------------
|
|
|
|
if is_pdf_already_attached(
|
|
pdf_path,
|
|
frappe_docname,
|
|
doctype_name,
|
|
request_id
|
|
):
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("SKIPPING PDF UPLOAD")
|
|
print("=" * 60)
|
|
|
|
print(
|
|
"Reason: PDF is already attached "
|
|
"to this Frappe document."
|
|
)
|
|
|
|
return {
|
|
"already_attached": True,
|
|
"file_name": pdf_path.name,
|
|
"doctype": doctype_name,
|
|
"docname": frappe_docname
|
|
}
|
|
|
|
print(
|
|
"Document:",
|
|
frappe_docname
|
|
)
|
|
|
|
print(
|
|
"PDF:",
|
|
pdf_path
|
|
)
|
|
|
|
print(
|
|
"Size:",
|
|
pdf_path.stat().st_size,
|
|
"bytes"
|
|
)
|
|
|
|
# --------------------------------------------------------
|
|
# Frappe standard upload_file endpoint
|
|
# --------------------------------------------------------
|
|
|
|
upload_url = (
|
|
f"{FRAPPE_URL}/api/method/upload_file"
|
|
)
|
|
|
|
headers = get_frappe_headers()
|
|
|
|
# --------------------------------------------------------
|
|
# Multipart upload
|
|
# --------------------------------------------------------
|
|
|
|
with open(
|
|
pdf_path,
|
|
"rb"
|
|
) as pdf_file:
|
|
|
|
files = {
|
|
"file": (
|
|
pdf_path.name,
|
|
pdf_file,
|
|
"application/pdf"
|
|
)
|
|
}
|
|
|
|
data = {
|
|
"doctype": doctype_name,
|
|
"docname": frappe_docname,
|
|
"is_private": "0",
|
|
}
|
|
|
|
print(
|
|
"Uploading PDF..."
|
|
)
|
|
|
|
response = requests.post(
|
|
upload_url,
|
|
headers=headers,
|
|
files=files,
|
|
data=data,
|
|
timeout=120
|
|
)
|
|
|
|
print(
|
|
"Upload status:",
|
|
response.status_code
|
|
)
|
|
|
|
if not response.ok:
|
|
|
|
print(
|
|
"Upload response:"
|
|
)
|
|
|
|
print(
|
|
response.text
|
|
)
|
|
|
|
raise RuntimeError(
|
|
"PDF upload/attachment failed."
|
|
)
|
|
|
|
try:
|
|
|
|
result = response.json()
|
|
|
|
except ValueError:
|
|
|
|
raise RuntimeError(
|
|
"Frappe returned a non-JSON response "
|
|
"after PDF upload."
|
|
)
|
|
|
|
print()
|
|
print(
|
|
"Frappe attachment response:"
|
|
)
|
|
|
|
print(
|
|
json.dumps(
|
|
result,
|
|
indent=4,
|
|
ensure_ascii=False
|
|
)
|
|
)
|
|
|
|
file_data = result.get(
|
|
"message",
|
|
{}
|
|
)
|
|
|
|
file_url = None
|
|
|
|
if isinstance(
|
|
file_data,
|
|
dict
|
|
):
|
|
|
|
file_url = file_data.get(
|
|
"file_url"
|
|
)
|
|
|
|
print()
|
|
print(
|
|
"=" * 60
|
|
)
|
|
|
|
print(
|
|
"PDF ATTACHED SUCCESSFULLY"
|
|
)
|
|
|
|
print(
|
|
"=" * 60
|
|
)
|
|
|
|
print(
|
|
"Frappe Document:",
|
|
frappe_docname
|
|
)
|
|
|
|
print(
|
|
"Attached File:",
|
|
pdf_path.name
|
|
)
|
|
|
|
if file_url:
|
|
|
|
print(
|
|
"Frappe File URL:",
|
|
file_url
|
|
)
|
|
|
|
return result
|
|
|
|
# ============================================================
|
|
# COMPLETE FRAPPE POST-PROCESSING
|
|
# ============================================================
|
|
|
|
def process_pdf_in_frappe(
|
|
request_id: str,
|
|
doctype_name: str,
|
|
pdf_path: str
|
|
):
|
|
|
|
print()
|
|
print()
|
|
print("#" * 60)
|
|
print("FRAPPE POST-PROCESSING")
|
|
print("#" * 60)
|
|
|
|
# --------------------------------------------------------
|
|
# STEP 1
|
|
# Find existing Frappe document using
|
|
# Request ID + Doctype from Excel
|
|
# --------------------------------------------------------
|
|
|
|
frappe_docname, system_doc_no, docstatus = (
|
|
find_frappe_document(
|
|
request_id,
|
|
doctype_name
|
|
)
|
|
)
|
|
|
|
# --------------------------------------------------------
|
|
# STEP 2
|
|
# Do not attach/submit a cancelled document
|
|
# --------------------------------------------------------
|
|
|
|
if docstatus == 2:
|
|
|
|
raise RuntimeError(
|
|
f"{doctype_name} '{frappe_docname}' "
|
|
"is cancelled."
|
|
)
|
|
|
|
# --------------------------------------------------------
|
|
# STEP 3
|
|
# If already submitted, stop
|
|
# --------------------------------------------------------
|
|
|
|
if docstatus == 1:
|
|
|
|
raise RuntimeError(
|
|
f"{doctype_name} '{frappe_docname}' "
|
|
"is already submitted. "
|
|
"PDF was not attached/submitted again."
|
|
)
|
|
|
|
# --------------------------------------------------------
|
|
# STEP 4
|
|
# Attach PDF
|
|
# --------------------------------------------------------
|
|
|
|
attachment_result = attach_pdf_to_frappe(
|
|
pdf_path,
|
|
frappe_docname,
|
|
doctype_name,
|
|
request_id
|
|
)
|
|
|
|
print()
|
|
print("=" * 60)
|
|
print("PROCESS COMPLETED SUCCESSFULLY")
|
|
print("=" * 60)
|
|
|
|
print("Request ID :", request_id)
|
|
print(f"{doctype_name} :", frappe_docname)
|
|
print("System Doc No. :", system_doc_no)
|
|
print("PDF :", Path(pdf_path).name)
|
|
if attachment_result.get("already_attached", False):
|
|
print("Attachment :", "Already Attached")
|
|
else:
|
|
print("Attachment :", "Successfully attached")
|
|
print("=" * 60)
|
|
|
|
return {
|
|
"request_id": request_id,
|
|
"frappe_docname": frappe_docname,
|
|
"system_doc_no": system_doc_no,
|
|
"pdf_path": str(pdf_path),
|
|
"already_attached": attachment_result.get(
|
|
"already_attached",
|
|
False
|
|
),
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|