Code to Print Multiple AR Invoices

thanks for the reply
i,m working on a similar issue - and will post up if we get it done
update…
been slightly delayed - but will get there

1 Like

Just curious if anyone has figured out a slick way to do this yet. I am working on replacing a legacy system that allows users to select any number of invoices for a customer on a web page and print them with the click of a button.

I have found that running a separate REST call to regenerate each invoice individually takes an unacceptable amount of time when there are more than say 10 invoices.

We found a while back the standard menu selection “Mass Print AR Invoices” - which is associated with Demand Mgmt / EDI processing does the trick for us.
It does work to ranges of invoices - in that we can easily tie a numeric range to an AR batch.

1 Like

Ok thanks

You guys could ask @Aaron_Moreng

I remember having a screenshot with him doing something like:

Aaron can you elaborate on your technique? Adapter, comma delimited list? or multiple calls to the BO?

1 Like

I leveraged the native EpiDataView “ReportParam” inside Mass Print AR Invoices to submit each invoice number in the list to the task agent quickly. The only problem I’ve had with this approach is that if they are submitted too quickly, there has been instances where the report header does not match the report details, resulting in some really wonky invoices. To solve, I put added Thread.Sleep(500) inside my foreach loop and that has served me well.

// **************************************************
/// Custom code for ARInvForm
// Created: 5/20/2016 3:08:40 PM
// **************************************************
using System;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Windows.Forms;
using Erp.UI;
using Ice.Lib.Customization;
using Ice.Lib.ExtendedProps;
using Ice.Lib.Framework;
using Ice.Lib.Searches;
using Ice.UI.FormFunctions;
using System.Text.RegularExpressions;
using System.Threading;

public class Script
{
	// ** Wizard Insert Location - Do Not Remove 'Begin/End Wizard Added Module Level Variables' Comments! **
	// Begin Wizard Added Module Level Variables **

	private EpiDataView edvReportParam;
	// End Wizard Added Module Level Variables **

	// Add Custom Module Level Variables Here **
	DataTable dtInput = new DataTable();
	string Invoice;
	bool printed = false;
	public void InitializeCustomCode()
	{
		// ** Wizard Insert Location - Do not delete 'Begin/End Wizard Added Variable Initialization' lines **
		// Begin Wizard Added Variable Initialization

		this.edvReportParam = ((EpiDataView)(this.oTrans.EpiDataViews["ReportParam"]));
		this.edvReportParam.EpiViewNotification += new EpiViewNotification(this.edvReportParam_EpiViewNotification);
		// End Wizard Added Variable Initialization

		// Begin Wizard Added Custom Method Calls

		this.btnAdd.Click += new System.EventHandler(this.btnAdd_Click);
		this.btnRemove.Click += new System.EventHandler(this.btnRemove_Click);
		this.btnPrint.Click += new System.EventHandler(this.btnPrint_Click);
		this.txtAddInvoice.Validated += new System.EventHandler(this.txtAddInvoice_Validated);
		this.btnClear.Click += new System.EventHandler(this.btnClear_Click);
		// End Wizard Added Custom Method Calls
		dtInput.Columns.Add("Invoice",typeof(int));
		dtInput.Columns.Add("Printed", typeof(bool));
	}

	public void DestroyCustomCode()
	{
		// ** Wizard Insert Location - Do not delete 'Begin/End Wizard Added Object Disposal' lines **
		// Begin Wizard Added Object Disposal

		this.edvReportParam.EpiViewNotification -= new EpiViewNotification(this.edvReportParam_EpiViewNotification);
		this.edvReportParam = null;
		this.btnAdd.Click -= new System.EventHandler(this.btnAdd_Click);
		this.btnRemove.Click -= new System.EventHandler(this.btnRemove_Click);
		this.btnPrint.Click -= new System.EventHandler(this.btnPrint_Click);
		this.txtAddInvoice.Validated -= new System.EventHandler(this.txtAddInvoice_Validated);
		this.btnClear.Click -= new System.EventHandler(this.btnClear_Click);
		// End Wizard Added Object Disposal

		// Begin Custom Code Disposal

		// End Custom Code Disposal
	}

	private void edvReportParam_EpiViewNotification(EpiDataView view, EpiNotifyArgs args)
	{
		// ** Argument Properties and Uses **
		// view.dataView[args.Row]["FieldName"]
		// args.Row, args.Column, args.Sender, args.NotifyType
		// NotifyType.Initialize, NotifyType.AddRow, NotifyType.DeleteRow, NotifyType.InitLastView, NotifyType.InitAndResetTreeNodes
		if ((args.NotifyType == EpiTransaction.NotifyType.Initialize))
		{
			if ((args.Row > -1))
			{
			//MessageBox.Show(edvReportParam.dataView[edvReportParam.Row]["InvoiceNum"].ToString());
			}
		}
	}
	

	private void ARInvForm_Load(object sender, EventArgs args)
	{
		// Add Event Handler Code
		EpiDataView dvRP = (EpiDataView)oTrans.EpiDataViews["ReportParam"];
		//MessageBox.Show(dvRP.dataView[dvRP.Row]["InvoiceNum"].ToString());
		dtInput.Clear();
		grdInput.DataSource = dtInput;
		//EpiDataView dvRP = (EpiDataView)oTrans.EpiDataViews["ReportParam"];
		Invoice = dvRP.dataView[dvRP.Row]["InvoiceNum"].ToString();	
		if (Invoice != "0")
		{
			dtInput.Rows.Add(Invoice, printed);
		}
	}

	private void ReadOnlyDT()
	{
	foreach (DataColumn dc in dtInput.Columns)
		{
		//dc.ReadOnly = true;
		}
	}
	
	private void btnAdd_Click(object sender, System.EventArgs args)
	{
		// ** Place Event Handling Code Here **
		//Invoice = txtAddInvoice.Value.ToString();
		if (String.IsNullOrEmpty(txtAddInvoice.Text)) return;
		Invoice = txtAddInvoice.Value.ToString();
		//regex
		Regex regex = new Regex(@"^[0-9]{6}$");
		Match match = regex.Match(Invoice);
		if (match.Success)	
		{
			dtInput.Rows.Add(Invoice, printed);
		}
		else
		{
			MessageBox.Show("Enter a 6 digit invoice number");
		}
		txtAddInvoice.Clear();
		txtAddInvoice.Focus();
	}

	private void btnRemove_Click(object sender, System.EventArgs args)
	{
		// ** Place Event Handling Code Here **	
		
		if (String.IsNullOrEmpty(txtAddInvoice.Text)) return;
		Invoice = txtAddInvoice.Value.ToString();					
		for(int i = dtInput.Rows.Count-1;i>=0;i--)//backwards loop 
		{
			DataRow dr = dtInput.Rows[i];
				if (dr["Invoice"].ToString() == Invoice)
				{
					dr.Delete();//will remove any duplicate entries, so be careful
				} 
		}	
		grdInput.Refresh();
		txtAddInvoice.Clear();
	}

	private void btnPrint_Click(object sender, System.EventArgs args)
	{
		// ** Place Event Handling Code Here **
		txtNumberInvoicesSent.Value = 0;
		if (dtInput.Rows.Count<1){ MessageBox.Show("No Invoices Selected for Print"); 
		return;}
		EpiDataView dvRP = (EpiDataView)oTrans.EpiDataViews["ReportParam"];
		string workID = oTrans.WorkStationID.ToString();
		try {
			foreach (DataRow dr in dtInput.Rows)
			{
				dvRP.dataView[dvRP.Row]["InvoiceNum"] = dr["Invoice"];
				dvRP.dataView[dvRP.Row]["AutoAction"] = "SSRSPREVIEW";
				dvRP.dataView[dvRP.Row]["WorkstationID"] = workID;
				dr["Printed"] = true;
				oTrans.Update();					
				oTrans.SubmitToAgent("SystemTaskAgent", 0, 0);
				Thread.Sleep(500);
			}
			
			txtNumberInvoicesSent.Value = dtInput.Rows.Count;
			oTrans.PushDisposableStatusText("Reports Submitted for Preview...", true);
		}
		catch (Exception ex)
		{
		MessageBox.Show(ex.Message);
		}
	}

	private void txtAddInvoice_Validated(object sender, System.EventArgs args)
	{
		// ** Place Event Handling Code Here **		
	}

	private void btnClear_Click(object sender, System.EventArgs args)
	{
		// ** Place Event Handling Code Here **
		dtInput.Clear();
		txtNumberInvoicesSent.Clear();
	}
}

Rpt.ARInvForm.ARInvForm_Customization_MassPrintSelectInvoice_CustomExport.xml (40.8 KB)

1 Like

If you have APM - you could create a Break Routing, and break on UD field added to the InvcHead table. That UD field would be updated by a dashboard used to select the invoices to print. A Condition widget in the Break/Routing could be used to filter out the invoices you didn’t select

Then in the BPM’s autoprint widget set:

  • the CustList, as all the CustNums (ex: "1~33~5661~4490")
  • the BeginNum to the MIN of invoices selected.
  • the EndNum to the MAX of invoices selected.

While the above selects any invoice in the range BeginNum to EndNum AND with the custnum in the CustList,(which includes Invoice you don’t want), the break rout would break it into different printjobs based on the UD filed. And a condition widget could throw away the ones not to be printed.

The following makes one print task,

image

but generates two separate reports.

image

Changing the widget connected to the FALSE output of the condition, to a Gen only shows that the “unselected” were only generated and not printed.

image

Have nothing connected to the FALSE, and you’ll only get the invoices you selected.

1 Like

And if you don’t want to update the InvcHead table with a UD field. Use a UD table to hold the desired invoice numbers. Then add it to the ARForm RDD, with a definition relationsship.

And that shouldn’t even need the Break Routing. The RDD output should be limited to the invoices in the UD table.

Thanks for the ideas everyone. I think I’m going to adjust my REST call to pass the parameters in using a date range, invoice status option, and customer list. I wish there were a way to choose just pass in an invoice list since I would like the flexibility to merge the invoices the user selects into one PDF but this will suffice.

Sooooo, have I ever mentioned Epicor Functions? :slight_smile:

Create a function that accepts a json string in a dataset format. Something like:

{
  "Invoices": [
    {
      "id": 100000
    },
    {
      "id": 100001
    }
  ]
}

Add a Reference to Newtonsoft.Json.

image

Create a function that accepts the Json string.

Make sure to add a using for the Newtonsoft library:

image

Declare a DataSet variable to hold your invoices:

Add the one line of code that deserializes your Json into the DataSet variable.

Now you have your invoice numbers in the dataset, so you can use @Aaron_Moreng’s code to do the actual printing.

			foreach (DataRow dr in dtInput.Rows)
			{
				dvRP.dataView[dvRP.Row]["InvoiceNum"] = dr["Invoice"];
				dvRP.dataView[dvRP.Row]["AutoAction"] = "SSRSPREVIEW";
				dvRP.dataView[dvRP.Row]["WorkstationID"] = workID;
				dr["Printed"] = true;
				oTrans.Update();					
				oTrans.SubmitToAgent("SystemTaskAgent", 0, 0);
				Thread.Sleep(500);
			}

You would need to add the references to do the SubmitToAgent instead of the oTrans but that’s left as an exercise for the printer, er, reader. Of course, 10.2.500+ required…

The tricky part is if you need to retrieve the document locally and not using APR or something to send them directly from the TaskAgent. One problem at a time…

Mark W.

9 Likes

To follow up on Marks post, If you wanted to retrieve the document locally using REST, this could easily be accomplished using a BAQ.

Simply stated, Epicor stores the document blob in the DB once generated in the table SysRptLst
Field: [SysRptLst].[RptData]

This is the report bytes so if you run this BAQ from REST you’ll get a base64encoded bytestream which can / should be able to easily turn into a PDF document.

As a test you could run the BAQ and paste the output of the RptData (base64 encoded) here

and it should generate your PDF. You can do the same in code with javascript (or whatever language of your choice)

5 Likes

Thanks that looks pretty cool. I am on 10.2.300 so looking forward to the day we can upgrade so I can utilize Epi-Functions.

Thanks Jose! That is what I’m doing, grabbing the RptData via a BAQ and downloading it as a PDF. The usability issue is that there is an AJAX call that is pulling the PDF into an iframe while the user waits for the task agent to submit and run the report. The user is willing to wait 10 seconds or so for one task to complete but when there are multiple invoices, they want to pick and choose them and download as one file. I was hoping to submit them all as a single task to the task agent to reduce the time it takes.

1 Like

You can also run the invoices by just doing a SQL INSERT into the ICE.SysAgentSched, ICE.SysAgentTask and ICE.SysAgentTaskParam tables. The nice thing with this is you can use as much SQL as you want to build the report parameters and run them on a schedule with a SQL Job or a Windows Scheduled Task.

Yeah that makes sense we created basically the equivalent of System Monitor in our web app to queue the Invoice Jobs and allow the user to continue working while epicor does it’s magic.
Then we notify the user when the job is done

2 Likes

That looks pretty slick! All I have is a spinning circle but one day hope to catch up to this type of interface :smiley:

1 Like

There isn’t a ton of “magic” or anything supper special here, just a simple Angular App but can be done in any framework witb just a bit of javascript

basically make an async call to the REST to Print the Report

Add the resulting TaskNum to a List (queue) that we loop through every X seconds and ask Epicor about the status.
When Epicor replies (Via REST BAQ) that it’s ready we just fetch the document bits and notify the user

1 Like

That is Kendo right?

Yes the Grid is Kendo UI for Angular (cough Kinetic Inspired cough).

The Job Queuing is a custom component we wrote. Basically uses a Styled DropDown with a bit of JavaScript.

1 Like

Yeah I think I recognized the orange/dark blue they use in their control demos in their website, basically kinetic, use epicor blue and you can say it is kinetic :stuck_out_tongue: