WinCC Explorer · Python EXE · Excel Reporting

Convert Python to EXE for WinCC Excel Report Generation

Create a one-button reporting workflow: the WinCC operator clicks Softwell_Report, a Windows filter popup opens, the operator selects date/time and furnace, and a formatted Excel report is generated from SQL Server.

WinCC Explorer button Tkinter filter popup PyInstaller EXE Filtered XLSX report

Lab Overview

Python Learning SeriesEstimated time: 75 minutesDifficulty: Intermediate–Advanced

Prerequisites / What You’ll Need

  • Working Python Excel-report script
  • PyInstaller and the required Python packages
  • WinCC test station with database access
Quick answer

Save the reporting program as softwell_report.py, package it with pyinstaller --onefile --windowed --name Softwell_Report softwell_report.py, deploy the generated EXE to a fixed local folder on the WinCC station, and configure the Softwell_Report button’s VBScript click action to run that absolute EXE path. With no command-line arguments, the EXE opens the date/time filter popup and generates the Excel report.

  • Python is not required on the runtime PC after a successful standalone build.
  • The SQL Server ODBC driver, Windows database permission and output-folder permission are still required.
  • Always prove the EXE independently before connecting it to the WinCC button.

How This Python-to-EXE WinCC Solution Fits Together

This tutorial answers the complete search path: how to convert Python to EXE with PyInstaller, launch the standalone executable from a Siemens WinCC Explorer button, collect an operator-selected date/time range, query SQL Server, and export the filtered result to Excel.

Related Search Topics

convert Python script to EXE on Windows · launch EXE from WinCC button · WinCC SQL Server Excel report · PyInstaller tkinter pandas openpyxl

1. WinCC One-Button Excel Reporting Workflow

WinCC Deployment • Python EXE Architecture

WinCC Button → Python EXE → SQL → Excel

WinCC launches an external packaged Python reporting application. The EXE collects filters, queries SQL Server and generates the Excel workbook outside the WinCC project.

1

WinCC Button

Operator clicks the configured report button in Graphics Runtime.

VBScript action Softwell_Report.exe
2

Launch EXE

Windows starts the packaged Python application.

PyInstaller build dist\Softwell_Report.exe
3

Filter UI

Tkinter collects start date, end date and furnace selection.

start / end SQF_No
4

Query SQL

pyodbc reads the matching SQF_DB event rows.

SELECT ... WHERE DT BETWEEN ? AND ?
5

Build Report

pandas organizes data and openpyxl formats the workbook.

DataFrame → XLSX header / filters / widths
6

Confirm Output

Show the saved file path and optionally open the report.

D:\SQF_Reports\...xlsx Report ready
WinCCRuntime trigger
PyInstallerPython → EXE
pyodbc / pandasData layer
openpyxl / XLSXReport layer
Architecture:WinCC→EXE→Tkinter Filters→SQL Server→pandas/openpyxl→Excel

Code-rendered diagram: the runtime station executes the packaged EXE; Python does not need to be exposed as editable source code to the WinCC operator.

The EXE is an external reporting application. WinCC launches it but does not need to host the Python GUI or Excel-generation libraries inside the WinCC project.

Lab Manual Index: 9 Guided Exercises

Every section of this tutorial carries its own lab manual — objective, procedure, a line-by-line explanation, copy-paste practice code, the exact output to expect, a checkpoint and a troubleshooting table. Work through them in order; the EXE and WinCC labs assume the Python labs before them already pass. Total bench time is about 80 minutes.

2. Software and Deployment Requirements

  • Build PC: compatible Python, pyodbc, pandas, openpyxl and pyinstaller.
  • Runtime PC: supported Windows version and Microsoft ODBC Driver 17 for SQL Server.
  • Network access from the WinCC station to SOFTWELL\WINCC.
  • Windows identity running the EXE must have SELECT access to SQF_DB.dbo.tblEvent.
  • Write permission to the selected report folder.
  • WinCC Graphics Designer permission to configure and execute the button action.
install_requirements.cmd
python -m pip install --upgrade pip
python -m pip install pyodbc pandas openpyxl pyinstaller
Architecture check: build and test on an environment compatible with the target WinCC computer. PyInstaller output is platform-specific. Confirm Windows version, 32/64-bit constraints, endpoint-security policy and ODBC-driver availability before site deployment.

3. How the Python Application Works

The program supports two modes:

  • Popup mode: double-click the EXE or launch it from WinCC with no arguments. run_popup() opens the operator form.
  • Command-line mode: pass start, end, optional furnace and output folder. This supports unattended or parameter-driven launches.
01_entry_point.py
def main():
    if len(sys.argv) == 1:
        run_popup()
    else:
        run_command_line()


if __name__ == "__main__":
    main()

The attached source duplicated this entry-point block. The corrected version in this blog contains it only once, preventing the GUI or command-line report from running twice.

Lab Manual 3 — Understand the Dual-Mode Application

6 min

Objective

Prove that the same program opens a filter popup when double-clicked and runs headless when WinCC passes arguments, so one EXE serves both the operator and the SCADA button.

Procedure

  1. Run python softwell_report.py with no arguments and confirm the popup opens.
  2. Close it, then run the same file with a start and end date on the command line.
  3. Confirm the second run produces a workbook with no window appearing.
  4. Read sys.argv in both cases so the switch is not a mystery.

What each line does

CodeMeaning in this lab
len(sys.argv) == 1sys.argv[0] is always the script or EXE path. A length of one means no arguments were supplied, so a human started it.
run_popup()Opens the tkinter filter window for an operator.
run_command_line()Parses arguments with argparse, so WinCC can pass a date range and furnace number.
if __name__ == "__main__":Lets another script import generate_report() without launching a window.
getattr(sys, "frozen", False)True only inside a PyInstaller EXE. It is how application_folder() finds the right folder in both modes.

Copy-paste practice code

01_mode_check.py
import sys
from pathlib import Path


def application_folder():
    if getattr(sys, "frozen", False):
        return Path(sys.executable).resolve().parent
    return Path(__file__).resolve().parent


print("Arguments      :", sys.argv)
print("Argument count :", len(sys.argv))
print("Running as EXE :", getattr(sys, "frozen", False))
print("App folder     :", application_folder())
print("Mode selected  :", "popup" if len(sys.argv) == 1 else "command line")

Run this file twice — once by double-clicking and once as python 01_mode_check.py 2026-07-20 2026-07-27. The same file must report two different modes.

Expected result

console output
Arguments      : ['01_mode_check.py']
Argument count : 1
Running as EXE : False
App folder     : C:\Softwell_Report
Mode selected  : popup

Checkpoint

Pass when: No arguments selects popup mode, any argument selects command-line mode, and the app folder points at the file's own directory.

If it fails

SymptomWhat to check
The popup opens even from WinCCWinCC is launching the EXE with no arguments. Add the date range to the shell.Run command line.
NameError: __file__ is not definedYou are inside an interactive shell or a frozen EXE. The frozen branch exists for exactly this reason.
Reports save to a random folderThe working directory differs from the EXE folder. Always build paths from application_folder(), never from a relative path.

4. Validate Date, Time and Furnace Filters

02_parse_datetime.py
def parse_datetime(value, end_of_day=False):
    """Accept YYYY-MM-DD or YYYY-MM-DD HH:MM[:SS]."""
    try:
        parsed = datetime.fromisoformat(value.strip())
    except ValueError as error:
        raise ValueError(
            "Use YYYY-MM-DD HH:MM:SS, for example 2026-07-27 14:30:00"
        ) from error

    # A date-only entry has 10 characters, so supply the missing time
    if len(value.strip()) == 10:
        parsed = datetime.combine(
            parsed.date(),
            time(23, 59, 59) if end_of_day else time.min,
        )

    return parsed

The form accepts YYYY-MM-DD or YYYY-MM-DD HH:MM[:SS]. A date-only start becomes midnight; a date-only end becomes 23:59:59. The program rejects an end before the start. “All Furnaces” becomes None; values 1–3 become integers.

For maximum precision, consider making the end boundary exclusive—such as the next day at midnight—especially when SQL timestamps can contain fractions beyond whole seconds.

Lab Manual 4 — Validate Date, Time and Furnace Filters

8 min

Objective

Turn operator text into real datetime objects, supply a sensible time when only a date is typed, and fail with a helpful message rather than a traceback.

Procedure

  1. Run the function against a date-only value for both start and end.
  2. Run it against a full timestamp.
  3. Run it against deliberate rubbish and read the error message.
  4. Confirm that an end date earlier than the start is rejected.

What each line does

CodeMeaning in this lab
datetime.fromisoformat(...)Parses ISO-8601 text. It accepts both 2026-07-27 and 2026-07-27 14:30:00.
len(value.strip()) == 10A date-only entry is exactly ten characters. That is how the code detects a missing time.
time(23, 59, 59) if end_of_day else time.minA date-only start becomes 00:00:00 and a date-only end becomes 23:59:59, so the whole day is included.
raise ValueError(...) from errorReplaces the cryptic parser message with an example format, while from error keeps the original chain for the log.
sqf_no = None if furnace_value in {"", "All Furnaces"}An empty box or the default text means no furnace filter is applied.

Copy-paste practice code

02_filter_drill.py
from datetime import datetime, time

tests = [
    ("2026-07-27", False),
    ("2026-07-27", True),
    ("2026-07-27 14:30:00", False),
    ("27/07/2026", False),
    ("", False),
]

for value, end_of_day in tests:
    label = f"{value!r:<24} end_of_day={end_of_day}"
    try:
        print(f"{label} -> {parse_datetime(value, end_of_day)}")
    except ValueError as error:
        print(f"{label} -> REJECTED: {error}")

for furnace_text in ("All Furnaces", "", "2", "abc"):
    try:
        value = furnace_text.strip()
        sqf_no = None if value in {"", "All Furnaces"} else int(value)
        print(f"furnace {furnace_text!r:<15} -> {sqf_no}")
    except ValueError:
        print(f"furnace {furnace_text!r:<15} -> REJECTED: not a number")

Paste this below the parse_datetime() function. The two rejected cases are the point of the drill — an operator will type both of them eventually.

Expected result

console output
'2026-07-27'             end_of_day=False -> 2026-07-27 00:00:00
'2026-07-27'             end_of_day=True  -> 2026-07-27 23:59:59
'2026-07-27 14:30:00'    end_of_day=False -> 2026-07-27 14:30:00
'27/07/2026'             end_of_day=False -> REJECTED: Use YYYY-MM-DD HH:MM:SS, for example 2026-07-27 14:30:00
''                       end_of_day=False -> REJECTED: Use YYYY-MM-DD HH:MM:SS, for example 2026-07-27 14:30:00
furnace 'All Furnaces'    -> None
furnace ''                -> None
furnace '2'               -> 2
furnace 'abc'             -> REJECTED: not a number

Checkpoint

Pass when: A date-only start gives midnight, a date-only end gives 23:59:59, and both bad inputs are rejected with the example format rather than a traceback.

If it fails

SymptomWhat to check
A whole day of data is missing from the reportThe end date was treated as midnight. end_of_day=True must be passed for the end value.
Operators type dd/mm/yyyy and it failsfromisoformat only accepts ISO order. Either train the format or add a second parse attempt with strptime.
A furnace typo crashes the popupint() raises before the handler. Validate the combo box value before converting, or catch ValueError around it.
End before start is acceptedThe comparison in generate_report() was removed. It must run before the query.

5. Query SQL Server with Bound Parameters

03_filtered_query.py
query = """
    SELECT
        [DT], [TM], [SQF_No], [ChargeNo],
        [Event_From], [Event_To],
        [Temp_Set], [Temp_Act],
        [Cp_Set], [Cp_Act],
        [Oil_Set], [Oil_Act],
        [Jacket_Set], [Jacket_Act],
        [Fan_Status]
    FROM [dbo].[tblEvent]
    WHERE [DT] >= ? AND [DT] < ?
"""

params = [start_datetime.date(), end_datetime.date() + timedelta(days=1)]

if sqf_no is not None:
    query += " AND [SQF_No] = ?"
    params.append(sqf_no)

query += " ORDER BY [DT] DESC, [TM] DESC;"

The initial SQL range safely narrows the candidate dates. The cursor results become a pandas DataFrame. Because this schema stores date and time separately, the program converts both columns and creates Event_Timestamp, then applies the exact inclusive range in pandas.

Performance note: filtering a separate text time column in pandas can transfer more rows than necessary. A production schema is cleaner when it stores one indexed datetime2 event timestamp; SQL Server can then apply the exact range efficiently.

Lab Manual 5 — Query SQL Server with Bound Parameters

9 min

Objective

Fetch only the days that can possibly match using bound parameters, then narrow to the exact minute in pandas, and understand why the work is split across the two.

Procedure

  1. Build the query with no furnace filter and print it.
  2. Build it again with a furnace number and compare the two statements and parameter lists.
  3. Run it and check the row count before the time filter is applied.
  4. Apply the Event_Timestamp filter and check the count again.

What each line does

CodeMeaning in this lab
WHERE [DT] >= ? AND [DT] < ?A half-open date range. Using < end + 1 day instead of <= includes the whole final day whatever the time type.
end_datetime.date() + timedelta(days=1)Builds that exclusive upper bound. Both values go in as parameters, never as formatted text.
query += " AND [SQF_No] = ?"The filter is appended only when a furnace was chosen, and its value is appended to params in the same order.
cursor.execute(query, params)Positional binding. The order of params must match the order the markers appear in the finished statement.
pd.to_timedelta(dataframe["TM"])Converts the separate TIME column so it can be added to the normalised date.
.between(start_datetime, end_datetime)The exact time filter. SQL narrowed to whole days; pandas narrows to the minute.

Copy-paste practice code

03_query_drill.py
from datetime import timedelta

def build_query(start_datetime, end_datetime, sqf_no=None):
    query = "SELECT ... FROM [dbo].[tblEvent] WHERE [DT] >= ? AND [DT] < ?"
    params = [start_datetime.date(), end_datetime.date() + timedelta(days=1)]

    if sqf_no is not None:
        query += " AND [SQF_No] = ?"
        params.append(sqf_no)

    query += " ORDER BY [DT] DESC, [TM] DESC;"
    return query, params


start = parse_datetime("2026-07-20")
end = parse_datetime("2026-07-27", end_of_day=True)

for sqf_no in (None, 2):
    query, params = build_query(start, end, sqf_no)
    print("furnace:", sqf_no)
    print("  markers   :", query.count("?"))
    print("  parameters:", len(params), params)
    print("  statement :", query[-60:])
    print()

The marker count and the parameter count must always match. This drill is the fastest way to catch an appended filter whose value was forgotten.

Expected result

console output
furnace: None
  markers   : 2
  parameters: 2 [datetime.date(2026, 7, 20), datetime.date(2026, 7, 28)]
  statement :  ORDER BY [DT] DESC, [TM] DESC;

furnace: 2
  markers   : 3
  parameters: 3 [datetime.date(2026, 7, 20), datetime.date(2026, 7, 28), 2]
  statement : [SQF_No] = ? ORDER BY [DT] DESC, [TM] DESC;

Checkpoint

Pass when: Markers equal parameters in both cases, the upper bound is one day after the end date, and the ORDER BY clause stays at the end of the statement.

If it fails

SymptomWhat to check
COUNT field incorrect or syntax errorA filter was appended without appending its value, so markers and parameters no longer match.
The last day is missing from the reportThe upper bound used <= against a date, which excludes everything after midnight on the final day.
ORDER BY appears in the middle of the queryThe furnace filter was appended after the ORDER BY. Append filters first, then the sort.
The time filter removes every rowTM did not convert to a timedelta, so Event_Timestamp is NaT. Print dataframe['TM'].head() to see the real format.

6. Generate the Formatted Excel Report

The exporter creates the folder, uses a timestamped filename, writes data below a report header, and adds title, selected period, furnace, record count, header styling, filters, frozen panes, widths and date formatting.

04_export_excel.py
output_file = output_folder / f"SQF_Filtered_Report_{furnace_text}_{stamp}.xlsx"

with pd.ExcelWriter(output_file, engine="openpyxl") as writer:
    dataframe.to_excel(
        writer,
        sheet_name="Filtered Report",
        index=False,
        startrow=4,
    )

    worksheet = writer.sheets["Filtered Report"]

    # Rows 1 to 3 hold the report title and filter summary
    worksheet["A1"] = "SQF FURNACE EVENT REPORT"

    # Data headers land on row 5, so freeze below them
    worksheet.freeze_panes = "A6"
    worksheet.auto_filter.ref = worksheet.dimensions

Using numeric column positions with get_column_letter() avoids failures caused by merged title cells, which do not expose the same column_letter property as normal cells.

Lab Manual 6 — Generate the Formatted Excel Report

10 min

Objective

Produce a workbook that looks like a delivered report: merged title, filter summary, formatted header row, frozen panes and readable column widths.

Procedure

  1. Write the workbook with startrow=4 and open it in Excel.
  2. Confirm the title is merged across the data width and the summary lines read correctly.
  3. Confirm the header sits on row 5 and the freeze line is under it.
  4. Check the timestamp column formatting from row 6 down.

What each line does

CodeMeaning in this lab
startrow=4Leaves rows 1 to 4 free, so the data header lands on row 5. Change this and every row number below must change too.
worksheet.merge_cells(...)Merges the title across the data width, using max(1, len(dataframe.columns)) so an empty result cannot produce a zero-column merge.
PatternFill("solid", fgColor="1F4E78")The navy title band. solid must be supplied or no colour appears.
header_row = 5The data header. It is a separate variable precisely because startrow decides it.
worksheet.freeze_panes = "A6"Freezes everything above row 6, so the title and header stay visible while scrolling.
get_column_letter(column_number)Row 1 holds merged cells, which have no column_letter attribute. The numeric position is used instead — this is the fix for a common crash.
iter_rows(min_row=6)Applies the datetime number format to data rows only, skipping the title block.

Copy-paste practice code

04_verify_report.py
from openpyxl import load_workbook
from openpyxl.utils import get_column_letter

workbook = load_workbook(output_file)
worksheet = workbook["Filtered Report"]

print("A1 title    :", worksheet["A1"].value)
print("A1 fill     :", worksheet["A1"].fill.fgColor.rgb)
print("A2 period   :", worksheet["A2"].value)
print("A3 summary  :", worksheet["A3"].value)
print("merged      :", list(worksheet.merged_cells.ranges))
print("header row 5:", [cell.value for cell in worksheet[5]][:4])
print("freeze panes:", worksheet.freeze_panes)
print("auto filter :", worksheet.auto_filter.ref)
print("A6 format   :", worksheet["A6"].number_format)
print("data rows   :", worksheet.max_row - 5)
workbook.close()

Reading the saved file back is the only honest check. A script that prints “report generated” without reopening the workbook has proved nothing.

Expected result

console output
A1 title    : SQF FURNACE EVENT REPORT
A1 fill     : 001F4E78
A2 period   : Period: 2026-07-20 00:00:00 to 2026-07-27 23:59:59
A3 summary  : Furnace: All | Records: 50
merged      : [<MergedCellRange A1:P1>]
header row 5: ['Event_Timestamp', 'DT', 'TM', 'SQF_No']
freeze panes: A6
auto filter : A1:P55
A6 format   : DD-MM-YYYY HH:MM:SS
data rows   : 50

Checkpoint

Pass when: The title merges across the full data width, the header sits on row 5, freeze panes reads A6, and the timestamp column carries the date format from row 6 down.

If it fails

SymptomWhat to check
AttributeError: column_letter on MergedCellThe width loop used column_cells[0].column_letter. Merged cells have no such attribute — use get_column_letter(column_number) as the corrected code does.
The filter dropdowns sit on the title rowauto_filter.ref = worksheet.dimensions starts at A1, which includes the title block. Set it to the header range instead: f"A5:{get_column_letter(worksheet.max_column)}{worksheet.max_row}".
The header is on the wrong rowstartrow and header_row disagree. header_row must be startrow + 1.
Title colour does not appearPatternFill was created without the solid fill type.
Timestamps show as numbersThe column is not a real datetime. Check that Event_Timestamp was built before export.

7. Create a Python EXE with PyInstaller

  1. Save the corrected program as softwell_report.py.
  2. Open Command Prompt in that folder.
  3. Create and activate an approved virtual environment if required by your deployment process.
  4. Install the dependencies.
  5. Run the PyInstaller command.
05_build_exe.cmd
python -m PyInstaller --clean --noconfirm --onefile --windowed --name Softwell_Report softwell_report.py

On Windows Command Prompt, enter the command on one line. The generated application is normally located at:

PyInstaller output
dist\Softwell_Report.exe
OptionPurpose
--onefilePackages the application into one deployable EXE
--windowedPrevents a console window behind the Tkinter popup
--name Softwell_ReportCreates the required operator-facing filename
--cleanClears cached build artifacts
--noconfirmReplaces previous build output without prompting

If a clean test reports a missing module or data file, inspect the PyInstaller warning file and add only the required hidden import or collection option. Avoid copying an untested build directly to a production HMI.

Lab Manual 7 — Build the EXE with PyInstaller

10 min

Objective

Compile the script into a single EXE that runs on a WinCC station with no Python installed, and understand what each build flag changes.

Procedure

  1. Activate the virtual environment that already has pyodbc, pandas and openpyxl.
  2. Run the PyInstaller command from the folder containing the script.
  3. Wait for the build to finish and find the EXE in the dist folder.
  4. Note the build time and the EXE size so a later rebuild can be compared.

What each line does

CodeMeaning in this lab
python -m PyInstallerRuns PyInstaller from the active environment, so it bundles the packages that environment actually has.
--cleanDeletes the previous build cache. Skip it and a stale module can silently survive into the new EXE.
--noconfirmOverwrites the previous dist folder without prompting, which matters for a scripted rebuild.
--onefileProduces a single EXE instead of a folder. It is easier to deploy but slower to start, because it unpacks to a temp folder each run.
--windowedSuppresses the console window. Correct for the operator popup — but it also hides command-line output, so test headless mode before adding this flag.
--name Softwell_ReportSets the EXE name. Without it the EXE takes the script filename.

Copy-paste practice code

05_build_and_check.cmd
cd C:\SoftwellLabs\wincc-report
.venv\Scripts\activate

python -m PyInstaller --clean --noconfirm --onefile --windowed --name Softwell_Report softwell_report.py

dir dist
python -c "import os;p=r'dist\Softwell_Report.exe';print(p, round(os.path.getsize(p)/1048576,1), 'MB')"

Record the size. A sudden jump on a later rebuild usually means an unintended package was pulled into the environment.

Expected result

console output
Building EXE from EXE-00.toc completed successfully.

 Directory of C:\SoftwellLabs\wincc-report\dist
19-09-2026  18:06        42,318,912 Softwell_Report.exe

dist\Softwell_Report.exe 40.4 MB

Checkpoint

Pass when: The build reports success, the EXE exists in dist, and its size is in the tens of megabytes — pandas alone accounts for most of that.

If it fails

SymptomWhat to check
ModuleNotFoundError when the EXE runsPyInstaller bundled a different environment. Activate the correct venv before building, and rebuild with --clean.
Antivirus quarantines the EXECommon with --onefile. Add a folder exclusion on the WinCC station, or ship the folder build instead.
The EXE takes 10 seconds to startExpected for --onefile, which unpacks to a temp folder. Drop --onefile for a faster folder-based build.
A console window flashes--windowed was omitted. Add it, but only after the command-line mode is proven.
tkinter missing in the EXEThe Python installation lacks tcl/tk. Reinstall Python with the tcl/tk option enabled and rebuild.

8. Test the EXE Before WinCC Integration

  1. Run dist\Softwell_Report.exe directly.
  2. Confirm the popup opens once.
  3. Generate an “All Furnaces” report for a short known period.
  4. Generate a single-furnace report.
  5. Verify record count, first/last timestamp, workbook formatting and output folder.
  6. Test invalid date text, end-before-start, no-data range, SQL outage and unwritable folder.
  7. Test from the same Windows account/context used by WinCC Runtime.

A successful interactive desktop test does not automatically prove the WinCC Runtime launch context has the same database, filesystem or desktop permissions.

Lab Manual 8 — Test the EXE Before WinCC Integration

8 min

Objective

Confirm both modes of the compiled EXE on the target machine before any SCADA object is touched, so later faults can be isolated to the button rather than the program.

Procedure

  1. Copy the EXE to its deployment folder on the WinCC station.
  2. Double-click it and generate a report from the popup.
  3. Run it from a command prompt with a date range and confirm a file appears with no window.
  4. Check that the report folder is created beside the EXE, not in a temp directory.

What each line does

CodeMeaning in this lab
dist\Softwell_Report.exeThe build output. Copy this one file to the station; nothing else from dist or build is needed for a onefile build.
Popup modeStarted with no arguments. Proves tkinter, the SQL connection and Excel export all survived compilation.
Command-line modeStarted with arguments. Proves the path WinCC will actually use.
application_folder()Inside the EXE this resolves to the EXE's own folder, not the PyInstaller temp folder — that is why reports land next to the EXE.

Copy-paste practice code

06_exe_smoke_test.cmd
cd C:\Softwell_Report

REM 1. Command-line mode, all furnaces
Softwell_Report.exe 2026-07-20 2026-07-27

REM 2. Command-line mode, single furnace
Softwell_Report.exe 2026-07-20 2026-07-27 2

REM 3. Explicit output folder
Softwell_Report.exe 2026-07-20 2026-07-27 --output C:\Softwell_Report\SQF_Reports

REM 4. Confirm the workbooks landed
dir SQF_Reports\*.xlsx

Build the EXE without --windowed for this test so the console output is visible, then rebuild with it once the four commands pass.

Expected result

console output
Exported 1284 records to:
C:\Softwell_Report\SQF_Reports\SQF_Filtered_Report_All_19-09-2026_18-11-02.xlsx

 Directory of C:\Softwell_Report\SQF_Reports
19-09-2026  18:11            34,816 SQF_Filtered_Report_All_19-09-2026_18-11-02.xlsx
19-09-2026  18:11            21,504 SQF_Filtered_Report_SQF-2_19-09-2026_18-11-04.xlsx

Checkpoint

Pass when: Both modes succeed on the target machine, the furnace filter changes the record count and the filename, and reports land beside the EXE.

If it fails

SymptomWhat to check
Nothing happens when the EXE is double-clickedIt was built with --windowed and crashed before the window appeared. Rebuild without that flag to see the error.
Login failed for userThe EXE runs under a different Windows account on the station. Grant that account SELECT on the database.
Reports appear in a Temp folderA relative path was used instead of application_folder(). In a onefile EXE the working directory is a temp extraction folder.
The EXE works for you but not for the operatorFile or folder permissions. Test while logged in as the operator account, not as an administrator.

9. Launch the Python EXE from a WinCC Explorer Button

In WinCC Graphics Designer, add a button labeled Softwell_Report. On its mouse-click event, configure a VBScript action similar to the following and use the actual deployed path:

WinCC_Button_OnClick.vbs
Sub OnClick(ByVal Item)
    Dim shell
    Set shell = CreateObject("WScript.Shell")
    shell.Run """C:\Softwell_Report\Softwell_Report.exe""", 1, False
    Set shell = Nothing
End Sub

The quoted path supports spaces. Window style 1 requests a normal visible window. False tells WinCC not to block while the EXE is running. The exact event wrapper generated by your WinCC version should be preserved; place the WScript.Shell statements inside that generated click procedure.

Do not launch multiple copies accidentally: disable the button temporarily, add a named mutex/single-instance check in Python, or train operators to wait for the popup. Multiple instances can create duplicate reports and extra SQL load.

Lab Manual 9 — Launch the EXE from a WinCC Button

9 min

Objective

Attach the EXE to a WinCC Explorer button so an operator generates a report with one click, without freezing WinCC Runtime while the report is produced.

Procedure

  1. Open the WinCC graphics designer and add a button to the screen.
  2. Attach a VBScript action to its mouse-click event.
  3. Paste the script and correct the path to the deployment folder.
  4. Activate Runtime and click the button.

What each line does

CodeMeaning in this lab
Sub OnClick(ByVal Item)The standard WinCC VBS click handler. Item is the object that was clicked.
CreateObject("WScript.Shell")Windows Script Host shell object, which is what actually launches the external program.
"""C:\Softwell_Report\..."""Three double quotes produce one literal quote in VBScript. The quotes around the path are essential once the path contains a space.
shell.Run path, 1, FalseThe 1 shows the window normally. The False means do not wait — WinCC keeps running while the report is generated.
Set shell = NothingReleases the COM object. Leaving it set leaks a handle on every click.

Copy-paste practice code

WinCC_Button_WithArguments.vbs
Sub OnClick(ByVal Item)
    Dim shell, exePath, startDate, endDate, command

    exePath = "C:\Softwell_Report\Softwell_Report.exe"

    ' Last 7 days up to today
    startDate = FormatDateTime(Date - 7, 2)
    endDate = FormatDateTime(Date, 2)

    command = """" & exePath & """ " & startDate & " " & endDate

    Set shell = CreateObject("WScript.Shell")
    shell.Run command, 1, False
    Set shell = Nothing
End Sub

The simple version in Section 9 opens the popup. This version passes a date range, so the report is produced with no operator input at all. Use whichever the plant asks for.

Expected result

console output
Runtime click -> Softwell_Report.exe launched
Report window appears, WinCC Runtime stays responsive
Workbook created in C:\Softwell_Report\SQF_Reports

Checkpoint

Pass when: One click launches the EXE, WinCC Runtime stays responsive during generation, and a workbook appears in the report folder.

If it fails

SymptomWhat to check
Nothing happens on clickThe path is wrong or unquoted. Test the exact command in a Run dialog first.
WinCC Runtime freezes until the report finishesThe third argument is True. Set it to False so the call does not wait.
Windows Script Host error 800A0046The Runtime account cannot read the EXE folder. Fix the folder permissions.
Two reports appear from one clickThe action is attached to both press and release events. Attach it to one only.
It works in the designer but not in RuntimeRuntime executes under a different account. Test with that account logged in.

10. Deploy on the WinCC Runtime Station

A practical local deployment layout is:

deployment folder
C:\Softwell_Report\
├── Softwell_Report.exe
└── SQF_Reports\
  • Copy the signed/approved EXE to a stable local folder.
  • Install the required SQL Server ODBC driver separately.
  • Allow the EXE through endpoint-security application controls.
  • Grant the WinCC operator/runtime identity read access to SQL and write access to the report folder.
  • Use a fixed local path in the WinCC action; avoid user-profile and mapped-drive dependencies.
  • Record application version, checksum, build environment and rollback copy.
  • Commission the complete workflow while WinCC Runtime is active.

PyInstaller bundles Python code and libraries, but it does not provision SQL Server, install the Microsoft ODBC driver, grant database rights, install Excel, or configure WinCC permissions. Excel itself is optional for generating the XLSX; it is needed only if operators want to open the report in Microsoft Excel.

Lab Manual 10 — Deploy on the WinCC Runtime Station

8 min

Objective

Place the EXE and its report folder where the Runtime account has the rights it needs, and confirm the deployment survives a station restart.

Procedure

  1. Create the deployment folder on the station's local drive.
  2. Copy in the EXE and create the report subfolder.
  3. Grant the Runtime account modify rights on the report folder only.
  4. Restart the station and click the button again.

What each line does

CodeMeaning in this lab
C:\Softwell_Report\A local folder. Avoid a network share for the first deployment — a dropped share turns a report failure into a station problem.
Softwell_Report.exeThe single compiled file. Nothing else from the build output is needed.
SQF_Reports\Created automatically by mkdir(parents=True, exist_ok=True), but pre-creating it lets you set permissions in advance.
Local drive, not Program FilesWindows protects Program Files, so a normal account cannot write reports there.
Least privilegeThe Runtime account needs read and execute on the EXE, and modify on the report folder. Nothing more.

Copy-paste practice code

07_deployment_check.cmd
cd C:\Softwell_Report

REM What is deployed
dir /b

REM Effective permissions on the report folder
icacls SQF_Reports

REM Prove the Runtime account can write there
echo test > SQF_Reports\_writetest.txt
del SQF_Reports\_writetest.txt
echo Write test passed.

REM How many reports exist and how much space they use
dir SQF_Reports\*.xlsx | find "File(s)"

Run this while logged in as the WinCC Runtime account, not as an administrator. An administrator can always write, which proves nothing about the operator.

Expected result

console output
Softwell_Report.exe
SQF_Reports

SQF_Reports NT AUTHORITY\SYSTEM:(OI)(CI)(F)
            BUILTIN\Administrators:(OI)(CI)(F)
            SOFTWELL\WinCCRuntime:(OI)(CI)(M)

Write test passed.
              14 File(s)        487,424 bytes

Checkpoint

Pass when: The folder listing shows only the EXE and the report folder, the Runtime account holds modify rights, and the write test passes under that account.

If it fails

SymptomWhat to check
Access denied creating the reportThe Runtime account lacks modify rights on the report folder. Grant them with icacls or the folder properties dialog.
Reports stop appearing after a rebootThe deployment sits on a mapped drive that is not reconnected at logon. Use a local path or a UNC path.
The disk fills up over monthsNothing deletes old reports. Add a scheduled cleanup that removes workbooks older than an agreed retention period.
The EXE is blocked after copyingWindows marked the file as downloaded. Right-click, Properties, Unblock — or copy it with a method that does not add the zone marker.

11. Complete Corrected Python Code

softwell_report.py
r"""
================================================================================
CODE 05: WINCC / WINDOWS FILTER POPUP AND EXCEL REPORT GENERATOR
================================================================================

THEORY
------
An operator selects a start date/time, end date/time, and optional furnace.
Python uses parameterized SQL to read the matching date range, applies the exact
time filter, and exports the result to a professionally formatted Excel workbook.

STEPS
-----
1. Run the Python file or double-click the compiled EXE.
2. Enter start and end date/time in the popup.
3. Select All Furnaces or type a furnace number.
4. Select the report output folder.
5. Click Generate Excel Report.
6. The program confirms the record count and generated workbook path.

EXPLANATION
-----------
GUI mode uses only Python's built-in tkinter library. pyodbc reads SQL Server
without a pandas/SQLAlchemy warning. pandas prepares timestamps, and openpyxl
formats the Excel report. Command-line mode remains available for WinCC.
================================================================================
"""

import argparse
import os
import sys
import tkinter as tk
from datetime import datetime, time, timedelta
from pathlib import Path
from tkinter import filedialog, messagebox, ttk

import pandas as pd
import pyodbc
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter

SQL_SERVER = r"SOFTWELL\WINCC"
DATABASE = "SQF_DB"
DRIVER = "{ODBC Driver 17 for SQL Server}"


def application_folder():
    """Return the EXE folder when compiled, otherwise the Python-file folder."""
    if getattr(sys, "frozen", False):
        return Path(sys.executable).resolve().parent
    return Path(__file__).resolve().parent


DEFAULT_OUTPUT_FOLDER = application_folder() / "SQF_Reports"


def parse_datetime(value, end_of_day=False):
    """Accept YYYY-MM-DD or YYYY-MM-DD HH:MM[:SS]."""
    try:
        parsed = datetime.fromisoformat(value.strip())
    except ValueError as error:
        raise ValueError(
            "Use YYYY-MM-DD HH:MM:SS, for example 2026-07-27 14:30:00"
        ) from error

    if len(value.strip()) == 10:
        parsed = datetime.combine(
            parsed.date(),
            time(23, 59, 59) if end_of_day else time.min,
        )

    return parsed


def create_connection():
    """Create a Windows-authenticated SQL Server connection."""
    return pyodbc.connect(
        f"Driver={DRIVER};Server={SQL_SERVER};Database={DATABASE};"
        "Trusted_Connection=yes;TrustServerCertificate=yes;"
    )


def get_filtered_data(connection, start_datetime, end_datetime, sqf_no=None):
    """Read the date range, then apply the exact time range."""
    query = """
        SELECT
            [DT], [TM], [SQF_No], [ChargeNo],
            [Event_From], [Event_To],
            [Temp_Set], [Temp_Act],
            [Cp_Set], [Cp_Act],
            [Oil_Set], [Oil_Act],
            [Jacket_Set], [Jacket_Act],
            [Fan_Status]
        FROM [dbo].[tblEvent]
        WHERE [DT] >= ? AND [DT] < ?
    """

    params = [start_datetime.date(), end_datetime.date() + timedelta(days=1)]

    if sqf_no is not None:
        query += " AND [SQF_No] = ?"
        params.append(sqf_no)

    query += " ORDER BY [DT] DESC, [TM] DESC;"

    cursor = connection.cursor()
    cursor.execute(query, params)

    columns = [column[0] for column in cursor.description]
    dataframe = pd.DataFrame.from_records(cursor.fetchall(), columns=columns)

    if dataframe.empty:
        return dataframe

    date_values = pd.to_datetime(dataframe["DT"], errors="coerce")
    time_values = pd.to_timedelta(dataframe["TM"].astype(str), errors="coerce")
    combined = date_values.dt.normalize() + time_values

    dataframe.insert(
        0,
        "Event_Timestamp",
        combined.where(combined.notna(), date_values),
    )

    return dataframe[
        dataframe["Event_Timestamp"].between(start_datetime, end_datetime)
    ].reset_index(drop=True)


def export_to_excel(dataframe, output_folder, start_datetime, end_datetime, sqf_no):
    """Create a timestamped, formatted Excel report."""
    output_folder.mkdir(parents=True, exist_ok=True)

    stamp = datetime.now().strftime("%d-%m-%Y_%H-%M-%S")
    furnace_text = "All" if sqf_no is None else f"SQF-{sqf_no}"
    output_file = output_folder / f"SQF_Filtered_Report_{furnace_text}_{stamp}.xlsx"

    with pd.ExcelWriter(output_file, engine="openpyxl") as writer:
        dataframe.to_excel(
            writer,
            sheet_name="Filtered Report",
            index=False,
            startrow=4,
        )

        worksheet = writer.sheets["Filtered Report"]

        # Report title
        worksheet["A1"] = "SQF FURNACE EVENT REPORT"
        worksheet["A1"].font = Font(bold=True, size=16, color="FFFFFF")
        worksheet["A1"].fill = PatternFill("solid", fgColor="1F4E78")
        worksheet.merge_cells(
            start_row=1,
            start_column=1,
            end_row=1,
            end_column=max(1, len(dataframe.columns)),
        )

        # Filter summary
        worksheet["A2"] = (
            f"Period: {start_datetime:%Y-%m-%d %H:%M:%S} to "
            f"{end_datetime:%Y-%m-%d %H:%M:%S}"
        )
        worksheet["A3"] = f"Furnace: {furnace_text} | Records: {len(dataframe)}"

        # Data header row
        header_row = 5
        header_fill = PatternFill("solid", fgColor="F4B183")

        for cell in worksheet[header_row]:
            cell.font = Font(bold=True)
            cell.fill = header_fill
            cell.alignment = Alignment(horizontal="center")

        worksheet.freeze_panes = "A6"
        worksheet.auto_filter.ref = worksheet.dimensions

        # Row 1 contains merged title cells. Use the numeric column position
        # instead of column_cells[0].column_letter, because merged cells do not
        # provide the column_letter attribute.
        for column_number, column_cells in enumerate(worksheet.columns, start=1):
            letter = get_column_letter(column_number)
            width = max(len(str(cell.value or "")) for cell in column_cells) + 3
            worksheet.column_dimensions[letter].width = min(width, 35)

        for row in worksheet.iter_rows(min_row=6):
            if isinstance(row[0].value, datetime):
                row[0].number_format = "DD-MM-YYYY HH:MM:SS"

    return output_file


def generate_report(start_text, end_text, furnace_text, output_text):
    """Validate filters, query SQL Server, and generate the workbook."""
    start_datetime = parse_datetime(start_text)
    end_datetime = parse_datetime(end_text, end_of_day=True)

    if end_datetime < start_datetime:
        raise ValueError("End date/time must be after start date/time.")

    furnace_value = furnace_text.strip()
    sqf_no = None if furnace_value in {"", "All Furnaces"} else int(furnace_value)

    output_folder = Path(output_text).expanduser().resolve()

    with create_connection() as connection:
        dataframe = get_filtered_data(
            connection,
            start_datetime,
            end_datetime,
            sqf_no,
        )

    output_file = export_to_excel(
        dataframe,
        output_folder,
        start_datetime,
        end_datetime,
        sqf_no,
    )

    return output_file, len(dataframe)


def run_popup():
    """Open the operator filter popup."""
    root = tk.Tk()
    root.title("SQF Filtered Excel Report")
    root.geometry("650x370")
    root.resizable(False, False)

    frame = ttk.Frame(root, padding=22)
    frame.pack(fill="both", expand=True)

    ttk.Label(
        frame,
        text="SQF Furnace Report Filter",
        font=("Segoe UI", 16, "bold"),
    ).grid(row=0, column=0, columnspan=3, pady=(0, 20))

    now = datetime.now()
    start_var = tk.StringVar(
        value=(now - timedelta(days=7)).strftime("%Y-%m-%d 00:00:00")
    )
    end_var = tk.StringVar(value=now.strftime("%Y-%m-%d 23:59:59"))
    furnace_var = tk.StringVar(value="All Furnaces")
    output_var = tk.StringVar(value=str(DEFAULT_OUTPUT_FOLDER))
    status_var = tk.StringVar(value="Select filters and click Generate Excel Report.")

    labels = [
        ("Start date/time", start_var),
        ("End date/time", end_var),
    ]

    for row_number, (label, variable) in enumerate(labels, start=1):
        ttk.Label(frame, text=label).grid(row=row_number, column=0, sticky="w", pady=7)
        ttk.Entry(frame, textvariable=variable, width=36).grid(
            row=row_number,
            column=1,
            columnspan=2,
            sticky="ew",
            pady=7,
        )

    ttk.Label(frame, text="Furnace").grid(row=3, column=0, sticky="w", pady=7)
    furnace_box = ttk.Combobox(
        frame,
        textvariable=furnace_var,
        values=["All Furnaces", "1", "2", "3"],
        width=33,
    )
    furnace_box.grid(row=3, column=1, columnspan=2, sticky="ew", pady=7)

    ttk.Label(frame, text="Output folder").grid(row=4, column=0, sticky="w", pady=7)
    ttk.Entry(frame, textvariable=output_var, width=36).grid(
        row=4,
        column=1,
        sticky="ew",
        pady=7,
    )

    def browse_folder():
        selected = filedialog.askdirectory(initialdir=output_var.get())
        if selected:
            output_var.set(selected)

    ttk.Button(frame, text="Browse...", command=browse_folder).grid(
        row=4,
        column=2,
        padx=(8, 0),
        pady=7,
    )

    def on_generate():
        try:
            status_var.set("Generating report, please wait...")
            root.update_idletasks()

            output_file, record_count = generate_report(
                start_var.get(),
                end_var.get(),
                furnace_var.get(),
                output_var.get(),
            )

            status_var.set(f"Completed: {record_count} records")
            messagebox.showinfo(
                "Report Generated",
                f"Report generated successfully.\n\n"
                f"Records: {record_count}\nFile:\n{output_file}",
            )

            if messagebox.askyesno(
                "Open Report",
                "Do you want to open the Excel report now?",
            ):
                os.startfile(output_file)

        except Exception as error:
            status_var.set("Report generation failed.")
            messagebox.showerror("Report Error", str(error))

    ttk.Button(
        frame,
        text="Generate Excel Report",
        command=on_generate,
    ).grid(row=5, column=0, columnspan=3, pady=(20, 10), ipadx=30, ipady=5)

    ttk.Label(frame, textvariable=status_var, foreground="#1F4E78").grid(
        row=6,
        column=0,
        columnspan=3,
    )

    frame.columnconfigure(1, weight=1)
    root.mainloop()


def run_command_line():
    """Generate a report from WinCC or a command prompt."""
    parser = argparse.ArgumentParser(description="Generate an SQF Excel report.")
    parser.add_argument("start", help='YYYY-MM-DD or "YYYY-MM-DD HH:MM:SS"')
    parser.add_argument("end", help='YYYY-MM-DD or "YYYY-MM-DD HH:MM:SS"')
    parser.add_argument("sqf_no", nargs="?", help="Optional furnace number")
    parser.add_argument("--output", default=str(DEFAULT_OUTPUT_FOLDER))
    args = parser.parse_args()

    output_file, count = generate_report(
        args.start,
        args.end,
        args.sqf_no or "All Furnaces",
        args.output,
    )

    print(f"Exported {count} records to:\n{output_file}")


def main():
    if len(sys.argv) == 1:
        run_popup()
    else:
        run_command_line()


if __name__ == "__main__":
    main()

Lab Manual 11 — Run the Complete Program End to End

12 min

Objective

Run the finished program from source in both modes before compiling, so any failure is a Python problem rather than a PyInstaller one.

Procedure

  1. Create the project folder and a virtual environment.
  2. Install pyodbc, pandas, openpyxl and pyinstaller.
  3. Save the Section 11 listing as softwell_report.py.
  4. Edit only three constants: SQL_SERVER, DATABASE and DRIVER.
  5. Run the popup mode, then the command-line mode, then build the EXE.

What each line does

CodeMeaning in this lab
python -m venv .venvAn isolated environment. PyInstaller bundles whatever this environment holds, so keeping it minimal keeps the EXE smaller.
pip install pyodbc pandas openpyxl pyinstallerAll four. tkinter and argparse ship with Python and need no install.
SQL_SERVER / DATABASE / DRIVERThe only three values that change between sites. Everything else is derived at runtime.
DEFAULT_OUTPUT_FOLDERDerived from application_folder(), so it follows the EXE rather than the working directory.
Source first, then EXEDebugging a traceback in the console is far easier than debugging a silent --windowed EXE.

Copy-paste practice code

setup_and_run.cmd
cd C:\SoftwellLabs\wincc-report
python -m venv .venv
.venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install pyodbc pandas openpyxl pyinstaller

REM 1. Popup mode
python softwell_report.py

REM 2. Command-line mode
python softwell_report.py 2026-07-20 2026-07-27

REM 3. Build only after both modes pass
python -m PyInstaller --clean --noconfirm --onefile --windowed --name Softwell_Report softwell_report.py

Do not skip steps 1 and 2. Almost every reported “the EXE does not work” turns out to be a script that never worked from source either.

Expected result

console output
Exported 1284 records to:
C:\SoftwellLabs\wincc-report\SQF_Reports\SQF_Filtered_Report_All_19-09-2026_18-11-02.xlsx

21234 INFO: Building EXE from EXE-00.toc completed successfully.

Checkpoint

Pass when: Popup mode generates a workbook, command-line mode prints the record count and path, and the build reports success — in that order.

If it fails

SymptomWhat to check
ModuleNotFoundError: No module named 'pandas'The environment is not active, or pip installed into a different Python. Run where python and confirm it points inside .venv.
The popup opens but Generate does nothingAn exception is being shown in the message box. Read it — it is usually the SQL connection or the date format.
Exported 0 recordsThe date range holds no data, or the time filter removed everything. Lab 5 isolates which.
The EXE behaves differently from the scriptAlmost always a path assumption. Build every path from application_folder().
tkinter fails to importPython was installed without tcl/tk. Reinstall with that option enabled.

12. Troubleshooting and Production Improvements

SymptomLikely causeCheck / correction
Button does nothingWrong path, blocked script or action not attachedTest absolute path and WinCC action diagnostics
EXE works manually but not from WinCCDifferent user/context or desktop permissionsTest as Runtime identity; inspect SQL/folder rights
Popup opens twiceDuplicate entry point or double launchUse corrected source and add single-instance protection
ODBC driver not foundDriver missing/architecture mismatchInstall and verify the required Microsoft driver
Login failedRuntime Windows account lacks SQF_DB accessGrant approved read-only rights to that identity
No records foundRange/furnace excludes data or DT/TM parsing failedVerify source values and filter boundaries
Excel PermissionErrorFolder denied, file locked or security softwareVerify local folder rights and close locked files
EXE startup is slowOne-file extraction and large pandas bundleConsider --onedir after controlled testing
Antivirus quarantines EXEUnsigned or unapproved generated binaryUse organization signing, allowlisting and change control

Recommended production upgrades

  • Add a single-instance lock so one operator cannot open several report windows.
  • Run database/export work in a worker thread so the GUI remains responsive.
  • Use a single indexed datetime2 column and exact SQL range filtering.
  • Load server/database/output configuration from a controlled config file.
  • Add application logging with timestamps, filters, row count, duration and errors.
  • Validate furnace selection against an allowlist.
  • Write to a temporary XLSX and rename after validation.
  • Digitally sign the final EXE and document the checksum/version.

Commissioning Checklist: WinCC Button to Excel Report

Site test
Before you start
  • Use an approved test/commissioning WinCC project and SQL dataset.
  • Keep a backup of the graphics picture and action.
  • Confirm the deployed EXE checksum and rollback path.
  • Estimated time: 35 minutes.
1

Prove standalone operation

Run the EXE locally as the WinCC Runtime user and generate a known short-period report.

The popup, SQL query, output folder and workbook all pass independently.
2

Configure and test the button

Add the Softwell_Report button and VBScript action, activate Runtime and click once.

Exactly one visible popup opens and WinCC remains responsive.
3

Verify filters and report data

Test all furnaces, one furnace, date-only input and exact start/end timestamps. Reconcile results with an approved SQL query.

Counts and boundary timestamps match the defined filter behavior.
4

Test failure and recovery

Test no-data, SQL unavailable, invalid filter and output-permission cases; then restore normal conditions.

Operators receive clear errors, WinCC remains stable and the next valid run succeeds.

Related Python and SQL Server Tutorials

Continue through the Softwell Python–SQL Server learning path:

Frequently asked questions

How does the WinCC button open the EXE?

A Graphics Designer VBScript click action creates WScript.Shell and runs the fixed absolute path to Softwell_Report.exe without waiting for it to finish.

Does the WinCC computer need Python installed?

Not for a properly built standalone PyInstaller EXE. The target still needs the required ODBC driver, SQL/network access, filesystem permissions and endpoint-security approval.

How are date and time filters applied?

The popup validates the inputs, SQL Server returns the broad date range using bound parameters, and pandas combines DT and TM into Event_Timestamp for the exact range filter.

Why does the EXE crash with an error about column_letter?

The column-width loop read column_cells[0].column_letter, but row 1 holds merged title cells and a MergedCell has no such attribute. Use get_column_letter(column_number) from the enumerate index instead.

Should I build with --onefile or as a folder?

--onefile is easier to deploy but unpacks to a temp folder on every start, so it is slower and more likely to be flagged by antivirus. A folder build starts faster and is usually the better choice on a Runtime station.

Why do reports appear in a Temp folder instead of next to the EXE?

A relative path was used. Inside a onefile EXE the working directory is the PyInstaller extraction folder, so every path must be built from application_folder().

Does WinCC freeze while the report is generated?

Not if the third argument of shell.Run is False. That tells Windows not to wait, so Runtime stays responsive.

Can the button pass a date range instead of opening the popup?

Yes. The program runs headless whenever arguments are supplied, so the VBScript can build a command line such as Softwell_Report.exe 2026-07-20 2026-07-27 2.

The EXE works for me but not for the operator. Why?

WinCC Runtime executes under a different Windows account. That account needs read and execute on the EXE, modify on the report folder, and its own SQL Server login with SELECT permission.

Reviewed by Bhawesh Kumar SinghIndustrial Automation Trainer and Industry 4.0 Consultant · Softwell Automation · 21+ years industry experience

Get the WinCC + Python reporting syllabus

Share your details—a Softwell advisor will contact you with batch dates, fees and project-practice options.

No spam. Used only to share course details for this enquiry.

Build one-button WinCC Excel reporting

Join live online, Pune classroom or corporate Industry 4.0 training.

Request Course Details
Complete Python for Industrial Automation Learning Path

Use Previous / Next for sequential training, or open any topic directly.

Verified learning pathway

Discuss WinCC Python Reporting Training

Explore practical curriculum, software, hardware and batch options for this technology.

Content reviewed: 2 August 2026

☎ Call WhatsApp ✉ Email Enquire Now