Просмотр исходного кода

Added Assignment.Cost and code to evaluate in the store.

Kenric Nugteren 1 месяц назад
Родитель
Сommit
e2fb49e545

+ 6 - 0
prs.classes/Entities/Assignment/Assignment.cs

@@ -20,10 +20,12 @@ namespace Comal.Classes
 
         [DateEditor]
         [EditorSequence(2)]
+        [RequiredColumn]
         public DateTime Date { get; set; }
 
         [EntityRelationship(DeleteAction.Cascade)]
         [EditorSequence(3)]
+        [RequiredColumn]
         public EmployeeLink EmployeeLink { get; set; }
 
         // [EntityRelationship(DeleteAction.Cascade)]
@@ -112,6 +114,10 @@ namespace Comal.Classes
         [TimestampEditor]
         [EditorSequence("Processing",2)]
         public DateTime Processed { get; set; }
+
+        [CurrencyEditor]
+        [Editable(Editable.Disabled)]
+        public double Cost { get; set; }
         
         [NullEditor]
         [EntityRelationship(DeleteAction.Cascade)]

+ 79 - 0
prs.classes/Utilities/OvertimeUtils.cs

@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Comal.Classes
+{
+    public static class OvertimeUtils
+    {
+        public delegate void EvaluateOvertimeDelegate<TBlock>(TBlock block, OvertimeInterval? interval, TimeSpan duration);
+
+        /// <summary>
+        /// Evaluate the overtime intervals given, using <paramref name="blocks"/> as the list of blocks that have been worked for this day.
+        /// <paramref name="durationSelector"/> is used to get the duration of each <typeparamref name="TBlock"/>, and
+        /// <paramref name="evaluateBlock"/> is called for every distinct block that needs evaluating. So if a given <typeparamref name="TBlock"/>
+        /// overlaps with multiple overtime intervals, <paramref name="evaluateBlock"/> will be called for each interval, with the length of time overlapped
+        /// with that interval provided.
+        /// </summary>
+        public static void EvaluateOvertime<TBlock>(
+            IEnumerable<TBlock> blocks,
+            OvertimeInterval[] overtimeIntervals,
+            Func<TBlock, TimeSpan> durationSelector,
+            EvaluateOvertimeDelegate<TBlock> evaluateBlock
+        )
+        {
+            var curOvertimeIdx = 0;
+            OvertimeInterval? GetOvertimeInterval() => curOvertimeIdx < overtimeIntervals.Length ? overtimeIntervals[curOvertimeIdx] : null;
+            var curInterval = GetOvertimeInterval()?.Interval ?? TimeSpan.Zero;
+
+            foreach (var block in blocks)
+            {
+                var duration = durationSelector(block);
+                while (duration > TimeSpan.Zero)
+                {
+                    var interval = GetOvertimeInterval();
+                    if (interval != null)
+                    {
+                        switch (interval.IntervalType)
+                        {
+                            case OvertimeIntervalType.Interval:
+                                if (duration >= curInterval)
+                                {
+                                    // In this case, the block is more than the rest of
+                                    // the current interval, so we use up all the remaining interval
+                                    // time, and then move to the next interval.
+                                    evaluateBlock(block, interval, curInterval);
+                                    duration -= curInterval;
+                                    ++curOvertimeIdx;
+                                    curInterval = GetOvertimeInterval()?.Interval ?? TimeSpan.Zero;
+                                }
+                                else
+                                {
+                                    evaluateBlock(block, interval, duration);
+                                    // Otherwise, we use up the entire block, and decrease the interval by the duration remaining.
+                                    curInterval -= duration;
+                                    duration = TimeSpan.Zero;
+                                }
+                                break;
+                            case OvertimeIntervalType.RemainingTime:
+                                // In this case, the interval is unchanged.
+                                evaluateBlock(block, interval, duration);
+                                duration = TimeSpan.Zero;
+                                break;
+                            default:
+                                throw new NotImplementedException($"Not implemented Overtime interval type {interval.IntervalType}");
+                        }
+                    }
+                    else
+                    {
+                        // If there is no overtime interval, then we use up the rest of the time on
+                        // the block with a blank PayrollID. Theoretically, this shouldn't happen,
+                        // since the "RemainingTime" interval is required.
+                        evaluateBlock(block, null, duration);
+                        duration = TimeSpan.Zero;
+                    }
+                }
+            }
+        }
+    }
+}

+ 20 - 82
prs.shared/Posters/Timberline/TimesheetTimberlinePoster.cs

@@ -554,96 +554,34 @@ public class Module
         private List<IBlock> EvaluateOvertime(IEnumerable<IBlock> time, Guid overtimeID)
         {
             var overtimeIntervals = _overtimeIntervals.GetValueOrDefault(overtimeID)?.ToArray() ?? [];
-            var curOvertimeIdx = 0;
-            OvertimeInterval? GetOvertimeInterval() => curOvertimeIdx < overtimeIntervals.Length ? overtimeIntervals[curOvertimeIdx] : null;
-            var curInterval = GetOvertimeInterval()?.Interval ?? TimeSpan.Zero;
-
             var newItems = new List<IBlock>();
-            foreach (var block in time)
+            OvertimeUtils.EvaluateOvertime(time, overtimeIntervals, x => x.Duration, (block, interval, duration) =>
             {
-                var duration = block.Duration;
-                while (duration > TimeSpan.Zero)
+                if(interval is null)
                 {
-                    var interval = GetOvertimeInterval();
-                    if (interval != null)
+                    // Shouldn't ever occur, thanks to RemainingTime being required.
+                    if(block is PaidWorkBlock paid)
                     {
-                        switch (interval.IntervalType)
-                        {
-                            case OvertimeIntervalType.Interval:
-                                if (duration >= curInterval)
-                                {
-                                    // In this case, the paid work block is more than the rest of
-                                    // the current interval, so we use up all the remaining interval
-                                    // time, and then move to the next interval.
-                                    if (interval.IsPaid)
-                                    {
-                                        if(block is PaidWorkBlock paid)
-                                        {
-                                            newItems.Add(new PaidWorkBlock(block.TaskID, curInterval, interval.PayrollID, block.Job, block.TimeSheet));
-                                        }
-                                        else if(block is LeaveBlock leave)
-                                        {
-                                            newItems.Add(new LeaveBlock(leave.PayrollID, curInterval, leave.TimeSheet));
-                                        }
-                                    }
-                                    duration -= curInterval;
-                                    ++curOvertimeIdx;
-                                    curInterval = GetOvertimeInterval()?.Interval ?? TimeSpan.Zero;
-                                }
-                                else
-                                {
-                                    // Otherwise, we use up the entire paid work block, and decrease the interval by the duration remaining.
-                                    if (interval.IsPaid)
-                                    {
-                                        if(block is PaidWorkBlock paid)
-                                        {
-                                            newItems.Add(new PaidWorkBlock(block.TaskID, duration, interval.PayrollID, block.Job, block.TimeSheet));
-                                        }
-                                        else if(block is LeaveBlock leave)
-                                        {
-                                            newItems.Add(new LeaveBlock(leave.PayrollID, duration, leave.TimeSheet));
-                                        }
-                                    }
-                                    curInterval -= duration;
-                                    duration = TimeSpan.Zero;
-                                }
-                                break;
-                            case OvertimeIntervalType.RemainingTime:
-                                // In this case, the interval is unchanged.
-                                if (interval.IsPaid)
-                                {
-                                    if(block is PaidWorkBlock paid)
-                                    {
-                                        newItems.Add(new PaidWorkBlock(block.TaskID, duration, interval.PayrollID, block.Job, block.TimeSheet));
-                                    }
-                                    else if(block is LeaveBlock leave)
-                                    {
-                                        newItems.Add(new LeaveBlock(leave.PayrollID, duration, leave.TimeSheet));
-                                    }
-                                }
-                                duration = TimeSpan.Zero;
-                                break;
-                            default:
-                                throw new NotImplementedException($"Not implemented Overtime interval type {interval.IntervalType}");
-                        }
+                        newItems.Add(new PaidWorkBlock(block.TaskID, duration, "", block.Job, block.TimeSheet));
                     }
-                    else
+                    else if(block is LeaveBlock leave)
                     {
-                        // If there is no overtime interval, then we use up the rest of the time on
-                        // the block with a blank PayrollID. Theoretically, this shouldn't happen,
-                        // since the "RemainingTime" interval is required.
-                        if(block is PaidWorkBlock paid)
-                        {
-                            newItems.Add(new PaidWorkBlock(block.TaskID, duration, "", block.Job, block.TimeSheet));
-                        }
-                        else if(block is LeaveBlock leave)
-                        {
-                            newItems.Add(new LeaveBlock(leave.PayrollID, duration, leave.TimeSheet));
-                        }
-                        duration = TimeSpan.Zero;
+                        newItems.Add(new LeaveBlock(leave.PayrollID, duration, leave.TimeSheet));
                     }
                 }
-            }
+                else if (interval.IsPaid)
+                {
+                    if (block is PaidWorkBlock paid)
+                    {
+                        newItems.Add(new PaidWorkBlock(block.TaskID, duration, interval.PayrollID, block.Job, block.TimeSheet));
+                    }
+                    else if (block is LeaveBlock leave)
+                    {
+                        newItems.Add(new LeaveBlock(leave.PayrollID, duration, leave.TimeSheet));
+                    }
+                }
+
+            });
             return newItems;
         }
 

+ 80 - 0
prs.stores/AssignmentStore.cs

@@ -55,7 +55,87 @@ namespace Comal.Stores
             base.AfterSave(entity);
 
             CheckActivityForms(entity);
+
+            CheckAssignmentCosts(entity);
+        }
+
+        private void CheckAssignmentCosts(Assignment entity)
+        {
+            var changedEmployee = entity.TryGetOriginalValue(x => x.EmployeeLink.ID, out var employeeID);
+            if (!changedEmployee)
+            {
+                employeeID = entity.EmployeeLink.ID;
+            }
+
+            var changedDate = entity.TryGetOriginalValue(x => x.Date, out var date);
+            if (!changedDate)
+            {
+                date = entity.Date;
+            }
+            if(changedEmployee || changedDate)
+            {
+                CheckAssignmentCosts(date, employeeID);
+            }
+
+            CheckAssignmentCosts(entity.Date, entity.EmployeeLink.ID);
         }
         
+        private void CheckAssignmentCosts(DateTime date, Guid employeeID)
+        {
+            if (employeeID == Guid.Empty) return;
+
+            var assignments = Provider.Query(
+                Filter<Assignment>.Where(x => x.Date).IsEqualTo(date)
+                    .And(x => x.EmployeeLink.ID).IsEqualTo(employeeID),
+                Columns.None<Assignment>()
+                    .Add(x => x.ID)
+                    .Add(x => x.Actual.Start)
+                    .Add(x => x.Actual.Finish)
+                    .Add(x => x.Actual.Duration)
+                    .Add(x => x.Booked.Start)
+                    .Add(x => x.Booked.Finish)
+                    .Add(x => x.Booked.Duration))
+                .ToArray<Assignment>();
+            var employee = Provider.Query(
+                Filter<Employee>.Where(x => x.ID).IsEqualTo(employeeID),
+                Columns.None<Employee>()
+                    .Add(x => x.RosterStart)
+                    .Add(x => x.HourlyRate))
+                .ToObjects<Employee>().FirstOrDefault();
+            if (employee is null) return;
+
+            var rosterItems = Provider.Query(
+                Filter<EmployeeRosterItem>.Where(x => x.Employee.ID).IsEqualTo(employeeID),
+                Columns.None<EmployeeRosterItem>()
+                    .Add(x => x.Overtime.ID),
+                new SortOrder<EmployeeRosterItem>(x => x.Day))
+                .ToArray<EmployeeRosterItem>();
+            var overtimeID = RosterUtils.GetRoster(rosterItems, employee.RosterStart, date)?.Overtime.ID ?? Guid.Empty;
+            if(overtimeID == Guid.Empty) return;
+
+            var overtime = Provider.Query(
+                Filter<OvertimeInterval>.Where(x => x.Overtime.ID).IsEqualTo(overtimeID),
+                Columns.None<OvertimeInterval>()
+                    .Add(x => x.Interval)
+                    .Add(x => x.IntervalType)
+                    .Add(x => x.Multiplier),
+                new SortOrder<OvertimeInterval>(x => x.Sequence))
+                .ToArray<OvertimeInterval>();
+
+            assignments.SortBy(x => x.EffectiveStartTime());
+            var totalHours = new Dictionary<Assignment, double>();
+            OvertimeUtils.EvaluateOvertime(assignments, overtime, x => x.EffectiveFinishTime() - x.EffectiveStartTime(), (assignment, interval, duration) =>
+            {
+                totalHours[assignment] = totalHours.GetValueOrAdd(assignment)
+                    + duration.TotalHours * (interval?.Multiplier ?? 1);
+            });
+
+            foreach(var assignment in assignments)
+            {
+                assignment.Cost = totalHours.GetValueOrDefault(assignment) * employee.HourlyRate;
+            }
+
+            Provider.Save(assignments.Where(x => x.IsChanged()));
+        }
     }
 }