import arcpy
import docx
from datetime import datetime
import os
from docx.shared import Inches
# Define the paths
inputfilename = r"Y:\GIS\FieldSummary_TEMPLATE.docx"
outputfolder = r"Y:\GIS\GeneratedDocuments"
sde = r'\\test\gis\ACCS_rw.sde'
# Set the workspace to the SDE connection
arcpy.env.workspace = sde
# Define the full feature class path
feature_class = os.path.join(sde, 'FieldWork')
# Define field mapping
field_mapping = {
"<<PROJECT_NAME>>": "PROJECT_NAME",
"<<FIELD_DATE>>": "FIELD_DATE",
"<<ARCH_CREW>>": "ARCH_CREW",
"<<PERMIT>>": "PERMIT",
"<<DIVISION>>": "DIVISION",
"<<METHOD>>": "METHOD",
"<<DIST_EXIST>>": "DIST_EXIST",
"<<DESCRIPTION>>": "DESCRIPTION",
"<<DIST_REQ>>": "DIST_REQ",
"<<HISTORY>>": "HISTORY",
"<<SUB_OB>>": "SUB_OB",
"<<ARCH_OB>>": "ARCH_OB",
"<<REC>>": "REC"
}
# Define the feature fields list based on field mapping plus required fields
feature_fields = list(field_mapping.values()) + ['last_edited_date', 'DEPT']
# Print debug information
print(f"Workspace: {arcpy.env.workspace}")
print(f"Feature Class: {feature_class}")
print(f"Fields to be queried: {feature_fields}")
# Rest of the code remains exactly the same as in the previous artifact, starting from here:
# Subtype to domain mapping
subtype_to_domain = {
0: "DBLBranch",
1: "ENGBranch",
2: "PARKSBranch",
3: "REFMBranch",
4: "CMOBranch"
}
def get_domain_description(domain_name, code):
"""Get the domain description for a given code"""
try:
# Get the workspace from the feature class path
workspace = os.path.dirname(arcpy.env.workspace)
# Get the domain object
domains = arcpy.da.ListDomains(workspace)
target_domain = next((d for d in domains if d.name == domain_name), None)
if target_domain and hasattr(target_domain, 'codedValues'):
# Return the domain description if found, otherwise return the original code
return target_domain.codedValues.get(code, str(code))
return str(code)
except Exception as e:
print(f"Error getting domain description: {e}")
return str(code)
def replace_text_in_paragraph(paragraph, old_text, new_text):
"""Replace text in a paragraph while preserving formatting"""
if old_text in paragraph.text:
inline = paragraph._element
for run in paragraph.runs:
if old_text in run.text:
new_runs = run.text.replace(old_text, new_text)
run.text = new_runs
def docx_find_replace_text(doc, old_text, new_text):
"""Replace text in all document parts including headers and footers"""
# Replace in headers
for section in doc.sections:
for paragraph in section.header.paragraphs:
replace_text_in_paragraph(paragraph, old_text, new_text)
# Replace in header tables
for table in section.header.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
replace_text_in_paragraph(paragraph, old_text, new_text)
# Replace in main document
for paragraph in doc.paragraphs:
replace_text_in_paragraph(paragraph, old_text, new_text)
# Replace in tables
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
replace_text_in_paragraph(paragraph, old_text, new_text)
# Replace in footers
for section in doc.sections:
for paragraph in section.footer.paragraphs:
replace_text_in_paragraph(paragraph, old_text, new_text)
# Replace in footer tables
for table in section.footer.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
replace_text_in_paragraph(paragraph, old_text, new_text)
def replace_date_in_footer(doc):
"""Replace date in footer with current date"""
current_date = datetime.now().strftime("%d %B %Y")
for section in doc.sections:
footer = section.footer
# Replace in footer paragraphs
for paragraph in footer.paragraphs:
if "<<DATE2>>" in paragraph.text:
# If the paragraph contains only the date placeholder
if paragraph.text.strip() == "<<DATE2>>":
paragraph.text = current_date
else:
# Handle cases where there might be other text in the paragraph
for run in paragraph.runs:
if "<<DATE2>>" in run.text:
run.text = run.text.replace("<<DATE2>>", current_date)
# Replace in footer tables
for table in footer.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
if "<<DATE2>>" in paragraph.text:
# If the paragraph contains only the date placeholder
if paragraph.text.strip() == "<<DATE2>>":
paragraph.text = current_date
else:
# Handle cases where there might be other text in the paragraph
for run in paragraph.runs:
if "<<DATE2>>" in run.text:
run.text = run.text.replace("<<DATE2>>", current_date)
def add_header_footer_if_missing(doc):
"""Add header and footer sections if they don't exist"""
for section in doc.sections:
# Add header if it doesn't exist
header = section.header
if not header.paragraphs:
header.add_paragraph()
# Add footer if it doesn't exist
footer = section.footer
if not footer.paragraphs:
footer.add_paragraph()
# Add the date placeholder to the first paragraph
footer.paragraphs[0].text = "<<DATE2>>"
# Main execution
try:
# Create output folder if it doesn't exist
if not os.path.exists(outputfolder):
os.makedirs(outputfolder)
with arcpy.da.SearchCursor(feature_class, feature_fields) as cursor:
for feature in cursor:
last_edited_date = feature[-2]
subtype_code = feature[-1]
if last_edited_date is None:
print("Skipping record with no last_edited_date.")
continue
project_name_index = feature_fields.index("PROJECT_NAME")
project_name = feature[project_name_index]
if not project_name:
print("Skipping record with no project name.")
continue
output_filename = os.path.join(outputfolder, f"{project_name}_FieldSummary.docx")
if os.path.exists(output_filename):
doc_mod_time = datetime.fromtimestamp(os.path.getmtime(output_filename))
if doc_mod_time >= last_edited_date:
print(f"Skipping {output_filename}, already up-to-date.")
continue
doc = docx.Document(inputfilename)
# Ensure header and footer exist
add_header_footer_if_missing(doc)
for placeholder, field in field_mapping.items():
field_index = feature_fields.index(field)
value = feature[field_index]
# Handle DIVISION with subtype-specific domains
if field == "DIVISION":
domain_name = subtype_to_domain.get(subtype_code)
if domain_name:
value = get_domain_description(domain_name, value)
# Convert None to empty string
if value is None:
value = ""
# Replace placeholders in the template
docx_find_replace_text(doc, placeholder, str(value))
# Replace date in the footer
replace_date_in_footer(doc)
doc.save(output_filename)
print(f"Created or updated: {output_filename}")
print("Process completed successfully.")
except Exception as e:
print(f"An error occurred: {str(e)}")
raise
Hi all,
I’ve been dealing with a confusing predicament. I have the above python script running successfully in Notebooks in ArcGIS Pro.
This script creates a new docx or updates an exisitng docx replacing placeholders within the template docx with the appropriate fields from the feature class in an enterprise geodatabase. This includes relevant info in the header and footer of the template docx.
It runs without a problem and successfully accomplishes the workflow. However, when I plug it in the Start Up Script Portion of the Creator or PythonCreator in FME, I am getting an error stating:
An error occurred: 'Section' object has no attribute 'header' Python Exception <AttributeError>: 'Section' object has no attribute 'header'
I have tried many different iterations of the script but am still running into that error when running it. I have already accounted for experimenting with the python compatibility portion and ensuring it is running ArcGIS Python 3.6+ which is the appropriate version.
Has anybody else come across this issue whereby a script works well in Notebooks/ArcGIS Pro and not when plugged into FME?