Creating a customized a Kinetic Function to output to a .csv file

Nothing saves without user interaction that’s the browser sandbox. We all click download links daily.

Try this one-liner in your button click event condition action:

(json => 
    Object.assign(
        document.createElement('a'),
        Object.fromEntries([
            [
                'href',
                URL.createObjectURL(
                    new Blob(
                        [
                            [
                                Object.keys(json[0]).join(','),
                                ...json.map(o => Object.values(o).join(','))
                            ].join('\n')
                        ],
                        { type: 'text/csv' }
                    )
                )
            ],
            ['download', 'data.csv']
        ])
    ).click()
)(
    trans.getDataView("Parts").data
)

To pass data from a DataView into a function, where you can then manipulate as needed, and back out again, use kinetic-rest widget instead of kinetic-function widget, configure it like this:

In App Studio, we assume you have two DataViews set up, “inDV” which is the input DataView to the function, and “outDV” which receives the output data from the function. “inDV” and “outDV” could be the same DataView if you want to change the data in-place.

Assuming “inDS” is your Input DataSet, coming from a DataView named “inDV”.
“inDV” will become “inDT” contained within “inDS” (your DataView will become a DataTable inside a DataSet)

Assuming “outDS” is your Output DataSet, “outDT” is your Output DataTable, “outDV” is your Output DataView.

Inside Application Studio, in an Event, add a rest-kinetic (rest-erp) widget, inside “Service Operation” put your Function Name.

In rest-erp → Rest Services → Call Options set “Epicor Functions Library” to the name of your Function Library.

In rest-erp → Rest Services → Kinetic Rest Arguments → Request Parameters, set it up like below (this is your input DataView from Application Studio becoming an input DataSet/DataTable in the function:

In rest-erp → Rest Services → Kinetic Rest Arguments → Response Parameters, set up like below: (this is the output DataSet from the function, containing an Output DataTable, which will become an Application Studio DataView)

In your function, you need an input signature with “inDS” (System.DataSet), and an output signature with “outDS” (System.DataSet)

You can access the input DataTable (AppStudio DataView) in the function like this (assuming your DataView has columns (string)“PartNum”, (decimal)“QtyShipped”, (decimal)“QtyReceived”), and perform your needed calculations inside the function (assuming you want to create a new column “QtyLost” which is the difference of QtyShipped and QtyReceived)

DataTable inDT = inDS.Tables["inDT"];
outDS = new DataSet();
DataTable outDT = inDT.Copy();
outDT.TableName = "outDT";
outDT.Columns.Add("QtyLost", typeof(decimal));
foreach (DataRow r in inDataTable.Rows) {
  r["QtyLost"] = (decimal)r["QtyShipped"] - (decimal)r["QtyReceived"];
}
outDS.Tables.Add(outDT);

Now, if you wanted to also write this data out to .csv inside the function in addition to adding a calculated column, do this:

string filename = "somefile.csv";
var filetext = new StringBuilder();
DataTable inDT = inDS.Tables["inDT"];
outDS = new DataSet();
DataTable outDT = inDT.Copy();
outDT.TableName = "outDT";
outDT.Columns.Add("QtyLost", typeof(decimal));
string headers = "";
foreach (DataColumn c in outDT.Columns) {
  headers = headers + c.ColumnName + ",";
}
headers = headers.Substring(0, headers.Length -1);
filetext.AppendLine(headers);
string values = "";
foreach (DataRow r in outDT.Rows) {
  r["QtyLost"] = (decimal)r["QtyShipped"] - (decimal)r["QtyReceived"];
  values = "";
  foreach (DataColumn c in outDT.Columns) {
    values = values + r[c] + @",";
  }
  values = values.Substring(0, values.Length -1);
  filetext.AppendLine(values);
}
outDS.Tables.Add(outDT);
var folderPath = new FilePath(ServerFolder.CompanyData, "SubFolderYouWantToWriteTo");
var filePath = new FilePath(ServerFolder.CompanyData, @"SubFolderYouWantToWriteTo\" + filename);
this.Sandbox.IO.Directory.CreateDirectory(folderPath);
this.Sandbox.IO.File.WriteAllText(filePath, filetext.ToString(), System.Text.Encoding.Unicode);

That should both write out your modified DataSet/DataTable to an output DataView, and simultaneously save the output dataview as a .csv.

Lastly, if the function returns no error, but does not output desired results, wrap your code in a try/catch block to handle any exceptions that may occur, and check epicor’s help for “Application Logging” - add some detailed application logging where you “talk to yourself from inside the function” and tell yourself the state of the data at various locations. This can give you more information to diagnose the problem and arrive at a solution.

To add detailed logging to the function above, I would do this, which will show the output in f12 dev tools console when running the function, in addition to writing a log file you can find in Server File Download \ UserData:

References: Assemblies\Microsoft.Extensions.Logging.Abstractions.dll
References: Assemblies\Microsoft.Extensions.Logging.dll
using Microsoft.Extensions.Logging;
using Ice.Logging;
string lfn = "MyFunctionLibrary-MyFunctionName.log";
string timestamp = DateTime.Now.ToString("[yyyy/MM/dd HH:mm:ss] ");
var logger = ApplicationLoggerBuilder.CreateBuilder().SetMinimumLevel(LogLevel.Information).AddFile(options => {
            options.Session = this.Session;
            options.FileName = lfn;
            options.Folder = LogFolder.User;
            options.MessageOptions.QuoteValues = false;
            options.MessageOptions.ShowLogLevel = false;
            options.MessageOptions.TimestampFormat = TimestampFormat.DateTime;
            options.TruncateFile = true;
  }).Build();

var log = new Action<string>(msg => {
    logger.LogInformation(msg);
    outResult += timestamp + $"{msg}\n";
});

using (logger) {
  try {
	string filename = "somefile.csv";
	var filetext = new StringBuilder();
	DataTable inDT = inDS.Tables["inDT"];
	outDS = new DataSet();
	log("output dataset initialized");
	DataTable outDT = inDT.Copy();
	outDT.TableName = "outDT";
	outDT.Columns.Add("QtyLost", typeof(decimal));
	log("added QtyLost column to outDT");
	string headers = "";
	foreach (DataColumn c in outDT.Columns) {
	  headers = headers + c.ColumnName + ",";
	}
	headers = headers.Substring(0, headers.Length -1);
	log($"adding csv header line: {headers}");
	filetext.AppendLine(headers);
	string values = "";
	foreach (DataRow r in outDT.Rows) {
	  r["QtyLost"] = (decimal)r["QtyShipped"] - (decimal)r["QtyReceived"];
	  values = "";
	  foreach (DataColumn c in outDT.Columns) {
		values = values + r[c] + @",";
	  }
	  values = values.Substring(0, values.Length -1);
	  log($"adding csv values line: {values}");
	  filetext.AppendLine(values);
	}
	log($"Adding DataTable: {outDT.TableName} to DataSet outDS");
	outDS.Tables.Add(outDT);
	string folderName = "SubFolderYouWantToWriteTo";
	var folderPath = new FilePath(ServerFolder.CompanyData, folderName);
	var filePath = new FilePath(ServerFolder.CompanyData, @"SubFolderYouWantToWriteTo\" + filename);
	log($"Writing CSV: {filename} to CompanyData\{folderName}");
	this.Sandbox.IO.Directory.CreateDirectory(folderPath);
	this.Sandbox.IO.File.WriteAllText(filePath, filetext.ToString(), System.Text.Encoding.Unicode);
  } catch (Exception ex) {
    string exinfo = $"Exception: {ex.GetType().FullName} HResult: 0x{ex.HResult:X} Text: {System.Runtime.InteropServices.Marshal.GetExceptionForHR(ex.HResult).Message}\nMessage: {ex.Message}\nSource: {ex.Source}\nHelpLink: {ex.HelpLink}\nTargetSite: {(ex.TargetSite != null ? ex.TargetSite.ToString() : "")}\nStackTrace: {ex.StackTrace}\nData: {(ex.Data != null && ex.Data.Count > 0 ? string.Join("; ", ex.Data.Cast<System.Collections.DictionaryEntry>().Select(d => d.Key + "=" + d.Value)) : "")}\n";
    if (ex.InnerException != null)
    {
        var inner = ex.InnerException;
        exinfo += $"Inner Exception: {inner.GetType().FullName} HResult: 0x{inner.HResult:X} Text: {System.Runtime.InteropServices.Marshal.GetExceptionForHR(inner.HResult).Message}\nMessage: {inner.Message}\nSource: {inner.Source}\nHelpLink: {inner.HelpLink}\nTargetSite: {(inner.TargetSite != null ? inner.TargetSite.ToString() : "")}\nStackTrace: {inner.StackTrace}\nData: {(inner.Data != null && inner.Data.Count > 0 ? string.Join("; ", inner.Data.Cast<System.Collections.DictionaryEntry>().Select(d => d.Key + "=" + d.Value)) : "")}\n";
    }
    log(exinfo);
  } finally {
    log("Finally");
  }
}

@GabeFranco you state “Now, if you wanted to also write this data out to .csv inside the function in addition to adding a calculated column, do this:”… not meaning to sound like a

nob but where is this (below code) inserted?

This would be near the top of your function, this code initializes everything. takes the input dataset and creates a variable inDT to address the datatable inside of it, and a variable outDT to hold the data to be sent back out of the function.

In the last section of code, it’s a complete function that could be copy pasted in, you can see where it is used there.

We were able to resolve the issue outputting data into a .csv … all your inputs helped very much. Big up’s @GabeFranco

If you have the final solution and are open to sharing it that would be of great help to others, much like the help you received to make it.

It was provided in the sample code by [GabeFranco] 2025.1.11 above… search for “a complete function that could be copy pasted”. Thanks again Gabe…

Interesting discussion TLDR… Could you not use an updatable baq to to what you do to generate the data, then just schedule that with baq export?