If you need to display the first record's data of your group, you can use the following expression:
=First(Fields!FieldName.Value, "GroupName")
Dynamics AX solutions for issues I encounter as I develop for Dynamics AX - now Dynamics 365 Finance & Operations
If you need to display the first record's data of your group, you can use the following expression:
=First(Fields!FieldName.Value, "GroupName")
This can be tricky for sure. The most important point to be understood is that there has to be a parameter from the Main report that can be passed to the Subreport in the subreport Parameters.
From the main report design, you Right-click > Insert > Subreport.
Then you Right-click the Subreport to view Subreport Properties.
On the General tab, you must set the report that you want to use as the subreport. This should be in the format: Report.Design.
Here is an example:
The parameters tab is where the real magic happens.
First of all, you need three parameters that are standard for all (most?) ssrs reports. The "Name" column on this parameters screen should be thought of as the parameters from the Sub report. The "Value" column in this screen should be thought of as the parameters from the Main. The value parameters can be an actual parameter field or just a data field from your report. The Name will be the name of the parameter from the sub report. The Name drop-down will not be helpful to you. You will need to type in the name of the parameter to pass. The Value drop-down will also not be helpful to you. You can use the "fx" to build the expression. If you do this, you can choose the "Parameters" category and double click the parameter you want in the Values section:
Or you can choose a field from your dataset as the parameter for the report.
Or you can type them in the Values section of the Subreport properties screen like this:Name Value
AX_CompanyName [@AX_CompanyName]
AX_UserContext [@AX_UserContext]
AX_ReportContext [@AX_ReportContext]
Then you need to pass the parameter(s) that the report needs for it to run. My report was passing the AccountNum. The customer account number in the main report had parameter "CustListReportDS_AccountNum." The customer account number in the subreport had parameter [@DataSet1_Customer_account], so my parameter looked like:
Name Value
CustListReportDS_AccountNum [@DataSet1_Customer_account]
It will look like this when you are done:
Hope this helps!
For some reason, the external item field on these forms do not have lookups. If you want to set the external item manually, you would need a lookup on the field. Standard AX allows the user to type any string into the externalItemId field, it doesn't actually have to be in the external item table. We had a requirement to keep that functionality. If you wanted to limit the selection to only the items in the table you would need to add a relation on the salesLine, salesquotationline, and purchline tables to the CustVendExternalItem table and make sure validation is set to yes.
Doing this is not all that difficult. First you need to create a lookup on the custVendExternalItem table and then override the lookup on the form(s) you want to use it.
1. Here is the lookup code on the custVendExternalItem table
[ExtensionOf(tableStr(CustVendExternalItem))]
final class ModelCustVendExternalItem_Extension
{
public static void modelLookupExternalItem(FormControl _formControl, CustVendRel _accountNum, ItemId _itemid)
{
Query query = new Query();
QueryBuildDataSource qbds;
QueryBuildRange qbr;
SysTableLookup sysTableLookup = SysTableLookup::newParameters(tableNum(CustVendExternalItem),_formControl);
sysTableLookup.addLookupfield(fieldNum(CustVendExternalItem, ExternalItemId),true);
sysTableLookup.addLookupfield(fieldNum(CustVendExternalItem, ItemId));
sysTableLookup.addLookupfield(fieldNum(CustVendExternalItem, CustVendRelation));
qbds = query.addDataSource(tableNum(CustVendExternalItem));
qbds.addRange(fieldNum(CustVendExternalItem, custvendrelation)).value(_accountNum);
qbds.addRange(fieldNum(CustVendExternalItem, ItemId)).value(_itemid);
sysTableLookup.parmQuery(query);
sysTableLookup.performFormLookup();
}
I was hoping there was an easier way to do this, but this is how I ended up doing it.
For each numeric field that you want to show parentheses around a negative number, do the following.
1. Change the field to return the absolute value of your field:
For example: abs(@GrossTotal)
2. Make sure you go outside of the "field" definition and add a beginning parenthesis before it and an ending parenthesis after it.
3. Highlight the first parenthesis and go up to the top and choose the Docentric "IF" to put around it. Only show the parenthesis if the value of @GrossTotal is less than zero. @GrossTotal < 0.
4. Highlight the last parenthesis and go up to the top and choose the Docentric "IF" to put around it. Only show the parenthesis if the value of @GrossTotal is less than zero. @GrossTotal < 0.
When you "show tags" on the template, it should look like this:
Our customer is automating their invoice processing with a 3rd party program (AxTension). The invoices they receive from vendors often have only part of the purchase order number in them. Their current PO format is PO#######. Sometimes they just receive the numeric part, '1234567', sometimes the numeric part is missing the leading zeroes, '12345'. This kicks the PO numbers out as errors and the users would have to manually match them to the PO in the system.
I was asked to write logic that will "fix" the PO numbers that come in. First of all, if the PO number is blank, I just want to return (this avoids adding a po number for NON PO invoices):
if(_purchId == strMin())
{
return _purchId;
}
Then I had to find the number sequence format for the PurchId extended data type.
NumberSequenceReference Ref;
NumberSequenceDataType dataType;
NumberSequenceTable numSeq;
extendedTypeId poTypeId;
//Find the EDT id for your extended data type (mine is PurchId)
poTypeId = extendedTypeNum(PurchId);
select firstOnly RecId from dataType
where dataType.DatatypeId == poTypeId
join numbersequenceid from Ref
where Ref.numbersequencedatatype == datatype.recid|
join Format from numSeq
where numSeq.recid == Ref.numbersequenceid;
Once I have the numberSequenceTable record and the format field, I can use it to check the purchId that was passed into the system. I also did not want to show the user any errors so I store the length of the info log before and clear it after calling in to the numCheckFormat() method run a check on my format.
When you incorporate this into your class(es), you can have the user pass in the purchId and return the newPurchId you have fixed (public static PurchId validatePurchId(_purchId)). When you run it as a job, you can create the PurchId purchId variable and set it to the purchId you want to "fix."
int lines;
purchid purchid, newpurchid;
//Save current infolog spot
lines = infolog.line();
// Check to see if purchid matches format
if(!NumberSeq::numCheckFormat(purchID,numSeq))
{ // if check fails, fix purchid
//Cut any infolog messages that were created
infolog.cut(lines ? lines : 1);
//loop through the format
newpurchid = purchid;
for(i=1; i <= strLen(numseq.format); i++)
{
if(i > strLen(_purchId))
{ // if the purchid is longer than the format, we don't need to modify it (this only adds)
break;
}
f = subStr(numSeq.format,i,1);
p = substr(purchId,i,1);
if (f != '#')
{ // if we aren't looking for a number in the format
if(f != p)
{ // insert the character if it is in the format but not the purchid
if(strLen(purchId) < strLen(numSeq.Format))
{
//insert character into string
purchId = strIns(purchId, f, i);
}
//fix issue where readsoft reads the letter O as a numeric 0
else if (p == '0')
{ //Overwrite character in the string
purchId = strPoke(purchId, f, i);
}
} // END if f!=p
} // END if f!= '#'
} // END for
// After inserting the characters that exist in the format but not in the purchid, check the length
// if it's not long enough, we want to insert 0s before the existing numbers in the purchid
if(strLen(newpurchid) < strLen(numSeq.format))
{
zeroPos = strNFind(numSeq.Format,'#',strLen(numSeq.Format),-strLen(numSeq.Format));
newPurchId = strIns(newpurchid,strRep('0',strLen(numSeq.format)-strLen(newpurchId)),zeroPos+1);
}
You could also add logic that deletes unwanted characters from the purchid if it's not in the format, but we were assuming that wasn't going to happen. If it did happen, we would actually need user interaction to figure out if it matched with a PO in the system.
If you want an easy way to test this, you can create a dialog that allows you to enter your PurchId and your Format and ensure that the logic above will modify the PurchId the way you intend.
I added this to the job to give me a dialog:
Dialog dialog = new Dialog("Purchase order number");
DialogField dlgpurchid,dlgFormat;
dlgpurchid = dialog.addField(extendedTypeStr(str60),"Num to validate");
dlgFormat = dialog.addField(extendedTypeStr(NumberSequenceFormat));
dlgFormat.value("PO#######"); // Set my current known number sequence as a starting point
if(dialog.run())
{
purchid = dlgPurchId.value();
// code to select the num seq record from the table can go here
//After selecting the numsequencetable record, you would need to store the format from your dialog
numSeq.format = dlgFormat.value();
// code to fix the purchid can go here
}
This next section specifically references where to put the code in AxTension to adjust the PurchId. It has to be done on the header and the lines (as you can pass invoice number per line with AxTension).
1. Add method to AXTip_ImportPurchInvoices that will receive the purchId, fix it, and return the new purchId
2. Add code in method addInvoiceLine() where it calls the setField() for #Field_Line_PurchId. Replace it with this:
this.setField(#Field_Line_PurchId, this.YOURMETHOD(getFieldorDefaultValue(this.getField(#Field_Line_PurchId, ''), this.getField(#Field_PurchId, ''))));
3. Add code in method handlePurchInvoice(). After the if(importRunId), add this line of code:
this.setfield(#Field_PurchId,this.YOURMETHOD(this.getField(#Field_PurchId)));
4. Add code in class AXTip_ImportInvoices_ReadSoftOnline. This extends a class that extends the earlier class where you put your new method to fix the purchId. Add code in the analyzeFile() method.
Set default Id with your new logic.
defaultPurchId = this.YOURMETHOD(this.getfield(#Field_PurchId, ''));
5. In the readsoftonline class in #4, add code in the setHeaderFields() method:
change the this.setField() for the #Field_PurchId to call your new method instead
this.setfield(#Field_PurchId, this.YOURMETHOD(this.marshalStr(documentData.get_PurchId(), identifierStr(PurchId))));
Hope this helps!
-Amber
I have a report type parameter that determines which parameters the report needs to run.
I created a new enum for the Report type, available options are Job Number or Invoice Number.
If the job number is selected there will be a start job number and end job number with labels "Starting number" and "Ending number." If Invoice Number is selected there will be a start invoice number and an end invoice number with the exact same labels.
Since the job number is the first enum, I decided to build the parameters as if Job number were selected. However, if the user selects invoice number and that is stored in usage data, the parameter screen would need to accommodate that and show the correct parameters based on the invoice number report type selection.
I created a UI Builder class to accomplish this. My report is already using a contract class to define the 5 parameters I need for the report, a controller class, and a DP class to process the report. In the build() method of the UIBuilder class, I add all the dialog fields I will need and then I set visible = false for the invoice start and end fields.
In the postRun() method (before the super()), I check the value of the reportType enum. PostRun() gets called after the usage data is stored in the field(s). If the reportType is InvoiceNumber, I set the jobstart/end fields to visible = false and the invoicestart/end fields to visible = true.
I also had to override the modified method of the reportType parameter in order to change the parameters shown when the user modifies the report type. For this, I used the postBuild() method tell the system to override the modified() method and where to go to find the new logic for this method.