Time and Expense Rest Call

Heyyyy!

Im back :sweat_smile:

I’m still working on this API to bring nonwork hours (pto) from one database (UKG) to Kinetic. I’ve made it to the point where all of my API calls are successful, the laborhed row is added, i’ve gotten my new labordtl sequence number, etc. The issue that i’m having now is getting the labordtl row to show up.

I made sure to follow the trace. I have RowMod set to A for add in the GetNewLaborDtl call and made it blank for the final Update call (as seen by the trace). I also tried it with A or U, but both of those gave 400 errors on the update call. I even compared the laborhed update to see if I was doing something different, but they’re pretty much the same.

Any other suggestions as to what I may need to check for the labordtl row to actually show up?

Appreciate any help! <3

2 Likes

You should have a ResponseError from your request, it normally has more detail than just 400, what does that show?

the response is 200, successful.

How are you getting 200 and 400 at the same time? :sweat_smile:

not at the same time. I was saying that when i tried to change the RowMod to see if that would allow the record to show it gave 400. However, the original way (empty RowMod) gives a 200 response. The issue is that even though all the calls are successful, the LaborDtl row is not added. The laborHed row is visible, though.

I apologize for the lack of clarity

So when you try to add the row mod and you get the 400, what error message does the ResponseError object contain?

it says that you cannot update or modify (depending on if rowmod is A or U) a record that has been submitted for approval. When i change the bool that enable’s submit, the error message says something like “you cannot modify/update a record that has NOT been submitted”. Go figure right?

However, when I follow the guidance given by the Trace log and leave rowmod blank, the result is 200.

hmm, mind posting your code?

sure. Theres a lot, so I’ll pick what I believe is most relevant.

   public static JObject GetNewLaborDtlWithHdr(JObject completedRecord)
        {
            Console.WriteLine("Step 19:");
            Console.WriteLine("Get new Labor Detail row");
            Console.WriteLine("");

            JArray laborDtlArray = (JArray)completedRecord["ds"]["LaborDtl"];
            JArray laborhedArray = (JArray)completedRecord["ds"]?["LaborHed"];

            if (laborDtlArray.Count > 0)
            {
                JObject newLaborDtl = (JObject)laborDtlArray[0];

                // Find the object with the highest LaborHedSeq
                JObject highestLaborHedSeqObj = (JObject)laborDtlArray
                    .OrderByDescending(obj => (long)obj["LaborHedSeq"])
                    .First();

                for (int i = 0; i < laborDtlArray.Count; i++)
                {
                    // labordtl rowmod value "A"
                    JObject obj = (JObject)laborDtlArray[i];

                    if (obj == highestLaborHedSeqObj)
                    {
                        obj["LaborHedSeq"] = laborhedArray[0]["LaborHedSeq"];

                        if (obj["RowMod"]?.ToString() == "")
                        {
                            obj["RowMod"] = "A";
                        }
                    }
                    else
                    {
                        // All other rowmod values ""
                        obj["RowMod"] = "";
                    }
                }

                completedRecord.Add("ipClockInDate", newLaborDtl["AppointmentStart"]);
                completedRecord.Add("ipClockInTime", newLaborDtl["ClockinTime"]);
                completedRecord.Add("ipClockOutDate", newLaborDtl["AppointmentEnd"]);
                completedRecord.Add("ipClockOutTime", newLaborDtl["ClockOutTime"]);
                completedRecord.Add("ipLaborHedSeq", newLaborDtl["LaborHedSeq"]);
            }

            // Update RowMod property in laborhedArray
            if (laborhedArray != null && laborhedArray.Count > 0)
            {
                // all laborhed rowmod values ""
                foreach (JObject obj in laborhedArray.Cast<JObject>())
                {
                    if (obj["RowMod"]?.ToString() != "")
                    {
                        obj["RowMod"] = "";
                    }
                }


            }

            string strGetNewLaborDtlWithHdr = completedRecord.ToString();

            try
            {
                using var client = CreateEpicorClient();
                var request = new HttpRequestMessage(HttpMethod.Post, StaticConfigurationClass.GetKineticTestURL() + "/api/v1/Erp.BO.LaborSvc/GetNewLaborDtlWithHdr");

                var content = new StringContent(strGetNewLaborDtlWithHdr, Encoding.UTF8, "application/json");
                request.Content = content;

                // Try to get more detailed error information from the response
                HttpResponseMessage responseLaborDtlHdr = null;
                try
                {
                    responseLaborDtlHdr = client.SendAsync(request).Result;
                    responseLaborDtlHdr.EnsureSuccessStatusCode();
                }
                catch (HttpRequestException ex)
                {
                    if (responseLaborDtlHdr != null)
                    {
                        var errorContent = responseLaborDtlHdr.Content.ReadAsStringAsync().Result;
                        Console.WriteLine($"****************************HTTP request failed creating employee   {completedRecord["ds"]["LaborDtl"][0]["EmployeeNum"]}'s LaborDtlHdr in Kinetic. Status: {responseLaborDtlHdr.StatusCode}, Error: {errorContent}");

                        if (errorContent.Contains("Labor Header does not exist"))
                        {
                            return GetNewLaborDtlWithNoHdr2(completedRecord);
                        }
                        else if (errorContent.Contains("A valid Date is required"))
                        {
                            return GetNewLaborDtlWithNoHdr2(completedRecord);
                        }
                    }
                    else
                    {
                        Console.WriteLine($"HTTP Request failed: {ex.Message}");

                    }
                    return null;
                }

                string GetNewLaborDtlWithHdrResponse = responseLaborDtlHdr.Content.ReadAsStringAsync().Result;
                JObject objGetNewLaborDtlWithHdrResponse = JObject.Parse(GetNewLaborDtlWithHdrResponse);
                JObject innerContent = objGetNewLaborDtlWithHdrResponse["parameters"] as JObject;

                if (innerContent == null)
                {
                    throw new InvalidOperationException("Response does not contain a 'parameters.ds' object.");
                }

                JArray returnedLaborDtlArray = (JArray)innerContent["ds"]?["LaborDtl"];
                JArray returnedLaborHedArray = (JArray)innerContent["ds"]?["LaborHed"];
                // Step 1: Find the highest LaborDtlSeq in returnedLaborDtlArray
                long highestLaborDtlSeq = returnedLaborDtlArray.Max(obj => (long)obj["LaborDtlSeq"]);

                // Step 2: Find the highest LaborHedSeq in laborhedArray
                long highestLaborHedSeq = laborhedArray.Max(obj => (long)obj["LaborHedSeq"]);

                // Step 3: Update laborDtlArray where LaborDtlSeq is 0 and LaborHedSeq is highestLaborHedSeq
                foreach (JObject laborDtlObj in laborDtlArray)
                {
                    if ((long)laborDtlObj["LaborDtlSeq"] == 0 && (long)laborDtlObj["LaborHedSeq"] == highestLaborHedSeq)
                    {
                        laborDtlObj["LaborDtlSeq"] = highestLaborDtlSeq;
                    }
                }



there are a few calls in between these two.

  1. retrieving the employee’s default labor type
  2. change labor type
  3. change indirect code
 public static JObject FinalUpdateToLaborRecord(JObject completedRecord)
        {
            Console.WriteLine("");
            Console.WriteLine("Step 26:");
            Console.WriteLine("Modify RowMod for Update");

            // Define arrays to alter RowMod
            JArray laborhedArray = (JArray)completedRecord["ds"]?["LaborHed"];
            JArray laborDtlArray = (JArray)completedRecord["ds"]?["LaborDtl"];

            // long highestLaborDtlSeq = laborDtlArray.Max(obj => (long)obj["LaborDtlSeq"]);
            // long highestLaborHedSeq = laborhedArray.Max(obj => (long)obj["LaborHedSeq"]);

            // Update RowMod property in laborhedArray
            if (laborhedArray != null && laborhedArray.Count > 0)
            {
                foreach (JObject obj in laborhedArray.Cast<JObject>())
                {
                    obj["RowMod"] = "";
                }
            }

            // Update RowMod property in laborDtlArray
            if (laborDtlArray != null && laborDtlArray.Count > 0)
            {
                foreach (JObject obj in laborDtlArray.Cast<JObject>())
                {
                    // if ((bool)(obj["LaborDtlSeq"] = highestLaborDtlSeq) && (bool)(obj["LaborHedSeq"] = highestLaborHedSeq))
                    // {
                    //     obj["RowMod"] = "A";
                    // }
                    // else
                    // {
                    obj["RowMod"] = "";
                    obj["RowSelected"] = false;

                }


            }


            string strCompletedRecord = completedRecord.ToString();

            try
            {
                Console.WriteLine("");
                Console.WriteLine("Step 27:");
                Console.WriteLine("Update Labor Record Again");

                using var client = CreateEpicorClient();
                var request = new HttpRequestMessage(HttpMethod.Post, StaticConfigurationClass.GetKineticTestURL() + "/api/v1/Erp.BO.LaborSvc/Update");
                var content = new StringContent(strCompletedRecord, Encoding.UTF8, "application/json");
                // Assign content to the request
                request.Content = content;

                var response = client.SendAsync(request).Result;
                response.EnsureSuccessStatusCode();
                string UpdateLaborResponse = response.Content.ReadAsStringAsync().Result;

                JObject objUpdateLaborResponse = JObject.Parse(UpdateLaborResponse);
                JObject innerContent = objUpdateLaborResponse["parameters"]?["ds"] as JObject;
                if (innerContent == null)
                {
                    throw new InvalidOperationException("Response does not contain a 'parameters.ds' object.");
                }

                // Update completedRecord with the inner content
                completedRecord["ds"] = innerContent;

                // Safely access and log the success message
                if (completedRecord["ds"]?["LaborDtl"] is JArray updatedLaborDtlArray && updatedLaborDtlArray.Count > 0)
                {
                    int labordtlIndex = updatedLaborDtlArray.Count - 1;
                    var newLaborDtl = updatedLaborDtlArray[labordtlIndex] as JObject;
                    if (newLaborDtl != null && newLaborDtl["EmployeeNum"] != null)
                    {
                        Console.WriteLine($"LaborDtl Record successfully updated for employee {newLaborDtl["EmployeeNum"]}"); Console.WriteLine("");

                    }
                    else
                    {
                        Console.WriteLine("EmployeeNum is missing in the first LaborDtl object.");
                    }
                }
                else
                {
                    Console.WriteLine("LaborDtl array is missing or empty in the ds object.");
                }
            }
            catch (HttpRequestException ex)
            {
                // Handle HTTP request exceptions
                Console.WriteLine($"****************************HTTP request failed updating employee   {completedRecord["ds"]["LaborDtl"][0]["EmployeeNum"]}'s final Labor record in Kinetic: {ex.Message}****************************");
            }
            catch (Exception ex)
            {
                // Handle other exceptions
                Console.WriteLine($"****************************An error occurred getting employee   {completedRecord["ds"]["LaborDtl"][0]["EmployeeNum"]}'s final Labor record in Kinetic: {ex.Message}****************************");
            }
            return ValidateChargeRate(completedRecord);
        }

Interesting. We use UKG too, although we aren’t doing any integrations

I’m curious why you are doing this? What is the benefit?

We have written several integrations for this. We are using powershell to execute dmt in the background. It works great once formatting gets cleaned up.

We use UKG now as the main source of data reguarding employee time/maintenance, but our payroll is still handled through Kinetic. So, we have employees clock in through Kinetic and then batch update that data into UKG. However, employees put time off requests into UKG, so that information is being sent to kinetic for payroll purposes.

1 Like

So, you export it from UKG to an excel document, then execute a dmt to update kinetic?

Yes. That is correct!

Probably not what you want to do.

If you are tracing in the client, it shows the RowMod after the call, not before.

Let me read this thread and see if I can help.

1 Like

So, after some consulting with our Epicor consulted, he suggested i remove data that isnt being used until i need it. For example, when updating laborhed, remove laborDtl array. I’ve done that, but the results are still not as expected.

current status of my issue:
If i send the data with rowmod blank and a laborhedseq of 0, through an update call first, it gives a laborhedseq number. then i send it with the sequence number and rowmod set to A. This results in no change when sent through my console app. When i copy that same data into the rest help site, it does enter into my laborhed table, but with a different laborhed sequence number.

for reference:
the object was pasted into the resthelp site with “LaborHedSeq”: 605021
in my table, sequence number is 600074.

the api AND RestHelpSite both return successful (200) with the originally retrieved sequence number

any experience with this?

I played with it a little bit but I didn’t have time to get very far.

I do however have some other pointers for you, and unfortunately you might not like it. :people_hugging:

You are likely doing a lot of unnecessary work. You should probably not be using the REST methods directly, but should package this up in Epicor Functions, and call that from your app. It will be much easier to do, as well as maintain.


That being said…
You need to either retrieve an active LaborHed or create one.
If you create one via GetNewLaborHed, you will need to call update and
save it before it can be used.

To get your laborDtl, you will call GetNewLaborDtl, with the laborHedSeq number.

Fill out all the fields you need on it, and call update. The rowmod will be “A”, and that’s how it will come from GetNewLaborDtl.

I’m unsure of what to tell you after that because I’m not exactly clear on what all you want to do.

1 Like

yes, you all have briefly mentioned to me the wonders of epicor functions. My software team does not use those, so it’ll take a bit of researching to figure out where to even start with that. In the meantime, i do need to finish this API before delving into any learning adventures.

I’ve done some playing around and I think the issue is that I was trying to input the records with a time status of already approved. However, Kinetic will not let you modify an approved record (even if that modification is adding it in the first place). As I discovered this at the end of the day yesterday, I will be testing this theory this morning.

Wish me luck! :sweat_smile:

In the meantime, if you have any references for functions within epicor, I would love to hear them. I have access to epicare and epicweb, so links from there would be awesome, if they exist.

1 Like

Its not too different from what you are doing right now, but since its server side, you get to call all the methods directly without all this HTTP/networking garbage complicating things. Which also has the benefit of sending less chaff over the network.

Not only would I use Epicor functions, but then if I did have to call the 1 or 2 Epicor functions I made, I would use the RestHelper library instead of reinventing the wheel.

I’m not sure it would take too long to switch as you would be mostly just translating your Rest calls into direct BO calls. It might be worth looking at before you finish this project.

1 Like

Did you get it all working the way you wanted in the end?