/// <summary> /// Split a timesheet entry into fragments. One fragment for each work type. /// </summary> /// <param name="timesheetEntryCursor"></param> /// <param name="minTimeFragmentSize"></param> /// <param name="database"></param> /// <param name="logger"></param> /// <returns></returns> public static List <TimesheetEntryFragment> GetTimesheetFragments( MongoCursor <BsonDocument> timesheetEntryCursor, int minTimeFragmentSize, MongoDatabase database, ILogger logger, HashSet <ObjectId> failedExports, HashSet <ObjectId> succeededExports, bool splitToDays = true) { var resultFragments = new List <TimesheetEntryFragment>(); foreach (BsonDocument timesheetEntry in timesheetEntryCursor) { try { TimesheetEntryWithDetails entryWithDetails = GetTimesheetEntryWithDetails(timesheetEntry, database); List <TimesheetEntryFragment> entryFragments = SplitTimesheetEntryWithDetailsToFragments(entryWithDetails, logger); if (splitToDays) { entryFragments = SplitTimesheetFragmentsToDays(entryFragments); } if (minTimeFragmentSize > 0) { RemoveTooShortFragments(entryFragments, logger, minTimeFragmentSize); } resultFragments.AddRange(entryFragments.ToArray()); succeededExports.Add((ObjectId)timesheetEntry[DBQuery.Id]); } catch (Exception ex) { logger.LogError("Failed to handle timesheet entry. Skipping this entry", ex, timesheetEntry[DBQuery.Id]); failedExports.Add((ObjectId)timesheetEntry[DBQuery.Id]); } } return(resultFragments); }
/// <summary> /// Add detail and pay type to TimesheetEntryWithDetails object. /// </summary> /// <param name="timesheetEntryWithDetails"></param> /// <param name="timesheetEntryDetail"></param> private static void AddDetailAndPayTypeToTimesheetEntryWithDetails( TimesheetEntryWithDetails timesheetEntryWithDetails, BsonDocument timesheetEntryDetail, MongoDatabase database) { BsonDocument timesheetEntryDetailType = GetTimesheetEntryDetailPayType(timesheetEntryDetail, database); // Order timesheet entry details by priority with a simple bubble sort. Times are // sorted with order so that we get overtime 100% before overtime 50% for example int newOrderNumber = 0; if (timesheetEntryDetailType.Contains("order")) { newOrderNumber = (int)timesheetEntryDetailType["order"]; } int index = 0; foreach (Tuple <BsonDocument, BsonDocument> documentAndPayTypes in timesheetEntryWithDetails.EntryDetailsAndPayTypes) { int currentOrderNumber = 0; if (documentAndPayTypes.Item2.Contains("order")) { currentOrderNumber = (int)documentAndPayTypes.Item2["order"]; } if (newOrderNumber > currentOrderNumber) { break; } index++; } timesheetEntryWithDetails.EntryDetailsAndPayTypes.Insert( index, new Tuple <BsonDocument, BsonDocument>(timesheetEntryDetail, timesheetEntryDetailType)); }
/// <summary> /// Split timesheet entry so that it doesn't span to multiple days /// </summary> private static List <TimesheetEntryFragment> SplitTimesheetEntryWithDetailsToFragments(TimesheetEntryWithDetails entryWithDetails, ILogger logger) { BsonDocument timesheetEntry = entryWithDetails.TimesheetEntry; DateTime startTime = ((DateTime)timesheetEntry["starttimestamp"]).ToLocalTime(); DateTime currentEndTimestamp = ((DateTime)timesheetEntry["endtimestamp"]).ToLocalTime(); DateTime currentStartTimestamp = currentEndTimestamp; var timesheetEntryFragments = new List <TimesheetEntryFragment>(); // Set to true is some details (such as contract pay) mean that regular hours shouldn't be exported. bool regularHoursInvalidated = false; // Handle detail types that don't count towards regular hours foreach (Tuple <BsonDocument, BsonDocument> detailAndPayType in entryWithDetails.EntryDetailsAndPayTypes) { if ((bool)detailAndPayType.Item2.GetValue("invalidatesregularhours", false)) { regularHoursInvalidated = true; } if ((bool)detailAndPayType.Item2.GetValue("countsasregularhours", false)) { continue; } var fragment = new TimesheetEntryFragment(); fragment.Detail = detailAndPayType.Item1; fragment.PayType = detailAndPayType.Item2; fragment.ProjectCategoryBase = entryWithDetails.WorkerCategory; fragment.ProjectId = entryWithDetails.ProjectId; fragment.WorkerId = entryWithDetails.WorkerId; fragment.WorkerProfitCenter = entryWithDetails.WorkerProfitCenter; // Use timesheet entry detail's note if present and base entry's note if not fragment.Note = (string)detailAndPayType.Item1.GetValue("note", string.Empty); if (string.IsNullOrEmpty(fragment.Note)) { fragment.Note = entryWithDetails.Note; } if (!fragment.Detail.Contains("duration")) { continue; } TimeSpan duration = TimeSpan.FromMilliseconds((int)fragment.Detail["duration"]); fragment.Start = startTime; fragment.End = fragment.Start + duration; timesheetEntryFragments.Add(fragment); } // Handle detail types that count towards regular hours foreach (Tuple <BsonDocument, BsonDocument> detailAndPayType in entryWithDetails.EntryDetailsAndPayTypes) { if (!(bool)detailAndPayType.Item2.GetValue("countsasregularhours", false)) { continue; } var fragment = new TimesheetEntryFragment(); fragment.Detail = detailAndPayType.Item1; fragment.PayType = detailAndPayType.Item2; fragment.ProjectCategoryBase = entryWithDetails.WorkerCategory; fragment.ProjectId = entryWithDetails.ProjectId; fragment.WorkerId = entryWithDetails.WorkerId; fragment.WorkerProfitCenter = entryWithDetails.WorkerProfitCenter; // Use timesheet entry detail's note if present and base entry's note if not fragment.Note = (string)detailAndPayType.Item1.GetValue("note", string.Empty); if (string.IsNullOrEmpty(fragment.Note)) { fragment.Note = entryWithDetails.Note; } if (!fragment.Detail.Contains("duration")) { continue; } TimeSpan duration = TimeSpan.FromMilliseconds((int)fragment.Detail["duration"]); fragment.End = currentEndTimestamp; currentStartTimestamp = currentEndTimestamp - duration; bool breakAfter = false; if (currentStartTimestamp < startTime) { logger.LogWarning("Timesheet entry details are longer than entry itself. Skipping extra parts.", timesheetEntry[DBQuery.Id]); currentStartTimestamp = startTime; breakAfter = true; } fragment.Start = currentStartTimestamp; timesheetEntryFragments.Add(fragment); // Continue processing backwards from current start timestamp currentEndTimestamp = currentStartTimestamp; if (breakAfter) { break; } } // Add the "root" version timesheet entry not based on any detail (overtime etc.) if (currentStartTimestamp > startTime && !regularHoursInvalidated) { var fragment = new TimesheetEntryFragment(); fragment.IsRootEntry = true; fragment.IsTravelTime = (bool)timesheetEntry.GetValue("istraveltime", false); fragment.ProjectCategoryBase = entryWithDetails.WorkerCategory; fragment.ProjectId = entryWithDetails.ProjectId; fragment.WorkerId = entryWithDetails.WorkerId; fragment.Note = entryWithDetails.Note; fragment.Start = startTime; fragment.End = currentStartTimestamp; fragment.WorkerProfitCenter = entryWithDetails.WorkerProfitCenter; timesheetEntryFragments.Add(fragment); } return(timesheetEntryFragments); }
/// <summary> /// Return tiemsheet entry split to different parts. First get all detail parts starting from end /// and counting using duration and what remains is the main part. /// </summary> /// <param name="timesheetEntry"></param> private static TimesheetEntryWithDetails GetTimesheetEntryWithDetails( BsonDocument timesheetEntry, MongoDatabase database) { if (!timesheetEntry.Contains("starttimestamp") || !timesheetEntry.Contains("endtimestamp")) { throw new HandlerException("Start or end time missing in timesheet entry."); } MongoCollection <BsonDocument> usersCollection = database.GetCollection("user"); BsonDocument worker = usersCollection.FindOne(Query.EQ(DBQuery.Id, timesheetEntry["user"][0])); MongoCollection <BsonDocument> projectsCollection = database.GetCollection("project"); BsonDocument project = projectsCollection.FindOne(Query.EQ(DBQuery.Id, (ObjectId)timesheetEntry["project"][0])); MongoCollection <BsonDocument> claContractsCollection = database.GetCollection("clacontract"); BsonDocument claContract = claContractsCollection.FindOne(Query.EQ(DBQuery.Id, (ObjectId)worker["clacontract"][0])); MongoCollection <BsonDocument> profitCenterCollection = database.GetCollection("profitcenter"); BsonDocument profitCenter = profitCenterCollection.FindOne(Query.EQ(DBQuery.Id, (ObjectId)worker["profitcenter"][0])); if (project == null) { throw new HandlerException("Project not found for timesheet entry."); } if (worker == null) { throw new HandlerException("Worker not found for timesheet entry."); } if (claContract == null) { throw new HandlerException("CLA contract not found for timesheet entry worker."); } if (profitCenter == null) { throw new HandlerException("Profit center not found for timesheet entry worker."); } if (!project.Contains("identifier")) { throw new HandlerException("Project is missing an identifier."); } if (!worker.Contains("identifier")) { throw new HandlerException("Worker is missing an identifier."); } var timesheetEntryWithDetails = new TimesheetEntryWithDetails(); timesheetEntryWithDetails.WorkerCategory = IntegrationHelpers.GetCategoryForWorker(claContract, profitCenter); timesheetEntryWithDetails.ProjectId = (string)project["identifier"]; timesheetEntryWithDetails.Note = (string)timesheetEntry.GetValue("note", ""); timesheetEntryWithDetails.WorkerId = (string)worker["identifier"]; timesheetEntryWithDetails.WorkerProfitCenter = (string)profitCenter["identifier"]; MongoCollection <BsonDocument> timsheetEntryDetailsCollection = database.GetCollection("timesheetentry"); MongoCursor <BsonDocument> cursor = timsheetEntryDetailsCollection.Find(Query.EQ("parent", timesheetEntry[DBQuery.Id])); timesheetEntryWithDetails.TimesheetEntry = timesheetEntry; foreach (BsonDocument timesheetEntryDetail in cursor) { AddDetailAndPayTypeToTimesheetEntryWithDetails(timesheetEntryWithDetails, timesheetEntryDetail, database); } return(timesheetEntryWithDetails); }