In my previous blog post, I introduced the concept of downloading (complex) data directly into Excel using passthru X++ [https://community.dynamics.com/ax/b/dynamicsax_wpfandnetinnovations/archive/2013/03/23/downloading-data-into-excel-using-passthru-x.aspx#.UVVc0xzJT3Q].
In this article, I’m now going to go the “other-way” by uploading-and-processing (complex) data from Excel (again using passthru X++). I’ve decided to specifically tackle the age-old problem of “uploading-journals”. There are a number of commercial tools and utilities already out there that will do this in AX 2012 but lets go ahead and write our own (free version).
I’m going to use some readily available X++ code that creates and posts a journal. This will be my template when it comes to building the dynamic X++. Here’s a sample:
Post a journal that transfers 100 GBP from account 22333001 to account 22200001 (with dimensions) |
str PostLedgerJournal() { AxLedgerJournalTable header; AxLedgerJournalTrans trans; LedgerJournalCheckPost ledgerJournalCheckPost; container ledgerDimensions, offsetDimensions; str status = ''; int i; try { ttsbegin; header = new AxLedgerJournalTable(); header.parmJournalName('GEN'); header.parmName('General journal uploaded from Excel'); header.parmCurrencyCode('GBP'); header.save(); status += 'Created journal: ' + header.parmJournalNum(); trans = new AxLedgerJournalTrans(); trans.parmTransDate(str2Date('2012/12/03', 321)); trans.parmAccountType(LedgerJournalACType::Ledger); trans.parmJournalNum(header.ledgerJournalTable().JournalNum); ledgerDimensions = ['22333001-00000065-COR', '22333001', 2, 'CostCenter', '00000065', 'Department', 'COR']; trans.parmLedgerDimension(AxdDimensionUtil::getLedgerAccountId(ledgerDimensions)); trans.parmAmountCurCredit(100); trans.parmTxt('Test 1'); offsetDimensions = ['22200001-00000065-COR', '22200001', 2, 'CostCenter', '00000065', 'Department', 'COR']; trans.parmOffsetLedgerDimension(AxdDimensionUtil::getLedgerAccountId(offsetDimensions)); trans.save(); ttscommit; ledgerJournalCheckPost = LedgerJournalCheckPost::newLedgerJournalTable(LedgerJournalTable::find(header.parmJournalNum()), NoYes::Yes); ledgerJournalCheckPost.run(); status += ' | Posted journal: ' + header.parmJournalNum(); } catch (Exception::Error) { ttsAbort; status += ' | Failed to post, reversing updates'; for (i=1; i<=infologLine(); i++) { status += ' | ' + infolog.text(i); } } return status; } |
This will create and post the following journal in AX:
· Using these standard framework classes will provide all the validation as well as providing the flexibility to handle multiple dimensions.
Next we need to create a new Excel project within Visual Studio and add the “dynamic-compile-and-execute” Class Library that I created in my previous post: [https://community.dynamics.com/ax/b/dynamicsax_wpfandnetinnovations/archive/2013/03/10/limitations-of-the-office-connector.aspx#.UVV8axzJT3Q]
The structure of the spreadsheet should mimic the journal in AX and look something like this:
· The number of columns in your design can vary depending on the number of dimensions required.
In the code-behind for the “Upload” button you want to paste the following .Net code:
privatevoid Upload_Click(object sender, EventArgs e) { if (!validateJournal()) { MessageBox.Show("Journal has not passed validation yet","GL Upload"); return; } Excel.Worksheet activeSheet = ((Excel.Worksheet)Application.ActiveSheet); int currentRow = startRow; Excel.Range message = activeSheet.get_Range("Message"); Excel.Range journalName = activeSheet.get_Range("JournalName"); Excel.Range journalCurrency = activeSheet.get_Range("JournalCurrency"); Excel.Range currentCell = (Excel.Range)activeSheet.Cells[currentRow, dateCol]; script = ""; script += "str PostLedgerJournal()\r\n"; script += "{\r\n"; script += "\tAxLedgerJournalTable header;\r\n"; script += "\tAxLedgerJournalTrans trans;\r\n"; script += "\tLedgerJournalCheckPost ledgerJournalCheckPost;\r\n"; script += "\tcontainer ledgerDimensions, offsetDimensions;\r\n"; script += "\tstr status = '';\r\n"; script += "\tint i;\r\n"; script += "\ttry\r\n"; script += "\t{\r\n"; script += "\t\tttsbegin;\r\n"; script += "\t\theader = new AxLedgerJournalTable();\r\n"; script += "\t\theader.parmJournalName('" + journalName.Value + "');\r\n"; script += "\t\theader.parmName('General journal uploaded from Excel');\r\n"; script += "\t\theader.parmCurrencyCode('" + journalCurrency.Value + "');\r\n"; script += "\t\theader.save();\r\n"; script += "\t\tstatus += 'Created journal: ' + header.parmJournalNum();\r\n"; string currentCellValue = ""; currentCellValue = currentCell.Value2.ToString(); while (currentCellValue != "") { Excel.Range dateCell = (Excel.Range)activeSheet.Cells[currentRow, dateCol]; DateTime dateCellValue = DateTime.Now; try { dateCellValue = dateCell.Value; } catch { }; script += "\t\ttrans = new AxLedgerJournalTrans();\r\n"; script += "\t\ttrans.parmTransDate(str2Date('" + dateCellValue.ToString("yyyy/MM/dd") + "', 321));\r\n"; script += "\t\ttrans.parmAccountType(LedgerJournalACType::Ledger);\r\n"; script += "\t\ttrans.parmJournalNum(header.ledgerJournalTable().JournalNum);\r\n"; int dimensionCount = 0; string mainAcct = ""; try { mainAcct = activeSheet.Cells[currentRow, mainAcctCol].Value; } catch { }; string mainAcctCostCenter = ""; try { mainAcctCostCenter = activeSheet.Cells[currentRow, mainAcctCostCenterCol].Value; } catch { }; string mainAcctDepartment = ""; try { mainAcctDepartment = activeSheet.Cells[currentRow, mainAcctDepartmentCol].Value; } catch { }; string mainAcctDisplay = ""; string mainAcctDimensions = ""; string offsetAcct = ""; try { offsetAcct = activeSheet.Cells[currentRow, offsetAcctCol].Value; } catch { }; string offsetAcctCostCenter = ""; try { offsetAcctCostCenter = activeSheet.Cells[currentRow, offsetAcctCostCenterCol].Value; } catch { }; string offsetAcctDepartment = ""; try { offsetAcctDepartment = activeSheet.Cells[currentRow, offsetAcctDepartmentCol].Value; } catch { }; string offsetAcctDimensions = ""; string offsetAcctDisplay = ""; string description = ""; try { description = activeSheet.Cells[currentRow, descriptionCol].Value; } catch { }; if (!(mainAcctCostCenter == null || mainAcctCostCenter == "")) { mainAcctDimensions += ", 'CostCenter', '" + mainAcctCostCenter + "'"; dimensionCount++; } if (!(mainAcctDepartment == null || mainAcctDepartment == "")) { mainAcctDimensions += ", 'Department', '" + mainAcctDepartment + "'"; dimensionCount++; } mainAcctDisplay = mainAcct + (mainAcctCostCenter == "" ? "" : "-") + mainAcctCostCenter + (mainAcctDepartment == "" ? "" : "-") + mainAcctDepartment; script += "\t\tledgerDimensions = ['" + mainAcctDisplay + "', '" + mainAcct + "', " + dimensionCount.ToString() + mainAcctDimensions + "];\r\n"; script += "\t\ttrans.parmLedgerDimension(AxdDimensionUtil::getLedgerAccountId(ledgerDimensions));\r\n"; double creditAmount = 0; try { creditAmount = activeSheet.Cells[currentRow, creditCol].Value; } catch { }; double debitAmount = 0; try { debitAmount = activeSheet.Cells[currentRow, debitCol].Value; } catch { }; if (creditAmount != 0) { script += "\t\ttrans.parmAmountCurCredit(" + creditAmount.ToString() + ");\r\n"; } if (debitAmount != 0) { script += "\t\ttrans.parmAmountCurDebit(" + debitAmount.ToString() + ");\r\n"; } script += "\t\ttrans.parmTxt('" + description + "');\r\n"; if (!(offsetAcct == null || offsetAcct == "")) { dimensionCount = 0; if (!(offsetAcctCostCenter == null || offsetAcctCostCenter == "")) { offsetAcctDimensions += ", 'CostCenter', '" + offsetAcctCostCenter + "'"; dimensionCount++; } if (!(offsetAcctDepartment == null || offsetAcctDepartment == "")) { offsetAcctDimensions += ", 'Department', '" + offsetAcctDepartment + "'"; dimensionCount++; } offsetAcctDisplay = offsetAcct + (offsetAcctCostCenter == "" ? "" : "-") + offsetAcctCostCenter + (offsetAcctDepartment == "" ? "" : "-") + offsetAcctDepartment; script += "\t\toffsetDimensions = ['" + offsetAcctDisplay + "', '" + offsetAcct + "', " + dimensionCount.ToString() + offsetAcctDimensions + "];\r\n"; script += "\t\ttrans.parmOffsetLedgerDimension(AxdDimensionUtil::getLedgerAccountId(offsetDimensions));\r\n"; } script += "\t\ttrans.save();\r\n"; //script += "\t\tstatus += ' | Row " + currentRow.ToString() + " is OK, Created voucher: ' + trans.parmVoucher();\r\n"; currentRow += 1; currentCell = (Excel.Range)activeSheet.Cells[currentRow, dateCol]; currentCellValue = ""; try { currentCellValue = currentCell.Value2.ToString(); } catch { }; } script += "\t\tttscommit;\r\n"; Excel.Range autoPost = activeSheet.get_Range("AutoPost"); if (autoPost.Value == "Yes") { script += "\t\tledgerJournalCheckPost = LedgerJournalCheckPost::newLedgerJournalTable(LedgerJournalTable::find(header.parmJournalNum()), NoYes::Yes);\r\n"; script += "\t\tledgerJournalCheckPost.run();\r\n"; script += "\t\tstatus += ' | Posted journal: ' + header.parmJournalNum();\r\n"; } script += "\t}\r\n"; script += "\tcatch (Exception::Error)\r\n"; script += "\t{\r\n"; script += "\t\tttsAbort;\r\n"; script += "\t\tstatus += ' | Failed to post, reversing updates';\r\n"; script += "\t\tfor (i=1; i<=infologLine(); i++)\r\n"; script += "\t\t{\r\n"; script += "\t\tstatus += ' | ' + infolog.text(i);\r\n"; script += "\t\t}\r\n"; script += "\t}\r\n"; script += "\treturn status;\r\n"; script += "}\r\n"; // attempt compile and execute message.Value = "Running..."; Application.ScreenUpdating = true; string result = objPowerSQuirreLClass.runScript(script); message.Value = result; } |
The .Net routine scan the rows in the worksheet (starting at Row 10) looking for non-empty cells in the first column. If the cell is not empty then an attempt is made to replace all hard-coded elements with data from the cells in the Excel row. Essentially, whats happening here is that we are dynamically building up the X++ template at the top of this article.
When all the rows have been processed, we pass the “batch” as a string to our Dynamic X++ processing-class. This then attempts to compile and execute the code. If the batch is in error then “line-specific-errors” will be returned and displayed in a status cell, otherwise a successful posting message is displayed.
The solution will work for any currency and all GL posting types. Very powerful and slightly more flexible than the standard AIF journal upload service (which doesn’t allow you to auto-post): [http://msdn.microsoft.com/en-us/library/cc589855.aspx]
REGARDS
· [Correction to previous article]: the class that runs the passthru X++ must be set to RunOn Server only as code-access-permission is currently only be granted on that tier.