TimesheetTimberlinePoster.cs 25 KB

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