Search code examples
project-serverpsi

Project Server Planned Work Does not update


I'm developing a Project Server 2010 Timesheet importer. It works just fine with Actual Hours in the Project Web App (they import).

The problem

The problem is the client needs the planned work updated accordingly to the entered actual work.
After calling timesheetClient.QueueUpdateTimesheet() it DOES NOT update planned work based on the Project Server automatic work planning feature :<

If you look at the screenshot provided there are 22 and 10 hours, but not "Zaplanowana" -> Planned Value!

If you just click the text and enter them manually, the planned work appears. PWA visualization

I can change timesheetDs.TS_ACT_PLAN_VALUE manually, but i don't want to write a complicated work planning algorithm by hand (it is really complicated in the Project world, trust me).

actual.TS_ACT_VALUE = 1000 * 60 * hours;
actual.TS_ACT_PLAN_VALUE = ???;

Two possible solutions

1. hacking the PWA Timesheet JSGrid

(Javascript controll) by trying to emulate user editing the rows manually which is "quite" hard

window.timesheetComponent.get_TimesheetSatellite().GetJsGridControlInstance()

2. Trying to use the same webservice which is used by PWA Timesheet Control

The problem is that it is undocumented.
We could (in theory) call TimeSheetSendGridUpdatesForSave Action on /pwa/_vti_bin/PSI/ProjectServer.svc and the next call to retrieve the hours (ReadTimesheet via Official TimesheetClient) or TimeSheetGetTimesheetForGridJsonFromViewUid as a pwa js client is calling, would result in updated planned hours

That apprach has a provlem i didnt solved yet - how to authenticate - more on this on a second question How to Authenticate into Project Server SOAP Api outside projectserverservices.dll

keywords: planned work, planned hours, pwa plan, project server 2010, planowane godziny


Solution

  • DISCLAIMER FIRST

    Use at your own risk. This code is NOT PORTABLE, which means Microsoft is not obliged to inform you that something changed within their internal webservices.

    Working Solution

    Trying to use the same webservice which is used by PWA Timesheet Control The problem is that it is undocumented. We can call TimeSheetSendGridUpdatesForSave Action on /pwa/_vti_bin/PSI/ProjectServer.svc.

    How?

    First, you have to authenticate against PSI outside of standard dll. Problem described and solved by mylself HERE


    Then you need to call the webservice with data. For example we will use this request, which changes actual hours on my task to 12h. Following snippet is copied from Chrome DevTools -> 'Network' Tab when you change hours and click save on a actual PWA Timesheet client (in browser at http://servername/pwa/Timesheet.aspx?tsUID=06b92bf0-806e-44d5-8c94-616c50471920)

    <?xml version="1.0" encoding="UTF-8"?>
    <soap:Envelope
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
        <soap:Body>
            <TimeSheetSendGridUpdatesForSave
                xmlns="http://schemas.microsoft.com/office/project/server/webservices/PWA/">
                <jobUid>{ba875aee-a59f-4c8f-974e-cd5f21dff7b6}</jobUid>
                <tsUid>{06b92bf0-806e-44d5-8c94-616c50471920}</tsUid>
                <changesJson>[
      {
        "updates": [
          {
            "type": 2,
            "recordKey": "a81cc5f7-307a-46ce-a131-77e15468c29f",
            "fieldKey": "TPD_col2a",
            "newProp": { "dataValue": "720000", "hasDataValue": true }
          },
          {
            "type": 2,
            "recordKey": "a81cc5f7-307a-46ce-a131-77e15468c29f",
            "fieldKey": "TPD_col2t",
            "newProp": { "dataValue": "720000", "hasDataValue": true }
          }
        ],
        "changeNumber": 20
      }
    ]
    </changesJson>
                <viewOptionsJson>{"dateFormat":3,"workFormat":2,"durationFormat":7,"filterType":5,"loadViewProperties":false,"newTasks":[],"importTasks":[],"removedLines":[]}</viewOptionsJson>
            </TimeSheetSendGridUpdatesForSave>
        </soap:Body>
    </soap:Envelope>
    

    In SOAP UI in the Auth Tab you have to supply you credentials as a user which owns the timesheet. You cannot just make a request from admin account because PSI will return

    <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
       <s:Body>
          <s:Fault>
             <faultcode>s:Server</faultcode>
             <faultstring xml:lang="pl-PL">ProjectServerError(s) LastError=GeneralInvalidOperation Instructions: Pass this into PSClientError constructor to access all error information</faultstring>
             <detail>
                <errinfo>
                   <general>
                      <class name="Nie można wprowadzić zmian w grafiku. Grafik nie został przesłany lub został odwołany.">
                         <error id="20011" name="GeneralInvalidOperation" uid="36ec16f4-db82-4fee-8922-2cd8d4084829"/>
                      </class>
                   </general>
                </errinfo>
             </detail>
          </s:Fault>
       </s:Body>
    </s:Envelope>
    

    That just works as on the gif below how planned hours are updated in the project server browser client

    Update 27.08.2019

    Code for a call:

    [TestMethod]
            public void SampleChangeHoursInPlainDotNetWebClient()
            {
                var httpClient = new WebClient();
                httpClient.UseDefaultCredentials = true; // you have to be in a timesheets user scope (windows auth, ntlm)
                httpClient.Headers.Add("SOAPAction", "http://schemas.microsoft.com/office/project/server/webservices/PWA/TimeSheetSendGridUpdatesForSave");
                httpClient.Headers.Add("Content-Type", "text/xml; charset=UTF-8");
                httpClient.Headers.Add("Accept-Language", "en-US,en;q=0.9,pl;q=0.8");
                httpClient.Headers.Add("Accept-Encoding", "gzip, deflate");
                httpClient.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED", "f");
                httpClient.Headers.Add("AsmxRoutedCall", "true");
                httpClient.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36");
                var data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><TimeSheetSendGridUpdatesForSave xmlns=\"http://schemas.microsoft.com/office/project/server/webservices/PWA/\"><jobUid>{ad8bd458-564c-49c5-8955-dfa577556d4a}</jobUid><tsUid>{6856cac1-7c88-4020-bdb5-c4e1b1a54b85}</tsUid><changesJson>[{\"updates\":[{\"type\":2,\"recordKey\":\"f88b5297-1985-4c2c-8717-554167471e81\",\"fieldKey\":\"TPD_col0a\",\"newProp\":{\"dataValue\":\"300000\",\"hasDataValue\":true}},{\"type\":2,\"recordKey\":\"f88b5297-1985-4c2c-8717-554167471e81\",\"fieldKey\":\"TS_LINE_STATUS\",\"newProp\":{\"dataValue\":\"0\",\"hasDataValue\":true,\"localizedValue\":\"Nieprzes%u0142ane\",\"hasLocalizedValue\":true}},{\"type\":2,\"recordKey\":\"f88b5297-1985-4c2c-8717-554167471e81\",\"fieldKey\":\"TPD_col0t\",\"newProp\":{\"dataValue\":\"300000\",\"hasDataValue\":true}}],\"changeNumber\":2},]</changesJson><viewOptionsJson>{\"dateFormat\":3,\"workFormat\":2,\"durationFormat\":7,\"filterType\":5,\"loadViewProperties\":true,\"newTasks\":[],\"importTasks\":[],\"removedLines\":[]}</viewOptionsJson></TimeSheetSendGridUpdatesForSave></soap:Body></soap:Envelope>";
                var response = httpClient.UploadString("http://preprod2010/pwa/_vti_bin/PSI/ProjectServer.svc", "POST", data);
            }
    

    Using RestSharp:

    [TestMethod]
    public void SampleChangeHours()
    {
        var client = new RestClient("http://preprod2010/pwa/_vti_bin/PSI/ProjectServer.svc");
        var request = new RestRequest(Method.POST);
        request.AddHeader("cache-control", "no-cache");
        client.Authenticator = new NtlmAuthenticator(new NetworkCredential("test1","Start123!@#","CORPNET"));
        request.AddHeader("Host", "preprod2010");
        request.AddHeader("SOAPAction", "http://schemas.microsoft.com/office/project/server/webservices/PWA/TimeSheetSendGridUpdatesForSave");
        request.AddHeader("Connection", "keep-alive");
    
        request.AddHeader("Content-Type", "text/xml; charset=UTF-8");
        request.AddHeader("Accept-Language", "en-US,en;q=0.9,pl;q=0.8");
        request.AddHeader("Accept-Encoding", "gzip, deflate");
        request.AddHeader("X-FORMS_BASED_AUTH_ACCEPTED", "f");
        request.AddHeader("AsmxRoutedCall", "true");
        request.AddHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36");
        request.AddParameter("undefined", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><TimeSheetSendGridUpdatesForSave xmlns=\"http://schemas.microsoft.com/office/project/server/webservices/PWA/\"><jobUid>{ad8bd458-564c-49c5-8955-dfa577556d4a}</jobUid><tsUid>{6856cac1-7c88-4020-bdb5-c4e1b1a54b85}</tsUid><changesJson>[{\"updates\":[{\"type\":2,\"recordKey\":\"f88b5297-1985-4c2c-8717-554167471e81\",\"fieldKey\":\"TPD_col0a\",\"newProp\":{\"dataValue\":\"300000\",\"hasDataValue\":true}},{\"type\":2,\"recordKey\":\"f88b5297-1985-4c2c-8717-554167471e81\",\"fieldKey\":\"TS_LINE_STATUS\",\"newProp\":{\"dataValue\":\"0\",\"hasDataValue\":true,\"localizedValue\":\"Nieprzes%u0142ane\",\"hasLocalizedValue\":true}},{\"type\":2,\"recordKey\":\"f88b5297-1985-4c2c-8717-554167471e81\",\"fieldKey\":\"TPD_col0t\",\"newProp\":{\"dataValue\":\"300000\",\"hasDataValue\":true}}],\"changeNumber\":2},]</changesJson><viewOptionsJson>{\"dateFormat\":3,\"workFormat\":2,\"durationFormat\":7,\"filterType\":5,\"loadViewProperties\":true,\"newTasks\":[],\"importTasks\":[],\"removedLines\":[]}</viewOptionsJson></TimeSheetSendGridUpdatesForSave></soap:Body></soap:Envelope>", ParameterType.RequestBody);
        IRestResponse response = client.Execute(request);
    }