1. Creating an Excel Report from Claris FileMaker

Although there are several plugins capable of creating Excel documents, they often come with a high cost. The ACF plugin, as a low-cost alternative, is also capable of creating Excel reports. This tutorial is intended for Mac users and covers the use of ACF-Plugin version 1.7.0.19.

  1. Creating an Excel Report from Claris FileMaker
    1. About the Excel XML Format
    2. The lib functions
    3. The report generation
    4. Finalizing the Report

This tutorial consists of two parts:

There are several aspects of the ACF plugin that make it highly ideal for this task:

1.1. About the Excel XML Format

Excel can work with various formats: XLSX, XLS, XML, and more. The XLSX format is a ZIP compressed file and is a bit more difficult to work with. The content in an XLSX archive comprises a bunch of XML files, so manipulating them is possible. However, this is not the focus of this tutorial. We will concentrate on the XML format, which is a text format. You can create such a file from Excel using the "Save as" function, and select the 2004/XML format.

The file can be opened in any text editor, and as you will see, it is a very detailed XML file. This file is the target for this tutorial.

With the ACF plugin, we create a source code file for some ACF functions. I have used the XML/JSON dev-tool to create most of the library functions below, each responsible for creating a small part of the XML. This approach ensures that the XML attributes are accurate for each part (XML attributes are in the XML start tag, before the closing bracket).

It might seem like a lot, but the XML format for Excel documents is very detailed, and every detail needs to be accurate to open the document in Excel. Most functions are general, reusable functions.

1.2. The lib functions

The functions are written in the ACF language on a single text file. The file is loaded and compiled using the ACF plugin. Then in a single script step: `Set variable[$result; ACF_Run("MakeSalesMenWeekStatReport")] the functions produce the reports.

A fully working demo can be downloaded at horneks.no/downloads

Here is the lib I created:

package ExcelReport "Functions for creating EXCEL reports";

/*
    ====================================================
    EXCEL report implementation - the excel lib
    ====================================================
*/

Function Excel_DocProp(string Author)
    timestamp createdZ = now(); 
    string CreatedS = string ( createdZ, "%Y-%m-%2023-12-12%h:%m:%sZ");
    XML xmlvar;
    xmlVar["DocumentProperties"] = 
            XML("Author",Author,"LastAuthor",Author,"Created",CreatedS
            ,"LastSaved",CreatedS,"Version","16.00");
    // Assign attributes to the XML...
    XMLattributes(xmlVar["DocumentProperties"], XML("xmlns","urn:schemas-microsoft-com:office:office");
    
    return XmlVar;
End

Function Excel_DocSettings()

    XML xmlvar;
    xmlVar["OfficeDocumentSettings"] = 
            XML("AllowPNG","");
    // Assign attributes to the XML...
    XMLattributes(xmlVar["OfficeDocumentSettings"], XML("xmlns","urn:schemas-microsoft-com:office:office");
    
    return XmlVar;
End


Function Excel_WorkBook()

    XML xmlvar;
    xmlVar["ExcelWorkbook"] = 
            XML("WindowHeight","17440","WindowWidth","28040","WindowTopX","11580"
            ,"WindowTopY","5400","ProtectStructure","False","ProtectWindows","False"
            );
    // Assign attributes to the XML...
    XMLattributes(xmlVar["ExcelWorkbook"], XML("xmlns","urn:schemas-microsoft-com:office:excel");
    
    return XmlVar;
End



Function Excel_Table(int col)

    XML xmlvar;
    xmlVar["Table"] = "";
    // Assign attributes to the XML...
    // removed : "ss:ExpandedRowCount","20",
    // to be added after all rows is added. 
    XMLattributes(xmlVar["Table"], XML("ss:ExpandedColumnCount",string(col),"x:FullColumns","1"
            ,"x:FullRows","1","ss:DefaultColumnWidth","65","ss:DefaultRowHeight","16");

    return XmlVar;
End

Function Excel_WorkSheetOptions()

    XML xmlvar;
    xmlVar["WorksheetOptions"] = 
            XML("Selected","","ProtectObjects","False","ProtectScenarios","False"
            );
    xmlVar["WorksheetOptions.PageSetup"] = 
            XML("Header","","Footer","","PageMargins",""
            );
    xmlVar["WorksheetOptions.Panes.Pane"] = 
            XML("Number","3","ActiveRow","13","ActiveCol","3"
            );
    // Assign attributes to the XML...
    XMLattributes(xmlVar["WorksheetOptions"], XML("xmlns","urn:schemas-microsoft-com:office:excel");
    XMLattributes(xmlVar["WorksheetOptions.PageSetup.Header"], XML("x:Margin","0.3");
    XMLattributes(xmlVar["WorksheetOptions.PageSetup.Footer"], XML("x:Margin","0.3");
    XMLattributes(xmlVar["WorksheetOptions.PageSetup.PageMargins"], XML("x:Bottom","0.75","x:Left","0.7","x:Right","0.7"
            ,"x:Top","0.75");
    
    return XmlVar;
End

Function Excel_Column(int index, int width)
    XML xmlvar;
    xmlVar["Column"] = "";
    // Assign attributes to the XML...
    XMLattributes(xmlVar["Column"], XML("ss:Index",string(index),"ss:AutoFitWidth","0","ss:Width",string(width));
    return XmlVar;
End

The functions described above help us create the document structure. However, we need a few more elements to complete the process.

Excel Styles

The functions below might need to be changed or expanded to reflect the actual styles in your reports. Some functions are designed to create a single style. These are used in the function at the bottom named setStyles. This function combines all the detailed styles into one comprehensive Styles structure.

Function Excel_Style()
    // Function to create a default Excel style
    XML xmlvar;
    xmlVar["Style"] = 
            XML("Alignment", "", "Borders", "", "Font", "", "Interior", "", "NumberFormat", "", "Protection", "");
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Style"], XML("ss:ID", "Default", "ss:Name", "Normal"));
    XMLattributes(xmlVar["Style.Alignment"], XML("ss:Vertical", "Bottom"));
    XMLattributes(xmlVar["Style.Font"], XML("ss:FontName", "Calibri", "x:Family", "Swiss", "ss:Size", "12", "ss:Color", "#000000"));
    
    return XmlVar;
End

Function Excel_RightStyle()
    // Function to create a right-aligned Excel style
    XML xmlvar;
    xmlVar["Style"] = 
            XML("Alignment", "", "Font", "");
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Style"], XML("ss:ID", "s66"));
    XMLattributes(xmlVar["Style.Alignment"], XML("ss:Horizontal", "Right", "ss:Vertical", "Bottom"));
    XMLattributes(xmlVar["Style.Font"], XML("ss:FontName", "Calibri", "x:Family", "Swiss", "ss:Size", "14", "ss:Color", "#000000", "ss:Bold", "1"));
    
    return XmlVar;
End

Function Excel_NumberStyle()
    // Function to create a numeric Excel style
    XML xmlvar;
    xmlVar["Style"] = 
            XML("Alignment", "", "Borders", "", "Font", "", "Interior", "", "NumberFormat", "", "Protection", "");
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Style"], XML("ss:ID", "cNum"));
    XMLattributes(xmlVar["Style.Alignment"], XML("ss:Vertical", "Bottom"));
    XMLattributes(xmlVar["Style.Font"], XML("ss:FontName", "Calibri", "x:Family", "Swiss", "ss:Size", "12", "ss:Color", "#000000"));
    XMLattributes(xmlVar["Style.NumberFormat"], XML("ss:Format", "#,##0"));
    
    return XmlVar;
End

Function Excel_DateStyle()
    // Function to create a date format Excel style
    XML xmlvar;
    xmlVar["Style"] = 
            XML("Alignment", "", "Borders", "", "Font", "", "Interior", "", "NumberFormat", "", "Protection", "");
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Style"], XML("ss:ID", "s68"));
    XMLattributes(xmlVar["Style.Alignment"], XML("ss:Vertical", "Bottom"));
    XMLattributes(xmlVar["Style.Font"], XML("ss:FontName", "Calibri", "x:Family", "Swiss", "ss:Size", "12", "ss:Color", "#000000"));
    XMLattributes(xmlVar["Style.NumberFormat"], XML("ss:Format", "dd/mm/yyyy;@"));
    
    return XmlVar;
End

Function CreateColorStyle(string ssid, string back, string textcolor)
    // Function to create a colored Excel style
    XML xmlvar;
    xmlVar["Style"] = XML("Interior", "");
    XMLattributes(xmlVar["Style"], XML("ss:ID", ssid));
    XMLattributes(xmlVar["Style.Interior"], XML("ss:Color", back, "ss:Pattern", "Solid"));
    return xmlvar; 
end

Function Excel_SumColStyle(string style)
    // Function to create a sum column style in Excel
    XML xmlvar;
    xmlVar["Style"] = 
            XML("Font", "", "NumberFormat", "", "Borders", XML("Border", "", "Border", ""));
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Style"], XML("ss:ID", style));
    XMLattributes(xmlVar["Style.Borders.Border[1]"], XML("ss:Position", "Bottom", "ss:LineStyle", "Double", "ss:Weight", "3"));
    XMLattributes(xmlVar["Style.Borders.Border[2]"], XML("ss:Position", "Top", "ss:LineStyle", "Continuous", "ss:Weight", "1"));
    XMLattributes(xmlVar["Style.Font"], XML("ss:FontName", "Calibri", "x:Family", "Swiss", "ss:Size", "12", "ss:Color", "#000000", "ss:Bold", "1"));
    XMLattributes(xmlVar["Style.NumberFormat"], XML("ss:Format", "#,##0"));
    
    return XmlVar;
End

Function Excel_BoldTextStyle(string style)
    // Function to create a bold text style in Excel
    XML xmlvar;
    xmlVar["Style"] = 
            XML("Font", "");
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Style"], XML("ss:ID", style));
    XMLattributes(xmlVar["Style.Font"], XML("ss:FontName", "Calibri", "x:Family", "Swiss", "ss:Size", "12", "ss:Color", "#000000", "ss:Bold", "1"));
    
    return XmlVar;
End

function SetStyles()
    // Function to set various Excel styles
    XML styles; 
    // Standard font style
    styles["Styles"] = Excel_Style(); // Style "Default"
    
    // 14pt Bold style
    styles["Styles.Style[]"] = xml("Font", ""); 
    xmlAttributes(styles["Styles.Style[2]"], xml("ss:ID", "s64")); 
    xmlAttributes(styles["Styles.Style[2].Font"], XML("ss:FontName", "Calibri", "x:Family", "Swiss", "ss:Size", "14", "ss:Color", "#000000", "ss:Bold", "1"));
        
    // Standard bold right justified style
    styles["Styles.Style[]"] = XML("Alignment", "", "Font", "");
    xmlAttributes(styles["Styles.Style[3]"], XML("ss:ID", "s66"));
    XMLattributes(styles["Styles.Style[3].Alignment"], XML("ss:Horizontal", "Right", "ss:Vertical", "Bottom"));
    xmlAttributes(styles["Styles.Style[3].Font"], XML("ss:FontName", "Calibri", "x:Family", "Swiss", "ss:Size", "14", "ss:Color", "#000000", "ss:Bold", "1"));
            
    // Standard bold with grey background style
    styles["Styles.Style[]"] = XML("Font", "", "Interior", "");
    XMLattributes(styles["Styles.Style[4]"], XML("ss:ID", "s67"));
    XMLattributes(styles["Styles.Style[4].Font"], XML("ss:FontName", "Calibri", "x:Family", "Swiss", "ss:Size", "12", "ss:Color", "#000000", "ss:Bold", "1"));
    XMLattributes(styles["Styles.Style[4].Interior"], XML("ss:Color", "#E7E6E6", "ss:Pattern", "Solid"));
    
    // Other styles defined in their respective functions
    styles["Styles+"] = Excel_DateStyle(); 
    styles["Styles+"] = Excel_NumberStyle();
    styles["Styles+"] = CreateColorStyle("color1", "#70AC46", "#000000");
    styles["Styles+"] = Excel_SumColStyle("cSumRow");
    styles["Styles+"] = Excel_BoldTextStyle("cBoldText"); 
    
    return styles; 
end

Functions for Data Content

Additionally, we require functions for handling the data content in the report. These functions are essential for simplifying content generation by abstracting the XML-specific details from the process of creating the content.

// Function for creating the left and right heading row
Function Excel_headingRow1(string leftText, string rightText, int colcnt)

    XML xmlvar;
    xmlVar["Row.Cell[]"] = XML("Data", leftText);
    xmlVar["Row.Cell[]"] = XML("Data", rightText);
    
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Row"], XML("ss:Height", "19"));
    XMLattributes(xmlVar["Row.Cell[1]"], XML("ss:StyleID", "s64")); // Style for left cell
    XMLattributes(xmlVar["Row.Cell[1].Data"], XML("ss:Type", "String"));
    
    XMLattributes(xmlVar["Row.Cell[2]"], XML("ss:Index", string(colcnt), "ss:StyleID", "s66")); // Style for right cell
    XMLattributes(xmlVar["Row.Cell[2].Data"], XML("ss:Type", "String"));

    return xmlVar;
End

// Function for creating a single left heading row
Function Excel_headingRow2(string leftText)

    XML xmlvar;
    xmlVar["Row.Cell"] = XML("Data", leftText);
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Row"], XML("ss:Height", "19"));
    XMLattributes(xmlVar["Row.Cell"], XML("ss:StyleID", "s64"));
    XMLattributes(xmlVar["Row.Cell.Data"], XML("ss:Type", "String"));
    
    return xmlVar;
End

// Function for creating cells containing text
Function Excel_TextCell(string text, string style)
    XML xmlvar;
    xmlVar["Cell"] = XML("Data", text);
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Cell"], XML("ss:StyleID", style));
    XMLattributes(xmlVar["Cell.Data"], XML("ss:Type", "String"));
    
    return xmlVar;
End

// Function for creating cells containing numeric values
Function Excel_NumberCell(float number, string style)

    XML xmlvar;
    xmlVar["Cell"] = XML("Data", string(number));
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Cell"], XML("ss:StyleID", style));
    XMLattributes(xmlVar["Cell.Data"], XML("ss:Type", "Number"));
    
    return xmlVar;
End

// Function for creating cells containing dates
Function Excel_DateCell(date dato, string style)

    XML xmlvar;
    xmlVar["Cell"] = XML("Data", string(dato, "%Y-%m-%dT00:00:00.000"));
    // Assign attributes to the XML elements
    XMLattributes(xmlVar["Cell"], XML("ss:StyleID", style));
    XMLattributes(xmlVar["Cell.Data"], XML("ss:Type", "DateTime"));
    return xmlVar;
End

// Function for creating a cell with a sum formula
Function Excel_SumColCell(int col, int row, int fromRow, int toRow, string style)

    XML xmlvar;
    xmlVar["Cell"] = XML("Data", "");
    // Assign attributes to the XML elements
    
    string formula = format("=SUM(R[%d]C:R[%d]C)", fromRow - row + 1, toRow - row); 
    print "\n" + formula; 
    XMLattributes(xmlVar["Cell"], XML("ss:Index", string(col), "ss:StyleID", style, "ss:Formula", formula));
    XMLattributes(xmlVar["Cell.Data"], XML("ss:Type", "Number"));
    
    return xmlVar;
End

Some utility functions:

There is also a need for utility functions to facilitate the creation of the final report. One such function is required to search for a specific value in an array and return its index. It's possible that a future version of the ACF plugin will include this functionality built-in.

function find_in_array ( array string arr, string key)
    int i; 
    for ( i = 1, sizeof(arr))
        if ( arr[i] == key) then
            return i; 
        end if
    end for
    return -1; 
end

1.3. The report generation

In this example, we will extract data from an order table named OrderHeader. This table will be used to generate a sales report, which will display one line for each salesperson, along with their sales amounts per week for the past 11 weeks, formatted as columns in the spreadsheet.

Main Function - We begin by querying for the report data after making some initial declarations.

function MakeSalesMenWeekStatReport()
// Set the date interval for the report (can also be parameterized)
    date dateTo = date(now()) - 1;  // Set 'dateTo' as yesterday
    int WeeksBack = 11;  // Define the number of weeks to look back
    date dateFrom = dateTo - WeeksBack * 7;  // Calculate 'dateFrom' as 11 weeks prior to 'dateTo'

// Declare arrays for storing query results
    array string uIDs, Perioder, PeriodeIdx;
    array int sAar, sUke; 
    array float ordSum; 

// SQL query definition
    string sql = "SELECT User_ID, Order_Year, Order_Week, sum(Sum_Order) FROM OrderHeader
    WHERE User_ID <> '1' AND Order_date BETWEEN :dateFrom AND :dateTo
    GROUP BY User_ID, Order_Year, Order_Week
    INTO :uIDs, :sAar, :sUke, :ordSum";
    
    // Execute the SQL query
    string res = ExecuteSQL(sql); 

As demonstrated in the snippet above, we calculate two dates: dateTo, set as yesterday, and dateFrom, set as 11 weeks prior.

Subsequently, we declare some arrays to receive the results from the SQL query. The OrderHeader table includes fields such as Order_date, a calculated field Order_Year, and another calculated field named Order_Week, which represents the week number. The WeekOfYearFiscal(Order_Date;2) function is utilized in these calculations to determine the week number.

The INTO part of the SQL query is a special feature of the ACF plugin. It specifies the arrays that will receive the query results. We obtain the results in four arrays: uIDs, sAar, sUke, and ordSum.

Additionally, the use of :dateFrom and :dateTo is specific to the ACF plugin. These placeholders represent actual data from those variables, which will be used at runtime.

To organize the data into weekly periods, we need to create an array of periods corresponding to each row in the other four arrays. We use the year and week numbers to create a period string formatted as YYYY-WW. These strings identify the columns that will receive the data.

int i, cnt = sizeof (uIDs); 
    string cmd = string(dateTo+1, "WeekOfYearFiscal(Date(%m;%d;%Y);2)"); 
    string firstPeriod = string (dateTo+1, "%Y") + format("-%02d", int(eval(cmd)));
    string per; 
    for (i=1, cnt)
        per =  format("%04d-%02d", sAar[i], sUke[i]);
        Perioder[] = per; 
        if ( per < firstPeriod) then
            firstPeriod = per; 
        end if
    end for

Next, we sort the arrays to ensure that all the user IDs are ordered correctly, which is essential for the subsequent handling of breaks. The sorting is done using the following ACF command:

sort_array ( < uIDs, <sAar, <sUke, ordSum, Perioder); 

We also need to create some column headings. In addition, we initialize a data array to store one line of data in the report. At the end of this process, we copy this data into an array named Empty. This approach allows us to easily reset the data array for each new salesperson.

  array float lineData; 
    array string lineHD; 
    date Sun; 
    for (i=1, WeeksBack)
        Sun = dateTo-(i-1)*7;
        cmd = string(Sun, "WeekOfYearFiscal(Date(%m;%d;%Y);2)"); 
        lineHD[] =  string (Sun, "%Y") + format("-%02d", int(eval(cmd)));
        linedata[] = 0; 
    end for
    array float Empty = linedata; 

The variable Sun represents the date of the Sunday corresponding to the week of the column. To obtain the week number for this Sunday, we create a FileMaker calculation. Although the ACF plugin doesn't have a direct function for this, it offers the eval function, which leverages the FileMaker calculation engine to evaluate expressions. The string function mentioned previously takes a date as its first parameter and a format string as the second argument. This format string is a FileMaker command, where %m, %d, and %Y are placeholders for the month, day, and year values, respectively.

Since the user names and signatures are not included in the data arrays, it's necessary to retrieve the user information to map them correctly in the final rows. The column Bruker_ID is numeric, but we need it in a text format. To achieve this, a short loop is used to convert and store these numeric IDs as text variants in the array Selger_IDs.

  array string selger_IDs, selger_Navn, selger_signatur; 
    array int selger_id_num; 
    
    // Grab users
    sql = "SELECT Bruker_ID, Navn, Signatur FROM Users
    ORDER BY Navn
    
    INTO :selger_id_num, :selger_Navn, :selger_signatur";
    res = ExecuteSQL ( sql ); 
    
    for ( i=1, sizeof ( selger_id_num))
        selger_IDs[] = string (selger_id_num[i] ); 
    end for

The Production of the Excel Document:

We start by declaring two variables: Excel as an XML datatype, and NumberOfRows to keep track of the row count in our report. It's important to update the XML with the row count once all the columns have been added.

XML excel; 
int NumberOfRows; 

Next, we construct the structural elements of the XML, utilizing the library functions discussed in the first part of this article:

    clear(excel); // We clear it up first.... 
    excel["Workbook"] = Excel_DocProp("John Doe Author");
    excel["Workbook"] = Excel_DocSettings();
    excel["Workbook"] = Excel_WorkBook();
    excel["Workbook"] = SetStyles();
    
    XMLattributes(excel["Workbook"], XML("xmlns","urn:schemas-microsoft-com:office:spreadsheet",
    "xmlns:o","urn:schemas-microsoft-com:office:office",
    "xmlns:x","urn:schemas-microsoft-com:office:excel",
    "xmlns:ss","urn:schemas-microsoft-com:office:spreadsheet",
    "xmlns:html","https://www.w3.org/TR/REC-html40");
    
    int TotalColumns = 3+WeeksBack; 
    XML worksheet; 
    worksheet["Worksheet"] = Excel_Table(TotalColumns);
        // Assign attributes to the XML...
    XMLattributes(worksheet["Worksheet"],XML("ss:Name","SalesWeekReport");
    
    // Define the column-with. All except A, B, and C should be 80. 
    array int cw = {65,200,65}; 
    int colw;
    for (i=1, TotalColumns)
        colw = (i>3)?80:cw[i]; 
        worksheet["Worksheet.Table+"] = Excel_Column(i,colw); 
    end for
     
    // Add a row for the headings, merging in the lineHD array created above
    array string headings = {"SelgerID", "Navn", "Signatur", lineHD};
     
    // Top two lines are report headings. 
    worksheet["Worksheet.Table+"] = Excel_headingRow2 ( "Salesperson report - Week statistics"); 
    worksheet["Worksheet.Table+"] = Excel_headingRow2 ( "To be run each monday ");
    // Next line has date interval left, and description for the data rows to the right, 
    worksheet["Worksheet.Table+"] = Excel_headingRow1 ( string (dateFrom, "%d.%m.%Y" +" - " + string (dateTo, "%d.%m.%Y")), "Year-Week number", TotalColumns);
    

Then we add the rows for the column headings:

// Create the heading rows for the table. 
    XML row; 
    clear ( row ); 
    for (i=1, sizeof(headings))
        row["Row+"] = Excel_TextCell(headings[i], "s67"); 
    end for
    worksheet["Worksheet.Table+"] = row;
    NumberOfRows = 5; // Next row is number 5
    
    excel["Workbook"] = worksheet; 
    excel["Workbook"] = Excel_WorkSheetOptions(); 

Now, we iterate through the arrays. The process involves a break upon each change in User-ID. For each new person identified, we append a new row to the Excel document. All the rows in our table corresponding to the same salesperson contribute data to the lineData array. During this process, we also retrieve each salesperson's name and signature to ensure this information is included.

int uix, j; 
string Name, Signature; 
slepSelger=uIDs[1]; 
float errorSum = 0; 
bool addData = False; 
// Produce data rows for the spreadsheet. 
for (i=1, cnt)
    // Check for a new salesperson. 
    if (uIDs[i] != slepSelger) then
        uix = find_in_array(selger_IDs, slepSelger); 
        if (uix > 0) then
            Name = selger_Navn[uix]; 
            Signature = selger_signatur[uix]; 
        else
            Name = "-"; 
            Signature = "-"; 
        end if
        
        // Add a new row to the Excel sheet.
        clear(row);
        row["Row+"] = Excel_TextCell(slepSelger, "Default");
        row["Row+"] = Excel_TextCell(Name, "Default");
        row["Row+"] = Excel_TextCell(Signature, "Default");
        
        for (j = 1, WeeksBack)
            row["Row+"] = Excel_NumberCell(linedata[j], "cNum");
        end for
        excel["Workbook.Worksheet.Table+"] = row; 
        NumberOfRows++; 
        
        linedata = Empty; 
        addData = False; 
        slepSelger = uIDs[i]; 
    end if
    
    // Accumulate data for the current salesperson.
    uix = find_in_array(lineHD, Perioder[i]); 
    if (uix > 0 && uix <= WeeksBack) then
        linedata[uix] = linedata[uix] + ordSum[i]; 
        addData = True; 
    else
        errorSum += ordSum[i]; 
    end if
end for

When we reach the last row in the table, there isn't a natural break because the loop is concluding. Therefore, we need to ensure that this final line is also added. The process for this involves replicating the code used in the previous conditional statement (the if statement). This step ensures that the data for the last salesperson is properly captured and included in the Excel document, just like for the preceding rows.

// Some data for the last sales person, not handled in the break above. 
    if ( addData ) then
        uix = find_in_array ( selger_IDs, slepSelger); 
        if ( uix > 0) then
            Name = selger_Navn[uix]; 
            Signature = selger_signatur[uix]; 
        else
            Name = "-"; 
            Signature = "-"; 
        end if
        
        // Add the last row to excel
        clear ( row );
        row["Row+"] = Excel_TextCell(slepSelger, "Default");
        row["Row+"] = Excel_TextCell(Name, "Default");
        row["Row+"] = Excel_TextCell(Signature, "Default");
    
        for (j = 1, WeeksBack)
            row["Row+"] = Excel_NumberCell(linedata[j], "cNum");
        end for
        excel["Workbook.Worksheet.Table+"] = row; 
        NumberOfRows ++; 
    end if

Adding a Sum Line Below the Table

It's beneficial to include a sum line below the table. Rather than calculating the sums directly, we insert Excel formulas for the summation. This approach allows users to manually edit data in the report while ensuring the sums remain accurate.

// Add a sum-line
     
    clear ( row );
    row["Row+"] = Excel_TextCell("", "cBoldText");
    row["Row+"] = Excel_TextCell("Sum alle selgere", "cBoldText");
    row["Row+"] = Excel_TextCell("", "cBoldText");
    for (i=1, WeeksBack)
        row["Row+"] = Excel_SumColCell(i+3, NumberOfRows, 4, NumberOfRows-1, "cSumRow"); 
    end for
    excel["Workbook.Worksheet.Table+"] = row; 
    NumberOfRows ++; 

1.4. Finalizing the Report

To simplify the process, we create a folder on the user's desktop named "Monday_reports". Within this folder, we establish a file path:

// Crete a path for the report. 
    res = create_directory ( Desktop_directory () + "/Monday_reports/"); 
    string path = Desktop_directory () + "/Monday_reports/SelgerHistorikk_"+ string ( now(), "%Y%m%d_%h%i%s") + ".xml";
    

Update table element with the number of rows:

// Update table attributes to reflex the number of rows. 
    XMLattributes (excel["Workbook.Worksheet.Table"], xml("ss:ExpandedRowCount",string(NumberOfRows)); 

Then we save the report to the file, and returns.

// Save the report to the file. 
    string report = string (excel); // Stringify the XML
    clear (excel); 
    
    int x = open (path, "w" ); 
    write ( x, report); 
    close (x); 
    
    return "OK";

end

Here is the produced report opened in Excel

SalesPersonWeek