TimesheetTimberlinePoster.cs 25 KB

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