04
Feb
10

#37 Simple Client calling a WS method in Deltek’s Open API

This is a simple method call to the Xtend61 API.  I am using Visual Studio 2008 & a simple legacy web service connection (Web Reference). To test I used a simple web form that has a button to invoke the call and some linq code to parse the response xml. I used the GetEmployeesByKey()  method to get name and phone number.

You have to add a web reference (right-click & add Web Reference) to the web service. The target url is http://<webserver>/vision/VisionWS.asmx.  You will have to authenticate to the target server. VS does a lot of work for you creating a proxy class which you’ll see under App_WebReferences. It also puts a small key reference in you web.config <appSettings>. That change and the proxy class will need to be copied to your production setup along with the new page.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Net;
using System.Xml.Linq;

public partial class Vision6Test : System.Web.UI.Page
{

 protected void Page_Load(object sender, EventArgs e)
 {

 System.Security.Principal.WindowsIdentity user = System.Security.Principal.WindowsIdentity.GetCurrent();  //operator

 if (IsPostBack)
 PagePostback();
 else
 PageInitialPost();

 }

 private void PageInitialPost()
 {
  Page.SetFocus(txtEmpNum);
 }

 private void PagePostback()
 {

 }

 protected void btnSubmit_Click(object sender, EventArgs e)
 {
 if (Utilities.hasNoData(txtEmpNum))             // check for no data inbound
 {
 lblMessage.Text = "Missing Employee Number";
 return;
 }

 string UserID = "someUser";
 string Password = "pwd";
 string Domain = "YourDomain";
 string DatabaseDescription = "Vision6";         // from Web link - see doc
 string IntegratedSecurity = "Y";                    // match ID setting in system

 vision6.DeltekVisionOpenAPIWebService proxy = new bwvision6.DeltekVisionOpenAPIWebService();        //Web Reference Proxy

 // Setup Parms
 string ConnInfoXml = @"<VisionConnInfo><databaseDescription>" + DatabaseDescription + @"</databaseDescription>";
 ConnInfoXml += @"<userName>" + UserID + @"</userName>";
 ConnInfoXml += @"<userPassword>" + Password + @"</userPassword>";
 ConnInfoXml += @" <integratedSecurity>" + IntegratedSecurity + @"</integratedSecurity>";
 ConnInfoXml += @"</VisionConnInfo>";

 string Keys = txtEmpNum.Text.Trim();              // inbound Employee ID from webpage
 string RecordDetail = "Primary";                        // send only primary record back

 // pass credentials on call - should use some dedicated account
 proxy.UseDefaultCredentials = true;
 proxy.Credentials = new NetworkCredential(UserID, Password, Domain);

 string WSResponse  = proxy.GetEmployeesByKey(ConnInfoXml, Keys, RecordDetail);

 // check empty response
 if (WSResponse == @"<RECS></RECS>" || Utilities.hasNoData(WSResponse))
 {
 string sEmp = txtEmpNum.Text.Trim();
 clearAll();
 lblMessage.Text = "Employee "  + sEmp + " Not Found";
 return;
 }
 string sXML = preProcessXML(WSResponse);
 lblResults.Text = parseXML(sXML, "FirstName");
 lblResults.Text += " " + parseXML(sXML, "LastName");
 lblResults.Text += " " + parseXML(sXML, "WorkPhone");
 lblResults.Text += @" x/" + parseXML(sXML, "WorkPhoneExt");
 }

 private string parseXML(string xml, string targetField)
 {
 string sReturn = "";
 XElement ele = XElement.Parse(xml);
 try
 {
 sReturn = ele.Elements().Descendants(targetField).First().Value;
 }
 catch (NullReferenceException nex) // drop nulls default to empty string
 {
 string sDrop = nex.ToString();
 }
 catch (Exception ex)
 {
 string sDrop = ex.ToString();   // you can throw a new exception or handle this
 }
 return (sReturn);
 }

 private string preProcessXML(string sXML)
 {
 sXML = sXML.Replace("&gt;", ">");
 sXML = sXML.Replace("&lt;", "<");
 return (sXML);
 }

 private void clearAll()
 {
 txtEmpNum.Text = "";
 lblMessage.Text = "";
 lblResults.Text = "";
 Page.SetFocus(txtEmpNum);
 }

 protected void btnClear_Click(object sender, EventArgs e)
 {
 clearAll();
 }
}
20
Jan
10

#36 RSS Feed from DB in a couple of minutes

I was reading this post and modified it to use my Data Access Layer (Enterprise Library) and morph my grid data into an easy rss feed.

I added in new line breaks with the normal  literal @”<br />” tag but here is a link if you have issues with that working.

Basically I created a page on the intranet and slapped the following code in:


using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Text;
using System.Data.SqlClient;

public partial class SRSFeed : System.Web.UI.Page
{

 protected void Page_Load(object sender, EventArgs e)
 {

 if (IsPostBack)
 PagePostback();
 else
 PageInitialPost();

 }

 private void PageInitialPost()
 {
 Response.Clear();
 Response.ContentType = "text/xml";
 XmlTextWriter rssFeed = new XmlTextWriter
 (Response.OutputStream, Encoding.UTF8);

 startFeed(rssFeed);
 processFeed(rssFeed);
 endFeed(rssFeed);

 }

 private void PagePostback()
 {

 }

 private void startFeed(XmlTextWriter rssFeed)
 {

 //writing RSS tags
 rssFeed.WriteStartDocument();
 rssFeed.WriteStartElement("rss");
 rssFeed.WriteAttributeString("version", "2.0");
 rssFeed.WriteStartElement("channel");
 rssFeed.WriteElementString("title", "Service Requests");
 rssFeed.WriteElementString("link", "http://someserver/link.aspx");
 rssFeed.WriteElementString("description", "Service Requests");
 }

private void processFeed(XmlTextWriter rssFeed)
 {
 DataSet ds = ServiceRequests.searchServiceRequest(null, null, null, null, null, null, null, true, true);
 DataTableReader dr = ds.CreateDataReader();
 StringBuilder sb = new StringBuilder();
 while (dr.Read())
 {
 rssFeed.WriteStartElement("item");
 rssFeed.WriteElementString("title", dr["SRSNumber"].ToString()  + " : " + dr["Subject"].ToString());
 sb.Length = 0; sb.Capacity = 0; // clear sb
 sb.Append("Summary: " + dr["Summary"].ToString());
 sb.Append(@"<br />  Status: " + dr["Status"].ToString());
 sb.Append(@"<br> Category: " + dr["Category"].ToString());
 sb.Append(@"<br> Submitted Date: " + ((DateTime)(dr["SubmittedDate"])).ToShortDateString());
 sb.Append(@"<br> Assigned To: " + dr["AssignedTo"].ToString());
 rssFeed.WriteElementString("description", sb.ToString());
 rssFeed.WriteElementString("link","http://portal/_layouts/Apps/SRS/editSRS.aspx?id=" + dr["ID"]);
 rssFeed.WriteElementString("pubDate", DateTime.Now.ToString());
 rssFeed.WriteEndElement();
 }
 dr.Close();
 dr.Dispose();

 }

 private void endFeed(XmlTextWriter rssFeed)
 {
 rssFeed.WriteEndElement();
 rssFeed.WriteEndElement();
 rssFeed.WriteEndDocument();
 rssFeed.Flush();
 rssFeed.Close();
 Response.End();

 }

}
19
Jan
10

#35 Project Planning Workflow Webservice Parsing Plan Info

Set up a simple web service  and code the following exposed WebMethod and private parsing routine.  On Deltek 6.1 you configure this by setting a reference to the WS (i.e. http://mywebserever/webservice.asmx?WSDL) and selecting the exposed method in the populated dropdown.  On Project Planning just add a custom button and map the WF call.  If you want to do some other processing and do not care about sending anything back send an empty string back to Deltek.


using System.Linq;
using System.Web.Services;
using System.Xml.Linq;
using System;

namespace WSJNG
{
 /// <summary>
 /// Summary description for Service1
 /// </summary>
 [WebService(Namespace = "http://someserver/testWS/")]
 [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
 [System.ComponentModel.ToolboxItem(false)]

public class Service1 : System.Web.Services.WebService
 {

/// <summary>
 ///  This takes in data from Deltek Workflow call which sends a serialized form of planning data and
 ///  sends back the Department Manager (Supervisor) as a Deltek Warning.
 ///  You could call a DB Proc or some other processing instead.
 /// </summary>
 /// <param name="sPlanInfo"></param>
 /// <returns></returns>
 [WebMethod]
 public string processPlanInfoSendWarning(string sPlanInfo)
 {
 string sXML = sPlanInfo.Replace("&gt;", ">");
 sXML = sXML.Replace("&lt;", "<");
 string DeptMgr = parseXML(sXML, "emSupervisor");
 return (@"<errors warning='y'><error>Department Manager For This Plan Is: " + DeptMgr + "</error></errors>");
 }

 private string parseXML(string xml, string targetField)
 {

 string sReturn = "";

 XElement ele = XElement.Parse(xml);

 try
 {
   sReturn = ele.Elements().Descendants(targetField).First().Value;
 }
 catch (NullReferenceException ex) // drop nulls default to empty string
 {
   string sDrop = ex.ToString();
 }

 return (sReturn);

 }
16
Dec
09

#34 Pending Deltek Contracts

Here is a Stored Procedure used to load a data grid (we use a Telerik RadGrid) with a list of groups and filtered Pending Contracts.

The proc accepts two parms that if null are ignored: (1) ClientCode could be ClientID but we use a simplier field called ClientCode & (2) ContractStatus which is what is the status colum dropdown is populated with.

Procedure [dbo].[BWSP_getPendingContracts]
(
 @ClientCode            varchar(32)
 ,@ContractStatus    varchar(50)
)
as
Select
 CL.Client
 , CL.Name
 , CT.*
 , EM.Email
 , 'MailTo:' + RTRIM(PM.Email)
 + RTRIM('&subject=Pending%20Contract&body=')
 + RTRIM('Client:')  + RTRIM(CL.Client) + '%0A'
 + RTRIM('Project:')  + RTRIM(PR.WBS1) + '%0A'
 + RTRIM('Status:')  + LTRIM(CT.custContract_Status) + '%0A'
 as PMMailTo
 , 'MailTo:' + RTRIM(CM.Email)
 + RTRIM('&subject=Pending%20Contract&body=')
 + RTRIM('Client:')  + RTRIM(CL.Client) + '%0A'
 + RTRIM('Project:')  + RTRIM(PR.WBS1) + '%0A'
 + RTRIM('Status:')  + LTRIM(CT.custContract_Status) + '%0A'
 as CMMailTo
 , ISNULL(RTRIM(EM.PreferredName),RTRIM(EM.FirstName))  + ' '+ RTRIM(EM.LastName) As Requestor
 , ISNULL(RTRIM(PM.PreferredName),RTRIM(PM.FirstName))  + ' '+ RTRIM(PM.LastName) As PMName
 , PM.Email as PMEmail
 , ISNULL(RTRIM(CM.PreferredName),RTRIM(CM.FirstName))  + ' '+ RTRIM(CM.LastName) As CMName
 , CM.Email as CMEmail
from dbo.Projects_ContractTracking CT
 Join PR on (PR.WBS1 = CT.WBS1 and PR.WBS2 = CT.WBS2 and PR.WBS3 = CT.WBS3)
 Join EM on (EM.Employee =  CT.custContract_RequestedBy)
 Join EM CM on (PR.Principal = CM.Employee)
 Join EM PM on (PR.ProjMgr = PM.Employee)
 Join CL on (CL.ClientID = PR.ClientID)
Where CT.WBS2 = ''
 and CT.custContract_Status in ('0-Draft being prepared','1-Pending (draft sent to Client)','2-Pending (final sent to Client)','6-On Hold-See Memo')
 and CL.Client = isNull(@ClientCode,CL.Client)
 and CT.custContract_Status = isNull(@ContractStatus, CT.custContract_Status)
Order By CT.WBS1
01
Dec
09

#33 Hyperlink error on SQL Reporting

It is a very nice option to link in a related update page to detail on report. I have used this on/off but since I have not done it in awhile I tripped over a dumb error. If you place a hyperlink in the navigation and append some querystring parm from your dataset – You need to call the ToString method at the end. I had a input report parm: Fields!SomeID.Value but without ‘.ToString‘ at the end and it does not create the link. No errors just nothing – adding the ToString method corrected the issue.

25
Nov
09

#32 Our Expense Notices

We send out simple email notices from information in Deltek’s database.

If you have the Enterprise Edition of SQL Server you can do this straight-up using Data Driven Subscriptions which is a really nice feature of SQL Reporting that allows you to use a query to determine who gets the report via email and what is in the report for each one sent. Here is a link to get you started: http://msdn.microsoft.com/en-us/library/ms169673.aspx

But since we do not have that license and option …  I am using an asp.net page & coding the two
queries needed for the Data Driven Subscriptons – (1) to get the target email address & (2) to get the report details (namely their expense report notes) as one simple query in a stored procedure.  The result is a custom email sent via SMTP to each person posting an expense report.

This is the extraction Stored Procedure:

Procedure [dbo].[getExpenseReceipts]
as
Select      EM.email
,               EM.Employee
,               EX.EmployeeName
,               EX.Amount
,               EX.CheckNo
,               EX.CheckDate
,               EX.ReportName
,               EX.ReportDate
from EM
Join exChecks EX on ( EM.Employee = EX.Employee )

On our intranet we have a grid that displays the pending check receipt data & a simple process button to approve. This page is secure to accounting personnel.

The process uses SMTP and data from the Stored Procedure above to form an individual email for each involved employee.

 private void sendNotifications()
    {

        string sTitle = "Expense Check Notification";
        string sMsg = "";
        string sSourceEmail = @"***@***";
        DataSet ds = DeltekUtilities.getPendingExpenseNotifications(); // ADO call to get data using the SP above
        DataTableReader  dr = ds.CreateDataReader();
        int ii = 0;
        while (dr.Read())
        {
            ii++;
            sMsg = "===================================================================================" + "\n";
            sMsg += "                    Expense Check Notification   " + "\n";
            sMsg += "===================================================================================" + "\n\n";
            //sMsg += "               Email: " + dr["email"].ToString() + "\n";
            sMsg += "            Employee: " + dr["EmployeeName"].ToString() + "\n";
            sMsg += "      Deposit Amount: " + String.Format("{0:c}", Double.Parse(dr["Amount"].ToString())) + "\n";
            sMsg += "        Deposit Date: " + String.Format("{0:d}", dr["CheckDate"]) + "\n";
            sMsg += "         Description: " + dr["ReportName"].ToString() + "\n";
            sMsg += "---------------------------------------------------------------------------------";

            try
            {
                Utilities.sendEmail(sSourceEmail, dr["email"].ToString().Trim(), string.Empty, sTitle, sMsg, false);
            }
            catch (Exception ex)
            {
                lblMessage.Text += "<br> error sending: " +  dr["email"].ToString();
            }
        }
        if (dr != null)
        {
            dr.Close();
            dr.Dispose();
        }

        lblMessage.Text += "<br>" +  ii.ToString() + " Emails Sent";

    }

// Send Email Utility Call

public static bool sendEmail(string sSourceEmail, string sTargetEmailList, string sCCEmailList, string sMsgTitle, string sMsg,  bool isHTMLemail)
    {

        bool bReturn    = false;
        SmtpClient client = new SmtpClient();
        client.Host = "****";
        MailMessage mm = new MailMessage();
        try
        {
            mm.Sender       = new MailAddress(sSourceEmail);
            mm.From         = new MailAddress(sSourceEmail);
            mm.To.Add (new MailAddress(sTargetEmailList));
            if (sCCEmailList.Length > 0)
                mm.CC.Add (new MailAddress(sCCEmailList));
            mm.Subject      = sMsgTitle;
            mm.Body         = sMsg;
            mm.IsBodyHtml   = isHTMLemail;

            client.Send(mm);
            bReturn = true;
        }
        catch (Exception ex)
        {
            string sEmails = " Email List [" + sSourceEmail + " " + sTargetEmailList + " " + sCCEmailList + "] ";
            throw new IntranetException(ex.Message + sEmails,
                                              ex,
                                              SystemConstants.Intranet_ErrorMessage);

        }

        return bReturn;
    }
26
Aug
09

#31 Linq Parsing – checking for missing XElement

I am loading an object fed from an XML document that expects a string for each element whether it exists or not. Since some elements are optional this bit of code helps return an empty string instead of getting a null exception. The method loadElement below checks for null and returns string.empty instead.

See http://www.switchonthecode.com/tutorials/introduction-to-linq-simple-xml-parsing for parsing code


private List<SampleObject> readParse( string FilePath)
{

XDocument xmlDoc = XDocument.Load(FilePath);

List< SampleObject > ObjectList  =
(from RANGEVARNAME in xmlDoc.Descendants("PARENTNODE")
select new SampleObject
{

item1     =  loadElement(RANGEVARNAME.Element("item1")),
item2     =  loadElement(RANGEVARNAME.Element("item2")),
. . .
}).ToList<SampleObject>();

lblMessage.Text = "Loading " + FilePath + " (" + ObjectList.Count + ") Records";

return (ObjectList);

}

 private string loadElement(XElement E)
{
  return((E==null)? string.Empty: E.Value);
} 
20
Jul
09

#30 Sharepoint: Accessing a Site Collection\SPWeb within a Workflow and updating a list

Below I am grabbing data off a Workflow with a browser enabled Infopath Form and loading a separate list.


public Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties workflowProperties = new Microsoft.SharePoint.Workflow.SPWorkflowActivationProperties(); 

  private void onWorkflowActivated1_Invoked(object sender, ExternalDataEventArgs e)
        {

              ...
                //Save the identifier for the workflow
                workflowId = workflowProperties.WorkflowId;

                // InitiationData comes from the initialization form
                XmlDocument document = new XmlDocument();
                document.LoadXml(workflowProperties.InitiationData);

                // Get Data Off Of Init Form
                XmlNamespaceManager ns = new XmlNamespaceManager(document.NameTable);
                ns.AddNamespace("my", "http://schemas.microsoft.com/office/infopath/2003/myXSD/2006-12-05T20:04:17");
                managerUsername = document.SelectSingleNode("/my:flowFields/my:managerUsername", ns).InnerText;
                managerFullname = document.SelectSingleNode("/my:flowFields/my:managerFullname", ns).InnerText;
                reviewType = document.SelectSingleNode("/my:flowFields/my:reviewType", ns).InnerText;
                reviewComments = document.SelectSingleNode("/my:flowFields/my:reviewComments", ns).InnerText; 

                using (SPSite siteCollection = workflowProperties.Site)
                {
                    using (SPWeb web = siteCollection.OpenWeb())
                    {
                        SPList EmployeeReviewList = (SPList)web.Lists["EmployeeReview"];                     // Specific List
                        SPListItem EmployeeReviewItem = OptimizedAddItem(EmployeeReviewList);       // Add New Item to List
                        EmployeeReviewItem["Review Name"] = reviewType;                                            // Update elements of new item
                        EmployeeReviewItem.Update();
                    }
                }  

  ... }

       //cf.  http://blog.robgarrett.com/2009/02/25/efficient-way-to-add-a-new-item-to-a-sharepoint-list
        public  SPListItem OptimizedAddItem(SPList list)
        {
            const string EmptyQuery = "0";
            SPQuery q = new SPQuery { Query = EmptyQuery };
            return list.GetItems(q).Add();
        }

Credit due for OptimizedAddItem(): http://blog.robgarrett.com/2009/02/25/efficient-way-to-add-a-new-item-to-a-sharepoint-list/

15
Jul
09

#29 PopUp under Deltek 6.x Smart Client

The issue is that by running a smart client an iframe or window open launches a browser window separate from the smart client which is a Windows client executable & not a browser app. Since that is unavoidable using a modal window with target self tag in the head works best. This method will however still leave the initial help window open in the smart client.

Add the Help Icon ? and replace the Help Text (directly via a control-V paste do not click on ellipse to launch editor) with:

<script>javascript:  window.showModalDialog("http://targetpage.aspx",'_parent', "dialogHeight: 200px;");</script>

In your target page (html or aspx) since this is a modal window this will ‘correct’ any postbacks from launching a new window.

<HTML>
                <HEAD>
                                <base target="_self" />
 ...

Put a button with a OnClick event to close this popup. The window.opener=’X';window.open(”,’_parent’,”);window.close(); will avoid the usual warning.

Credit Due on avoiding the window close warning: http://blogs.x2line.com/al/articles/350.aspx

					function CopyToClipboard()
					{
						document.frmVisible.txtJobNumber.focus();
						document.frmVisible.txtJobNumber.select();
						CopiedTxt = document.selection.createRange();
						CopiedTxt.execCommand("Copy");
						alert('Number has been copied. Use Control-V to paste into new project screen.');
						window.opener='X';window.open('','_parent','');window.close();
					}

cf . http://dmgorman.wordpress.com/2008/01/11/02-simple-popup-help-page/

26
Jun
09

#28 Deltek operator (agent) ID & email via Workflow

If you need to get the agent processing the workflow you can call the UDF GetVisionAuditUsername()

You can then wrap this UDF call in a SP to get something like an Email address from the EM table for email processing to the operator/agent which is something missing from Deltek’s WF options. Sending the person doing the operation (agent) an email has been missing in Deltek WF (you can send their manager an email but not the person doing the work). At least this is what we see in 5.1 and since 6.0 is same/same we probably have the same issue there – though I have not checked there. If anyone sees otherwise please post a comment. Thanks!


** Note: This will require the context provided by Deltek’s WF since the UDF uses ‘context_info’ from master.dbo.sysprocesses **

Create Procedure getAgentEmail()
AS
BEGIN
	Declare @Employee as varchar(20)

	Select @Employee = [dbo].[GetVisionAuditUsername]()

	Select EM.Email
        From EM
        Where EM.Employee = Substring(@Employee,1,3)
            -- substring if needed to get proper key

         -- It would be wise to parm these values in!
         EXEC msdb..sp_send_dbmail
            @profile_name='ProfileName'
           , @recipients= @Email
           , @subject='Test message'
           , @body='This is the body of the test message'

END

SQL 2008 Mail Setup
http://blog.sqlauthority.com/2008/08/23/sql-server-2008-configure-database-mail-send-email-from-sql-database/