TimesheetTimberlinePoster.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  1. using Comal.Classes;
  2. using CsvHelper;
  3. using CsvHelper.Configuration.Attributes;
  4. using InABox.Core;
  5. using InABox.Core.Postable;
  6. using InABox.Poster.Timberline;
  7. using InABox.Scripting;
  8. using Microsoft.Win32;
  9. using PRS.Shared.TimeSheetTimberline;
  10. using System.ComponentModel;
  11. using System.Globalization;
  12. using System.IO;
  13. using System.Linq;
  14. using System.Text;
  15. using System.Threading.Tasks;
  16. using System.Windows.Input;
  17. namespace PRS.Shared
  18. {
  19. namespace TimeSheetTimberline
  20. {
  21. public class ActivityBlock
  22. {
  23. public Guid Activity { get; set; }
  24. public TimeSpan Start { get; set; }
  25. public TimeSpan Finish { get; set; }
  26. public TimeSheet TimeSheet { get; set; }
  27. public TimeSpan Duration => Finish - Start;
  28. public ActivityBlock(Assignment assignment, TimeSheet sheet)
  29. {
  30. Activity = assignment.ActivityLink.ID != Guid.Empty
  31. ? assignment.ActivityLink.ID
  32. : sheet.ActivityLink.ID;
  33. Start = assignment.EffectiveStartTime();
  34. Finish = assignment.EffectiveFinishTime();
  35. TimeSheet = sheet;
  36. }
  37. public ActivityBlock(TimeSheet sheet)
  38. {
  39. Activity = sheet.ActivityLink.ID;
  40. Start = sheet.ApprovedStart;
  41. Finish = sheet.ApprovedFinish;
  42. TimeSheet = sheet;
  43. }
  44. public ActivityBlock(TimeSheet sheet, TimeSpan start, TimeSpan finish)
  45. {
  46. Activity = sheet.ActivityLink.ID;
  47. Start = start;
  48. Finish = finish;
  49. TimeSheet = sheet;
  50. }
  51. public ActivityBlock Chop(TimeSheet sheet)
  52. {
  53. if (Start < sheet.ApprovedStart)
  54. {
  55. Start = sheet.ApprovedStart;
  56. }
  57. if (Finish > sheet.ApprovedFinish)
  58. {
  59. Finish = sheet.ApprovedFinish;
  60. }
  61. return this;
  62. }
  63. public bool ContainedInTimeSheet(TimeSheet sheet) =>
  64. Start < sheet.ApprovedFinish && Finish > sheet.ApprovedStart;
  65. public bool IntersectsWith(ActivityBlock other)
  66. {
  67. return Start < other.Finish && Finish > other.Start;
  68. }
  69. }
  70. public interface IBlock
  71. {
  72. string Job { get; set; }
  73. string Extra { get; set; }
  74. string TaskID { get; set; }
  75. TimeSpan Duration { get; set; }
  76. string PayrollID { get; set; }
  77. TimeSheet TimeSheet { get; set; }
  78. }
  79. public class PaidWorkBlock : IBlock
  80. {
  81. public string Job { get; set; }
  82. public string Extra { get; set; }
  83. public string TaskID { get; set; }
  84. public TimeSpan Duration { get; set; }
  85. public string PayrollID { get; set; }
  86. public TimeSheet TimeSheet { get; set; }
  87. public PaidWorkBlock(string taskID, TimeSpan duration, string payID, string job, TimeSheet timeSheet)
  88. {
  89. TaskID = taskID;
  90. Duration = duration;
  91. PayrollID = payID;
  92. Job = job;
  93. Extra = "";
  94. TimeSheet = timeSheet;
  95. }
  96. }
  97. public class LeaveBlock : IBlock
  98. {
  99. public string Job { get; set; }
  100. public string Extra { get; set; }
  101. public string TaskID { get; set; }
  102. public TimeSpan Duration { get; set; }
  103. public string PayrollID { get; set; }
  104. public TimeSheet TimeSheet { get; set; }
  105. public LeaveBlock(string payrollID, TimeSpan duration, TimeSheet timeSheet)
  106. {
  107. PayrollID = payrollID;
  108. Duration = duration;
  109. Job = "";
  110. Extra = "";
  111. TaskID = "";
  112. TimeSheet = timeSheet;
  113. }
  114. }
  115. public class BaseArgs : CancelEventArgs
  116. {
  117. public IDataModel<TimeSheet> Model { get; set; }
  118. public Guid Employee { get; set; }
  119. public DateTime Date { get; set; }
  120. public BaseArgs(IDataModel<TimeSheet> model, Guid employee, DateTime date)
  121. {
  122. Model = model;
  123. Employee = employee;
  124. Date = date;
  125. }
  126. }
  127. public class ProcessRawDataArgs : BaseArgs
  128. {
  129. public List<TimeSheet> TimeSheets { get; set; }
  130. public List<Assignment> Assignments { get; set; }
  131. public ProcessRawDataArgs(
  132. IDataModel<TimeSheet> model, Guid employee, DateTime date,
  133. List<TimeSheet> timeSheets, List<Assignment> assignments): base(model, employee, date)
  134. {
  135. TimeSheets = timeSheets;
  136. Assignments = assignments;
  137. }
  138. }
  139. public class ProcessActivityBlocksArgs : BaseArgs
  140. {
  141. public List<ActivityBlock> ActivityBlocks { get; set; }
  142. public ProcessActivityBlocksArgs(
  143. IDataModel<TimeSheet> model, Guid employee, DateTime date,
  144. List<ActivityBlock> activityBlocks) : base(model, employee, date)
  145. {
  146. ActivityBlocks = activityBlocks;
  147. }
  148. }
  149. public class ProcessTimeBlocksArgs : BaseArgs
  150. {
  151. public List<PaidWorkBlock> WorkBlocks { get; set; }
  152. public List<LeaveBlock> LeaveBlocks { get; set; }
  153. public ProcessTimeBlocksArgs(
  154. IDataModel<TimeSheet> model, Guid employee, DateTime date,
  155. List<PaidWorkBlock> workBlocks, List<LeaveBlock> leaveBlocks) : base(model, employee, date)
  156. {
  157. WorkBlocks = workBlocks;
  158. LeaveBlocks = leaveBlocks;
  159. }
  160. }
  161. public class ProcessItemArgs : BaseArgs
  162. {
  163. public TimesheetTimberlineItem Item { get; set; }
  164. public ProcessItemArgs(
  165. IDataModel<TimeSheet> model, Guid employee, DateTime date,
  166. TimesheetTimberlineItem item) : base(model, employee, date)
  167. {
  168. Item = item;
  169. }
  170. }
  171. }
  172. public class TimeSheetTimberlineResult : PostResult<TimeSheet>
  173. {
  174. private List<TimesheetTimberlineItem> items = new List<TimesheetTimberlineItem>();
  175. public IEnumerable<TimesheetTimberlineItem> Items => items;
  176. public void AddItem(TimesheetTimberlineItem item)
  177. {
  178. items.Add(item);
  179. }
  180. }
  181. public class TimesheetTimberlineItem
  182. {
  183. [Index(0)]
  184. public string Employee { get; set; } = "";
  185. [Index(1)]
  186. [CsvHelper.Configuration.Attributes.TypeConverter(typeof(TimberlinePosterDateConverter))]
  187. public DateOnly InDate { get; set; }
  188. [Index(2)]
  189. public string Job { get; set; } = "";
  190. [Index(3)]
  191. public string Extra { get; set; } = "";
  192. [Index(4)]
  193. public string Task { get; set; } = "";
  194. [Index(5)]
  195. public double Hours { get; set; }
  196. [Index(6)]
  197. public string PayID { get; set; } = "";
  198. }
  199. public enum TimesheetTimberlineActivityCalculation
  200. {
  201. TimesheetOnly,
  202. TimesheetPriority,
  203. AssignmentPriority
  204. }
  205. public class TimesheetTimberlineSettings : TimberlinePosterSettings<TimeSheet>
  206. {
  207. [EnumLookupEditor(typeof(TimesheetTimberlineActivityCalculation), LookupWidth = 200)]
  208. public TimesheetTimberlineActivityCalculation ActivityCalculation { get; set; }
  209. protected override string DefaultScript()
  210. {
  211. return
  212. @"using PRS.Shared;
  213. using PRS.Shared.TimeSheetTimberline;
  214. using InABox.Core;
  215. using System.Collections.Generic;
  216. public class Module
  217. {
  218. public void BeforePost(IDataModel<TimeSheet> model)
  219. {
  220. // Perform pre-processing
  221. }
  222. public void ProcessRawData(ProcessRawDataArgs args)
  223. {
  224. // Before PRS calculates anything, you can edit the list of timesheets and assignments it is working with here.
  225. }
  226. public void ProcessActivityBlocks(ProcessActivityBlocksArgs args)
  227. {
  228. // Once PRS has aggregated the list of timesheets and assignments into a list of time blocks with given activities, you can edit these time blocks here.
  229. }
  230. public void ProcessTimeBlocks(ProcessTimeBlocksArgs args)
  231. {
  232. // This function is called after PRS has determined the length, duration and overtime rules for all the blocks of time. Here, you can edit
  233. // this data before it is collated into the export.
  234. }
  235. public void ProcessItem(ProcessItemArgs args)
  236. {
  237. // This is the final function before PRS exports each item. You can edit the data as you wish.
  238. }
  239. public void AfterPost(IDataModel<TimeSheet> model)
  240. {
  241. // Perform post-processing
  242. }
  243. }";
  244. }
  245. }
  246. public class TimesheetTimberlinePoster : ITimberlinePoster<TimeSheet, TimesheetTimberlineSettings>
  247. {
  248. public ScriptDocument? Script { get; set; }
  249. public TimesheetTimberlineSettings Settings { get; set; }
  250. private Dictionary<Guid, Activity> _activities = null!; // Initialised on DoProcess()
  251. private Dictionary<Guid, OvertimeInterval[]> _overtimeIntervals = null!; // Initialised on DoProcess()
  252. public bool BeforePost(IDataModel<TimeSheet> model)
  253. {
  254. model.RemoveTable<Document>("CompanyLogo");
  255. model.RemoveTable<CoreTable>("CompanyInformation");
  256. model.RemoveTable<Employee>();
  257. model.RemoveTable<User>();
  258. model.SetColumns<TimeSheet>(new Columns<TimeSheet>(x => x.ID)
  259. .Add(x => x.Approved)
  260. .Add(x => x.EmployeeLink.ID)
  261. .Add(x => x.Date)
  262. .Add(x => x.ApprovedDuration)
  263. .Add(x => x.ApprovedStart)
  264. .Add(x => x.ApprovedFinish)
  265. .Add(x => x.ActivityLink.ID)
  266. .Add(x => x.JobLink.JobNumber));
  267. model.AddTable<Activity>(
  268. null,
  269. new Columns<Activity>(x => x.ID).Add(x => x.Code).Add(x => x.PayrollID).Add(x => x.IsLeave),
  270. isdefault: true);
  271. model.AddTable<OvertimeInterval>(
  272. null,
  273. new Columns<OvertimeInterval>(x => x.ID)
  274. .Add(x => x.Overtime.ID)
  275. .Add(x => x.Sequence)
  276. .Add(x => x.IntervalType)
  277. .Add(x => x.Interval)
  278. .Add(x => x.PayrollID)
  279. .Add(x => x.IsPaid),
  280. isdefault: true);
  281. model.AddLookupTable<TimeSheet, Employee>(x => x.EmployeeLink.ID, x => x.ID,
  282. new Filter<Employee>(x => x.PayrollID).IsNotEqualTo(""),
  283. new Columns<Employee>(x => x.ID).Add(x => x.Code).Add(x => x.PayrollID).Add(x => x.OvertimeRuleLink.ID)
  284. .Add(x => x.RosterStart),
  285. lookupalias: "Employees", isdefault: true);
  286. model.AddChildTable<Employee, EmployeeRosterItem>(x => x.ID, x => x.Employee.ID,
  287. columns: new Columns<EmployeeRosterItem>(x => x.ID)
  288. .Add(x => x.Overtime.ID)
  289. .Add(x => x.Employee.ID),
  290. parentalias: "Employees", childalias: "Rosters", isdefault: true);
  291. model.AddLookupTable<TimeSheet, Assignment>(x => x.Date, x => x.Date, null,
  292. new Columns<Assignment>(x => x.ID)
  293. .Add(x => x.Date)
  294. .Add(x => x.EmployeeLink.ID)
  295. .Add(x => x.Actual.Start)
  296. .Add(x => x.Actual.Duration)
  297. .Add(x => x.Actual.Finish)
  298. .Add(x => x.Booked.Start)
  299. .Add(x => x.Booked.Duration)
  300. .Add(x => x.Booked.Finish)
  301. .Add(x => x.ActivityLink.ID),
  302. isdefault: true);
  303. Script?.Execute(methodname: "BeforePost", parameters: new object[] { model });
  304. return true;
  305. }
  306. private void ProcessRawData(ProcessRawDataArgs args)
  307. {
  308. Script?.Execute(methodname: "ProcessRawData", parameters: new object[] { args });
  309. }
  310. private void ProcessActivityBlocks(ProcessActivityBlocksArgs args)
  311. {
  312. Script?.Execute(methodname: "ProcessActivityBlocks", parameters: new object[] { args });
  313. }
  314. private void ProcessTimeBlocks(ProcessTimeBlocksArgs args)
  315. {
  316. Script?.Execute(methodname: "ProcessTimeBlocks", parameters: new object[] { args });
  317. }
  318. private void ProcessItem(ProcessItemArgs args)
  319. {
  320. Script?.Execute(methodname: "ProcessItem", parameters: new object[] { args });
  321. }
  322. private IEnumerable<ActivityBlock> GetMaskedActivityBlocks(IEnumerable<Assignment> assignments, TimeSheet sheet)
  323. {
  324. if (sheet.ActivityLink.ID != Guid.Empty
  325. && _activities.TryGetValue(sheet.ActivityLink.ID, out var activity)
  326. && activity.IsLeave)
  327. {
  328. yield return new ActivityBlock(sheet);
  329. yield break;
  330. }
  331. var blocks = assignments.Select(x => new ActivityBlock(x, sheet))
  332. .Where(x => x.ContainedInTimeSheet(sheet)).Select(x => x.Chop(sheet))
  333. .OrderBy(x => x.Start).ToList();
  334. for(int i = 0; i < blocks.Count; ++i)
  335. {
  336. var block = blocks[i];
  337. var totalTime = block.Duration;
  338. var maxFinish = block.Finish;
  339. // Find all overlapping blocks; j represents the next non-overlapping block.
  340. int j = i + 1;
  341. for (; j < blocks.Count && block.IntersectsWith(blocks[j]); ++j)
  342. {
  343. totalTime += blocks[j].Duration;
  344. if (blocks[j].Finish > maxFinish)
  345. {
  346. maxFinish = blocks[j].Finish;
  347. }
  348. }
  349. var netTime = maxFinish - block.Start;
  350. var start = block.Start;
  351. foreach(var newBlock in blocks.Skip(i).Take(j - i))
  352. {
  353. var frac = newBlock.Duration.TotalHours / totalTime.TotalHours;
  354. var duration = netTime.Multiply(frac);
  355. newBlock.Start = start;
  356. newBlock.Finish = start + duration;
  357. start = newBlock.Finish;
  358. }
  359. }
  360. var curTime = sheet.ApprovedStart;
  361. foreach(var block in blocks)
  362. {
  363. if (block.Start > curTime)
  364. {
  365. yield return new ActivityBlock(sheet, curTime, block.Start);
  366. }
  367. yield return block;
  368. curTime = block.Finish;
  369. }
  370. if(curTime < sheet.ApprovedFinish)
  371. {
  372. yield return new ActivityBlock(sheet, curTime, sheet.ApprovedFinish);
  373. }
  374. }
  375. private List<ActivityBlock> GetActivityBlocks(IEnumerable<Assignment> assignments, IList<TimeSheet> sheets)
  376. {
  377. switch (Settings.ActivityCalculation)
  378. {
  379. case TimesheetTimberlineActivityCalculation.TimesheetOnly:
  380. return sheets.Select(x => new ActivityBlock(x)).OrderBy(x => x.Start).ToList();
  381. case TimesheetTimberlineActivityCalculation.TimesheetPriority:
  382. var sheetLookup = sheets.ToLookup(x => x.ActivityLink.ID == Guid.Empty);
  383. return sheetLookup[false].Select(x => new ActivityBlock(x))
  384. .Concat(sheetLookup[true].SelectMany(x => GetMaskedActivityBlocks(assignments, x)))
  385. .OrderBy(x => x.Start)
  386. .ToList();
  387. case TimesheetTimberlineActivityCalculation.AssignmentPriority:
  388. return sheets.SelectMany(x => GetMaskedActivityBlocks(assignments, x)).OrderBy(x => x.Start).ToList();
  389. default:
  390. throw new Exception($"Invalide Activity calculation {Settings.ActivityCalculation}");
  391. }
  392. }
  393. private List<PaidWorkBlock> EvaluateOvertime(IEnumerable<PaidWorkBlock> workTime, Guid overtimeID)
  394. {
  395. var overtimeIntervals = _overtimeIntervals.GetValueOrDefault(overtimeID)?.ToList() ?? new List<OvertimeInterval>();
  396. overtimeIntervals.Reverse();
  397. var workItems = new List<PaidWorkBlock>();
  398. foreach (var block in workTime)
  399. {
  400. var duration = block.Duration;
  401. while (duration > TimeSpan.Zero)
  402. {
  403. var interval = overtimeIntervals.LastOrDefault();
  404. if (interval != null)
  405. {
  406. switch (interval.IntervalType)
  407. {
  408. case OvertimeIntervalType.Interval:
  409. if (duration >= interval.Interval)
  410. {
  411. if (interval.IsPaid)
  412. {
  413. workItems.Add(new(block.TaskID, interval.Interval, interval.PayrollID, block.Job, block.TimeSheet));
  414. }
  415. overtimeIntervals.RemoveAt(overtimeIntervals.Count - 1);
  416. duration -= interval.Interval;
  417. }
  418. else
  419. {
  420. if (interval.IsPaid)
  421. {
  422. workItems.Add(new(block.TaskID, duration, interval.PayrollID, block.Job, block.TimeSheet));
  423. }
  424. interval.Interval -= duration;
  425. duration = TimeSpan.Zero;
  426. }
  427. break;
  428. case OvertimeIntervalType.RemainingTime:
  429. if (interval.IsPaid)
  430. {
  431. workItems.Add(new(block.TaskID, duration, interval.PayrollID, block.Job, block.TimeSheet));
  432. }
  433. duration = TimeSpan.Zero;
  434. break;
  435. default:
  436. throw new NotImplementedException($"Not implemented Overtime interval type {interval.IntervalType}");
  437. }
  438. }
  439. else
  440. {
  441. workItems.Add(new(block.TaskID, duration, "", block.Job, block.TimeSheet));
  442. duration = TimeSpan.Zero;
  443. }
  444. }
  445. }
  446. return workItems;
  447. }
  448. private TimeSheetTimberlineResult DoProcess(IDataModel<TimeSheet> model)
  449. {
  450. var items = new TimeSheetTimberlineResult();
  451. var timesheets = model.GetTable<TimeSheet>().ToObjects<TimeSheet>().ToList();
  452. if(timesheets.Any(x => x.Approved.IsEmpty()))
  453. {
  454. throw new Exception("Unapproved Timesheets detected");
  455. }
  456. else if (!timesheets.Any())
  457. {
  458. throw new Exception("No approved timesheets found");
  459. }
  460. _activities = model.GetTable<Activity>().ToObjects<Activity>().ToDictionary(x => x.ID, x => x);
  461. _overtimeIntervals = model.GetTable<OvertimeInterval>().ToObjects<OvertimeInterval>()
  462. .GroupBy(x => x.Overtime.ID)
  463. .ToDictionary(x => x.Key, x => x.OrderBy(x => x.Sequence).ToArray());
  464. var rosters = model.GetTable<EmployeeRosterItem>("Rosters").ToObjects<EmployeeRosterItem>()
  465. .GroupBy(x => x.Employee.ID).ToDictionary(x => x.Key, x => x.ToArray());
  466. var employees = model.GetTable<Employee>("Employees").ToObjects<Employee>()
  467. .ToDictionary(x => x.ID, x => x);
  468. var assignments = model.GetTable<Assignment>().ToObjects<Assignment>()
  469. .GroupBy(x => new { x.Date, Employee = x.EmployeeLink.ID }).ToDictionary(x => x.Key, x => x.ToList());
  470. var daily = timesheets.GroupBy(x => new { x.Date, Employee = x.EmployeeLink.ID }).ToDictionary(x => x.Key, x => x.ToList());
  471. foreach(var (key, sheets) in daily)
  472. {
  473. var dateAssignments = assignments.GetValueOrDefault(new { key.Date, key.Employee }, new List<Assignment>());
  474. var rawArgs = new ProcessRawDataArgs(model, key.Employee, key.Date, sheets, dateAssignments);
  475. ProcessRawData(rawArgs);
  476. if (rawArgs.Cancel)
  477. {
  478. foreach(var sheet in sheets)
  479. {
  480. items.AddFailed(sheet, "Post cancelled by script.");
  481. }
  482. continue;
  483. }
  484. var activityBlocks = GetActivityBlocks(rawArgs.Assignments, rawArgs.TimeSheets);
  485. var activityArgs = new ProcessActivityBlocksArgs(model, key.Employee, key.Date, activityBlocks);
  486. ProcessActivityBlocks(activityArgs);
  487. if (activityArgs.Cancel)
  488. {
  489. foreach (var sheet in sheets)
  490. {
  491. items.AddFailed(sheet, "Post cancelled by script.");
  492. }
  493. continue;
  494. }
  495. var approvedDuration = rawArgs.TimeSheets.Aggregate(TimeSpan.Zero, (x, y) => x + y.ApprovedDuration);
  496. var leave = new List<LeaveBlock>();
  497. var workTime = new List<PaidWorkBlock>();
  498. foreach (var block in activityArgs.ActivityBlocks)
  499. {
  500. string payID;
  501. bool isLeave;
  502. if (block.Activity == Guid.Empty
  503. || !_activities.TryGetValue(block.Activity, out var activity))
  504. {
  505. if(block.Activity != Guid.Empty)
  506. {
  507. Logger.Send(LogType.Error, "", $"Error in Timesheet Timberline export: Activity {block.Activity} does not exist!");
  508. }
  509. payID = "";
  510. isLeave = false;
  511. }
  512. else
  513. {
  514. isLeave = activity.IsLeave;
  515. payID = activity.PayrollID;
  516. }
  517. if (isLeave)
  518. {
  519. leave.Add(new(payID, block.Finish - block.Start, block.TimeSheet));
  520. }
  521. else
  522. {
  523. // Leave PayID blank until we've worked out the rosters
  524. workTime.Add(new(payID, block.Finish - block.Start, "", block.TimeSheet.JobLink.JobNumber, block.TimeSheet));
  525. }
  526. }
  527. if (approvedDuration > TimeSpan.Zero)
  528. {
  529. var employee = employees.GetValueOrDefault(key.Employee);
  530. var employeeRosters = rosters.GetValueOrDefault(employee != null ? employee.ID : Guid.Empty);
  531. var overtimeID = RosterUtils.GetRoster(employeeRosters, employee?.RosterStart, key.Date)?.Overtime.ID ?? Guid.Empty;
  532. var workItems = EvaluateOvertime(workTime, overtimeID);
  533. var blockArgs = new ProcessTimeBlocksArgs(model, key.Employee, key.Date, workItems, leave);
  534. ProcessTimeBlocks(blockArgs);
  535. if (blockArgs.Cancel)
  536. {
  537. foreach (var sheet in sheets)
  538. {
  539. items.AddFailed(sheet, "Post cancelled by script.");
  540. }
  541. continue;
  542. }
  543. // Succeed all sheets, and then fail them if any of their blocks are failed.
  544. foreach (var sheet in sheets)
  545. {
  546. items.AddSuccess(sheet);
  547. }
  548. var blocks = (blockArgs.WorkBlocks as IEnumerable<IBlock>).Concat(blockArgs.LeaveBlocks);
  549. var newItems = new List<Tuple<TimesheetTimberlineItem, List<TimeSheet>>>();
  550. foreach(var block in blocks.GroupBy(x => new { x.Job, x.TaskID, x.PayrollID }, x => x))
  551. {
  552. var item = new TimesheetTimberlineItem
  553. {
  554. Employee = employee?.PayrollID ?? "",
  555. InDate = DateOnly.FromDateTime(key.Date),
  556. Job = block.Key.Job,
  557. Extra = "",
  558. Task = block.Key.TaskID,
  559. Hours = block.Sum(x => x.Duration.TotalHours),
  560. PayID = block.Key.PayrollID
  561. };
  562. var itemArgs = new ProcessItemArgs(model, key.Employee, key.Date, item);
  563. ProcessItem(itemArgs);
  564. var blockTimeSheets = block.Select(x => x.TimeSheet).ToList();
  565. if (!itemArgs.Cancel)
  566. {
  567. newItems.Add(new(itemArgs.Item, blockTimeSheets));
  568. }
  569. else
  570. {
  571. foreach(var sheet in blockTimeSheets)
  572. {
  573. (sheet as IPostable).FailPost("Post cancelled by script.");
  574. }
  575. }
  576. }
  577. foreach(var item in newItems)
  578. {
  579. if(item.Item2.All(x => x.PostedStatus == PostedStatus.Posted))
  580. {
  581. items.AddItem(item.Item1);
  582. }
  583. }
  584. }
  585. else
  586. {
  587. foreach (var sheet in sheets)
  588. {
  589. items.AddFailed(sheet, "Zero Approved Duration");
  590. }
  591. }
  592. }
  593. return items;
  594. }
  595. public IPostResult<TimeSheet> Process(IDataModel<TimeSheet> model)
  596. {
  597. var items = DoProcess(model);
  598. var dlg = new SaveFileDialog()
  599. {
  600. Filter = "CSV Files (*.csv)|*.csv"
  601. };
  602. if (dlg.ShowDialog() == true)
  603. {
  604. using var writer = new StreamWriter(dlg.FileName);
  605. using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
  606. foreach (var item in items.Items)
  607. {
  608. csv.WriteRecord(item);
  609. csv.NextRecord();
  610. }
  611. return items;
  612. }
  613. else
  614. {
  615. throw new PostCancelledException();
  616. }
  617. }
  618. public void AfterPost(IDataModel<TimeSheet> model, IPostResult<TimeSheet> result)
  619. {
  620. Script?.Execute(methodname: "AfterPost", parameters: new object[] { model });
  621. }
  622. }
  623. public class TimesheetTimberlinePosterEngine<T> : TimberlinePosterEngine<TimeSheet, TimesheetTimberlineSettings>
  624. {
  625. }
  626. }