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 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.
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 = ???;
(Javascript controll) by trying to emulate user editing the rows manually which is "quite" hard
window.timesheetComponent.get_TimesheetSatellite().GetJsGridControlInstance()
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
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.
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
.
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
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);
}