pinnedBottomRowData and Print Modal aren't working alongside eachother

Hello there,

I’ve made some decent progress on this, but am hitting a wall with my update_print_grid_content callback, as well as that print modal. If everything were to work perfectly, these would be the ideal steps:

  1. Page is loaded
  2. Query is ran
  3. Totals row updates with currently shown totals of the four columns
  4. User filters the data they need (for a specific product or location)
  5. User hits “Print Report”
  6. The print modal pops up
  7. The print modal shows only what they’ve filtered + the Total row in a printer friendly format
  8. They hit the “Print” button
  9. Their browser printing prompt opens and they print their filtered data/total
  10. Off to the warehouse with the sheet they need to mark off

Step 7 is where I’ve been getting hung up. Have been going over this documentation for the past couple days: Printing | Dash for Python Documentation | Plotly but the modal isn’t showing the total row with the proper calculation. At some point the update_print_grid_content was showing the rows filtered, just not the correct total row calculation, but that’s not working now either.

Is the update_print_grid_content the culprit here? And if so, what am I missing? Relatively new to all this so thanks in advance for any advice!

import os
import pyodbc
import dash_html_components as html
import dash_ag_grid as dag
import pandas as pd
from dash.dependencies import Input, Output, State
from django_plotly_dash import DjangoDash
import dash_core_components as dcc
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc

app = DjangoDash('finished_goods_inventory', add_bootstrap_links=True)

def get_data():
    conn = pyodbc.connect('DRIVER={ODBC Driver 17 for SQL Server};'
                        f'SERVER={os.getenv("INNOVA_SERVER")};'
                        'DATABASE=ClearView;'
                        f'UID={os.getenv("CLEARVIEW_SQL_USER")};'
                        f'PWD={os.getenv("CLEARVIEW_SQL_PASSWORD")}')

    query = "EXEC sp_in_selectFinishedInventory"
    df = pd.read_sql(query, conn)
    conn.close()

    df['inventoryLocation'] = df['inventoryLocation'].str.replace('--', '-')
    df['inventoryLocation'] = df['inventoryLocation'].str.replace('Henningsen TF', 'Cedar Grove')

    df['packCount'] = df['packCount'].astype(int)
    df['packPounds'] = df['packPounds'].astype(int)

    df = df.sort_values(by='receivedDate')
    return df

def calculate_totals(data):
    sum_values = data[["packCount", "packPounds"]].sum()
    return {"inventoryLocation": "Total",
            "packCount": sum_values["packCount"],
            "packPounds": sum_values["packPounds"]}

columnDefs = [
    {"headerName": "Pallet", "field": "palletNumber", "rowGroup": True, "filter": True, "suppressColumnsToolPanel": True},
    {"headerName": "Item #", "field": "materialName", "rowGroup": True, "filter": True},
    {"field": "inventoryLocation", "sortable": True, "filter": True},
    {"headerName": "Total Cases", "field": "packCount", "sortable": True, "filter": True, "aggFunc": "sum"},
    {"headerName": "Total Pounds", "field": "packPounds", "sortable": True, "filter": True, "aggFunc": "sum"},
    {"field": "lotName", "sortable": True, "filter": True},
    {"field": "receivedDate", "sortable": True, "filter": True},
]

print_grid = html.Div(
    [
        html.Hr(),
        html.H1('sadfasdf'),
        dcc.Store(id="fgi-data-store", storage_type="memory", data={"data": None, "filter_model": None}),
        dag.AgGrid(
            id="grid-modal-print",
            defaultColDef=dict(
                resizable=True,
                sortable=True,
                filter=True
            ),
            columnDefs=columnDefs,
            enableEnterpriseModules=True,
            dashGridOptions={
                'pinnedBottomRowData': [calculate_totals(get_data())],
                'groupDefaultExpanded': 2, 
                "domLayout": "print"
            },
            style={"height": 'calc(100vh - 210px)', 'margin-bottom': '10px'},
        )
    ]
)

print_layout = html.Div(
    [
        dbc.ModalHeader(dbc.Button("Print", id="grid-browser-print-btn")),
        dbc.ModalBody(
            [
                print_grid,
            ],
            id="grid-print-area",
        ),
        html.Div(id="dummy"),
    ]
)

print_grid = html.Div(
    [
        html.Hr(),
        dcc.Store(id="fgi-data-store", storage_type="memory", data={"data": None, "filter_model": None}),
        html.Div(id="print-grid-content"),  # Placeholder for print grid content
    ]
)

app.layout = html.Div(
    [
        html.H1("Finished Goods Inventory"),
        dbc.Button("Print Report", id="grid-modal-print-btn"),
        dbc.Modal(print_layout, id="modal-print-container", is_open=False, size='xl'),
        html.Hr(),
        dcc.Store(id="fgi-data-store", storage_type="memory", data={"data": None, "filter_model": None}),
        dag.AgGrid(
            id="fgi-grid",
            defaultColDef=dict(
                resizable=True,
                sortable=True,
                filter=True
            ),
            columnDefs=columnDefs,
            csvExportParams={"fileName": "finished_goods_inventory.csv"},
            enableEnterpriseModules=True,
            dashGridOptions={
                'pinnedBottomRowData': [calculate_totals(get_data())],
                'groupDefaultExpanded': 2,  
            },
            style={"height": 'calc(100vh - 210px)', 'margin-bottom': '10px'},
        
        )
    ]
)

@app.callback(
    [Output("fgi-data-store", "data"),
     Output('fgi-grid', 'pinnedBottomRowData')],
    [Input("fgi-grid", "filterModel")]
)
def update_data_store(filter_model):
    if filter_model is None:
        raise PreventUpdate

    if filter_model == {}:
        data = get_data().to_dict("records")
    else:
        data = None

    data_store = {"data": data, "filter_model": filter_model}
    
    filtered_data = pd.DataFrame(data) if data else get_data()
    for column, filter_options in filter_model.items():
        values = filter_options.get('values', [])
        if values:
            filtered_data = filtered_data[filtered_data[column].isin(values)]

    totals = calculate_totals(filtered_data)
    return data_store, [totals]

@app.callback(
    Output('fgi-grid', 'rowData'),
    [Input("fgi-data-store", "data")]
)
def update_output(data_store):
    if data_store["data"] is None:
        raise PreventUpdate

    filtered_data = pd.DataFrame(data_store["data"])
    filter_model = data_store["filter_model"]

    if filter_model:
        for column, filter_options in filter_model.items():
            values = filter_options.get('values', [])
            if values:
                filtered_data = filtered_data[filtered_data[column].isin(values)]

    return filtered_data.to_dict("records")


@app.callback(
    Output("grid-modal-print", "columnState"),
    Output("grid-modal-print", "filterModel"),
    Output("modal-print-container", "is_open"),
    Input("grid-modal-print-btn", "n_clicks"),
    State("fgi-grid", "columnState"),
    State("fgi-grid", "filterModel"),
    prevent_initial_call=True,
)
def open_print_modal(_, col_state, filter_model):
    return col_state, filter_model, True


@app.callback(
    #Output('fgi-data-store', 'data'),
    Output("grid-modal-print", "rowData"),
    Output('grid-modal-print', 'pinnedBottomRowData'),
    Input("grid-modal-print-btn", "n_clicks"),
    State("fgi-data-store", "data"),
    prevent_initial_call=True,
)
def update_print_grid_content(n_clicks, data_store):
    if n_clicks is None:
        raise PreventUpdate

    if data_store is None or data_store["data"] is None:
        raise PreventUpdate

    filtered_data = pd.DataFrame(data_store["data"])
    totals = [calculate_totals(filtered_data)]

    return data_store["data"] + totals

if __name__ == '__main__':
    app.run_server(debug=True)

Hi @jimbob

print_grid is defined twice - that may be why the filtered rows aren’t showing correctly.

Then try combining the callbacks where the print button is used as an Input.

1 Like

@AnnMarieW

I was a goober defining print_grid twice and you are a genius with the suggestion to combine the print button callbacks. Below is what I’m working with now. This works now, to a point. The table loads, I’m able to filter data, and the totals row updates depending on that filtering.

However, when I hit that Print Report button- one of two things will happen.

-If no filters are applied, the modal loads with the entirety of the table loaded in that printable format.
-If any filter has been applied, the print modal shows no data at all.

My thought was to add Output(‘grid-modal-print’, ‘pinnedBottomRowData’) and Output(‘gird-modal-print’, ‘rowData’) to the update_data_store and update_output callbacks, as that should do the same updates to the print modal table as the base table right? Unfortunately, when I add those lines to those two callbacks, the comment out those same lines in the handle_print_button callback- that print modal table shows nothing, and the original table breaks, also showing no data.

What am I doing wrong as far as updating the print modal table with the filtered data?

Really appreciate your insight and hope your holidays are great!

import os
import pyodbc
import dash_html_components as html
import dash_ag_grid as dag
import pandas as pd
from dash.dependencies import Input, Output, State
from django_plotly_dash import DjangoDash
import dash_core_components as dcc
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc

app = DjangoDash('finished_goods_inventory', add_bootstrap_links=True)

def get_data():
    conn = pyodbc.connect('DRIVER={ODBC Driver 17 for SQL Server};'
                        f'SERVER={os.getenv("INNOVA_SERVER")};'
                        'DATABASE=ClearView;'
                        f'UID={os.getenv("CLEARVIEW_SQL_USER")};'
                        f'PWD={os.getenv("CLEARVIEW_SQL_PASSWORD")}')

    query = "EXEC sp_in_selectFinishedInventory"
    df = pd.read_sql(query, conn)
    conn.close()

    df['inventoryLocation'] = df['inventoryLocation'].str.replace('--', '-')
    df['inventoryLocation'] = df['inventoryLocation'].str.replace('Henningsen TF', 'Cedar Grove')

    df['packCount'] = df['packCount'].astype(int)
    df['packPounds'] = df['packPounds'].astype(int)

    df = df.sort_values(by='receivedDate')
    return df

def calculate_totals(data):
    sum_values = data[["packCount", "packPounds"]].sum()
    return {"inventoryLocation": "Total",
            "packCount": sum_values["packCount"],
            "packPounds": sum_values["packPounds"]}

columnDefs = [
    {"headerName": "Pallet", "field": "palletNumber", "rowGroup": True, "filter": True, "suppressColumnsToolPanel": True},
    {"headerName": "Item #", "field": "materialName", "rowGroup": True, "filter": True},
    {"field": "inventoryLocation", "sortable": True, "filter": True},
    {"headerName": "Total Cases", "field": "packCount", "sortable": True, "filter": True, "aggFunc": "sum"},
    {"headerName": "Total Pounds", "field": "packPounds", "sortable": True, "filter": True, "aggFunc": "sum"},
    {"field": "lotName", "sortable": True, "filter": True},
    {"field": "receivedDate", "sortable": True, "filter": True},
]

print_grid = html.Div(
    [
        html.Hr(),
        html.H1('sadfasdf'),
        dcc.Store(id="fgi-data-store", storage_type="memory", data={"data": None, "filter_model": None}),
        dag.AgGrid(
            id="grid-modal-print",
            defaultColDef=dict(
                resizable=True,
                sortable=True,
                filter=True
            ),
            columnDefs=columnDefs,
            enableEnterpriseModules=True,
            dashGridOptions={
                'pinnedBottomRowData': [calculate_totals(get_data())],
                'groupDefaultExpanded': 2, 
                "domLayout": "print"
            },
            style={"height": 'calc(100vh - 210px)', 'margin-bottom': '10px'},
        )
    ]
)


print_layout = html.Div(
    [
        dbc.ModalHeader(dbc.Button("Print", id="grid-browser-print-btn")),
        dbc.ModalBody(
            [
                print_grid,
            ],
            id="grid-print-area",
        ),
        html.Div(id="dummy"),
    ]
)


app.layout = html.Div(
    [
        html.H1("Finished Goods Inventory"),
        dbc.Button("Print Report", id="grid-modal-print-btn"),
        dbc.Modal(print_layout, id="modal-print-container", is_open=False, size='xl'),
        html.Hr(),
        dcc.Store(id="fgi-data-store", storage_type="memory", data={"data": None, "filter_model": None}),
        dag.AgGrid(
            id="fgi-grid",
            defaultColDef=dict(
                resizable=True,
                sortable=True,
                filter=True
            ),
            columnDefs=columnDefs,
            csvExportParams={"fileName": "finished_goods_inventory.csv"},
            enableEnterpriseModules=True,
            dashGridOptions={
                'pinnedBottomRowData': [calculate_totals(get_data())],
                'groupDefaultExpanded': 2,  
            },
            style={"height": 'calc(100vh - 210px)', 'margin-bottom': '10px'},
        
        )
    ]
)

@app.callback(
    [Output("fgi-data-store", "data"),
     Output('fgi-grid', 'pinnedBottomRowData')],
     #Output('grid-modal-print', 'pinnedBottomRowData')],
    [Input("fgi-grid", "filterModel")]
)
def update_data_store(filter_model):
    if filter_model is None:
        raise PreventUpdate

    if filter_model == {}:
        data = get_data().to_dict("records")
    else:
        data = None

    data_store = {"data": data, "filter_model": filter_model}
    
    filtered_data = pd.DataFrame(data) if data else get_data()
    for column, filter_options in filter_model.items():
        values = filter_options.get('values', [])
        if values:
            filtered_data = filtered_data[filtered_data[column].isin(values)]

    totals = calculate_totals(filtered_data)
    return data_store, [totals]

@app.callback(
    Output('fgi-grid', 'rowData'),
    #Output('gird-modal-print', 'rowData'),
    [Input("fgi-data-store", "data")]
)
def update_output(data_store):
    if data_store["data"] is None:
        raise PreventUpdate

    filtered_data = pd.DataFrame(data_store["data"])
    filter_model = data_store["filter_model"]

    if filter_model:
        for column, filter_options in filter_model.items():
            values = filter_options.get('values', [])
            if values:
                filtered_data = filtered_data[filtered_data[column].isin(values)]

    return filtered_data.to_dict("records")

@app.callback(
    [Output("grid-modal-print", "columnState"),
     Output("grid-modal-print", "filterModel"),
     Output("modal-print-container", "is_open"),
     Output("grid-modal-print", "rowData"),
     Output('grid-modal-print', 'pinnedBottomRowData')],
    [Input("grid-modal-print-btn", "n_clicks"),
     Input("fgi-grid", "columnState"),
     Input("fgi-grid", "filterModel")],
    [State("fgi-data-store", "data")],
    prevent_initial_call=True,
)
def handle_print_button(n_clicks, col_state, filter_model, data_store):
    if n_clicks is None:
        raise PreventUpdate

    if data_store is not None and data_store["data"] is not None:
        filtered_data = pd.DataFrame(data_store["data"])
        totals = [calculate_totals(filtered_data)]
        row_data = data_store["data"] + totals
    else:
        row_data = None
        totals = None

    return col_state, filter_model, True, row_data, totals

if __name__ == '__main__':
    app.run_server(debug=True)

Hi @jimbob

Thanks for your kind words, but it’s hard to see exactly what’s going on without a minimal example. Can you make one with some toy data set rather than the database calls? Also, I’ve never used DjanoDash - can you reproduce the issue with regular Dash?

Hi @AnnMarieW

Showing my inepdtitude again here, but when I told this to hit a .csv, or an .xlsx, that I had saved into my project folder- Dash was throwing errors that it couldn’t find that file, even though the name was right and path was right of that file.

Also tried to just have this be app = Dash, instead of DjangoDash, but that also prevented to report from working at all. I do have some sample data below though!

Column 1 Column 2 Column 3 Column 4 E F G H I J
materialId materialName inventoryName inventoryLocation palletNumber packCount packPounds pieceCount lotName receivedDate
8000 08000:FZ SMK TROUT 3.5OZ UP10# CP-Finished Goods Inventory Holding Freezer N-18 212844 9 90 0 OIQF-196 2 7/15/2022
8000 08000:FZ SMK TROUT 3.5OZ UP10# Henningsen’s Twin Falls Henningsen TF 223963 80 800 0 OIQF-307 2 11/3/2022
8000 08000:FZ SMK TROUT 3.5OZ UP10# Henningsen’s Twin Falls Henningsen TF 217066 72 720 0 OIQF-242 2 8/30/2022
8000 08000:FZ SMK TROUT 3.5OZ UP10# Henningsen’s Twin Falls Henningsen TF 217069 52 520 0 OIQF-242 2 8/30/2022
8000 08000:FZ SMK TROUT 3.5OZ UP10# Henningsen’s Twin Falls Henningsen TF 223969 90 900 0 OIQF-307 2 11/3/2022
8000 08000:FZ SMK TROUT 3.5OZ UP10# Henningsen’s Twin Falls Henningsen TF 223967 80 800 0 OIQF-307 2 11/3/2022
8001 08001:FZ SMK GARTROUT 3.5UP10# CP-Finished Goods Inventory Holding Freezer N-15 209789 2 18 0 OIQF-330 1 11/26/2021
8001 08001:FZ SMK GARTROUT 3.5UP10# CP-Finished Goods Inventory Holding Freezer N-16 212846 24 240 0 OIQF-196 2 7/15/2022
8001 08001:FZ SMK GARTROUT 3.5UP10# CP-Finished Goods Inventory Holding Freezer N-18 207170 11 99 0 OIQF-330 1 11/26/2021
8001 08001:FZ SMK GARTROUT 3.5UP10# CP-Finished Goods Inventory Holding Freezer N-18 223970 32 320 0 OIQF-315 2 11/11/2022
8001 08001:FZ SMK GARTROUT 3.5UP10# Henningsen’s Twin Falls Henningsen TF 217070 56 560 0 OIQF-243 2 8/31/2022

Do you know if this is the extent of documentation on that printing feature? Printing | Dash for Python Documentation | Plotly
Feel bad wasting your time with my lack of knowledge but that page feels pretty thin on explaining how this is supposed to work.

Thank you!

Hi @jimbob

You can find a little more info in the AG Grid docs.

Note that you shouldn’t use the print format for large data. Perhaps you could disable the print button if the report is longer than x pages. The examples in the docs have the data limited to 75 lines - just to prevent someone from printing a huge report by accident.

Rather than creating an example based on your sample data, I used the example in the docs and added groups and a pinned bottom row. I thinks some of the issues with the example you posted is how you are storing the filtered data.

Try running this:

import dash_ag_grid as dag
from dash import Dash, html, dcc, Input, Output, State, callback, Patch
import pandas as pd
import dash_bootstrap_components as dbc

app = Dash(__name__, external_stylesheets=[dbc.themes.SPACELAB])

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)

#df = df.head(75)

columnDefs = [
    {"headerName": "ID", "valueGetter": {"function": "params.node.rowIndex + 1"}, "width": 70},
    {"field": "country", "rowGroup": True},
    {"field": "year"},
    {"field": "athlete"},
    {"field": "date"},
    {"field": "sport"},
    {"field": "total", "aggFunc": "sum"},
]

defaultColDef = {
    "resizable": True,
    "sortable": True,
    "filter": True,
    "maxWidth": 150,
}

grid = dag.AgGrid(
    id="grid-regular-layout",
    columnDefs=columnDefs,
    rowData=df.to_dict("records"),
    defaultColDef=defaultColDef,
    dashGridOptions={},
    enableEnterpriseModules=True
)

print_grid = dag.AgGrid(
    id="grid-modal-print",
    columnDefs=columnDefs,
    rowData=df.to_dict("records"),
    defaultColDef=defaultColDef,
    style={"height": "", "width": ""},
    dashGridOptions={"domLayout": "print"},
    enableEnterpriseModules=True
)

latin_text = dcc.Markdown("""        
        ### Latin Text
        Lorem ipsum dolor sit amet, ne cum repudiare abhorreant. Atqui molestiae neglegentur ad nam, mei amet eros ea, 
        populo deleniti scaevola et pri. Pro no ubique explicari, his reque nulla consequuntur in. His soleat doctus 
        constituam te, sed at alterum repudiandae. Suas ludus electram te ius.
        """, style={"maxWidth": 800})

more_latin_text = dcc.Markdown("""
    ### More Latin Text
    Lorem ipsum dolor sit amet, ne cum repudiare abhorreant. Atqui molestiae neglegentur ad nam, mei amet eros ea, 
    populo deleniti scaevola et pri. Pro no ubique explicari, his reque nulla consequuntur in. His soleat doctus 
    constituam te, sed at alterum repudiandae. Suas ludus electram te ius.
    """, style={"maxWidth": 800})

print_layout = html.Div(
    [
        dbc.ModalHeader(dbc.Button("Print", id="grid-browser-print-btn")),
        dbc.ModalBody(
            # customize your printed report here
            [
                html.H1("Dash AG Grid Print Report Demo", className="py-4"),
                latin_text,
                print_grid,
                more_latin_text,
            ],
            id="grid-print-area",
        ),
        html.Div(id="dummy"),
    ]
)

app.layout = html.Div(
    [
        dcc.Store(id="store-pinned-data"),
        dbc.Button("Print Report", id="grid-modal-print-btn"),
        grid,
        latin_text,
        dbc.Modal(
            print_layout, id="modal-print-container", is_open=False, size='xl'
        ),
    ],
    style={"margin": 20},
)

@callback(
    Output("grid-regular-layout", "dashGridOptions"),
    Output("store-pinned-data", "data"),
    Input("grid-regular-layout", "virtualRowData"),
)
def row_pinning_bottom(data):
    dff = df if data is None else pd.DataFrame(data)
    total = dff.total.sum()
    pinned_data =  [{"total": total}]

    grid_option_patch = Patch()
    grid_option_patch["pinnedBottomRowData"] = pinned_data
    return grid_option_patch, pinned_data



@callback(
    Output("grid-modal-print", "columnState"),
    Output("grid-modal-print", "filterModel"),
    Output("grid-modal-print", "dashGridOptions"),
    Output("modal-print-container", "is_open"),
    Input("grid-modal-print-btn", "n_clicks"),
    State("grid-regular-layout", "columnState"),
    State("grid-regular-layout", "filterModel"),
    State("store-pinned-data", "data"),
    prevent_initial_call=True,
)
def open_print_modal(_, col_state, filter_model, pinned_data):
    grid_option_patch = Patch()
    grid_option_patch["pinnedBottomRowData"] = pinned_data
    return col_state, filter_model, grid_option_patch, True


app.clientside_callback(
    """
    function () {            

         var printContents = document.getElementById('grid-print-area').innerHTML;
         var originalContents = document.body.innerHTML;

         document.body.innerHTML = printContents;

         window.print();

         document.body.innerHTML = originalContents;      
         location.reload()                              

        return window.dash_clientside.no_update
    }
    """,
    Output("dummy", "children"),
    Input("grid-browser-print-btn", "n_clicks"),
    prevent_initial_call=True,
)

if __name__ == "__main__":
    app.run_server(debug=True)


@AnnMarieW that did it! Restructured my code and created a similar clientside callback and now we’re in business.

Thank you for breaking this down the way you have and have a wonderful new year!