InvoiceUtilities.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. using Comal.Classes;
  2. using InABox.Clients;
  3. using InABox.Core;
  4. using PRSDimensionUtils;
  5. using Syncfusion.Windows.Controls.Grid;
  6. namespace PRS.Shared;
  7. public enum InvoiceTimeCalculation
  8. {
  9. Detailed,
  10. Activity,
  11. Collapsed,
  12. }
  13. public enum InvoiceMaterialCalculation
  14. {
  15. Detailed,
  16. Product,
  17. CostCentre,
  18. Collapsed,
  19. }
  20. public enum InvoiceExpensesCalculation
  21. {
  22. Detailed,
  23. Collapsed,
  24. }
  25. public enum InvoicingStrategy
  26. {
  27. InvoiceOnIssue,
  28. InvoiceOnPurchase
  29. }
  30. public static class InvoiceUtilities
  31. {
  32. private class InvoiceLineDetail
  33. {
  34. public String Description { get; set; }
  35. public TaxCodeLink TaxCode { get; set; }
  36. public double Quantity { get; set; }
  37. public double Charge { get; set; }
  38. public InvoiceLineDetail()
  39. {
  40. TaxCode = new TaxCodeLink();
  41. }
  42. }
  43. private static async Task<InvoiceLine[]> TimeLines(Invoice invoice, InvoiceTimeCalculation timesummary)
  44. {
  45. var timelines = new Dictionary<Guid, InvoiceLineDetail>();
  46. var activitiesTask = Task.Run(() =>
  47. {
  48. return Client.Query(
  49. Filter<CustomerActivitySummary>.Where(x => x.Customer.ID).InList(invoice.Customer.ID, Guid.Empty),
  50. Columns.None<CustomerActivitySummary>().Add(x => x.Customer.ID)
  51. .Add(x => x.Activity.ID)
  52. .Add(x => x.Activity.Code)
  53. .Add(x => x.Activity.Description)
  54. .Add(x => x.Charge.TaxCode.ID)
  55. .Add(x => x.Charge.TaxCode.Rate)
  56. .Add(x => x.Charge.Chargeable)
  57. .Add(x => x.Charge.FixedCharge)
  58. .Add(x => x.Charge.ChargeRate)
  59. .Add(x => x.Charge.ChargePeriod)
  60. .Add(x => x.Charge.MinimumCharge))
  61. .ToObjects<CustomerActivitySummary>()
  62. .GroupByDictionary(x => (CustomerID: x.Customer.ID, ActivityID: x.Activity.ID));
  63. });
  64. var assignmentsTask = Task.Run(() =>
  65. {
  66. return Client.Query(
  67. Filter<Assignment>.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true),
  68. Columns.None<Assignment>()
  69. .Add(x => x.ID)
  70. .Add(x => x.Activity.ID)
  71. .Add(x => x.Activity.Description)
  72. .Add(x => x.Date)
  73. .Add(x => x.Title)
  74. .Add(x => x.Description)
  75. .Add(x => x.Charge.OverrideCharge)
  76. .Add(x => x.Charge.Charge)
  77. .Add(x => x.Charge.OverrideQuantity)
  78. .Add(x => x.Charge.Quantity)
  79. .Add(x => x.Actual.Duration),
  80. new SortOrder<Assignment>(x => x.Date))
  81. .ToArray<Assignment>();
  82. });
  83. var activities = await activitiesTask;
  84. foreach (var assignment in await assignmentsTask)
  85. {
  86. var id = timesummary switch
  87. {
  88. InvoiceTimeCalculation.Detailed => assignment.ID,
  89. InvoiceTimeCalculation.Activity => assignment.Activity.ID,
  90. _ => Guid.Empty
  91. };
  92. var description = timesummary switch
  93. {
  94. InvoiceTimeCalculation.Detailed => $"{assignment.Date:dd MMM yy} - {assignment.Title}: {assignment.Description}",
  95. InvoiceTimeCalculation.Activity => assignment.Activity.Description,
  96. _ => "Labour"
  97. };
  98. var quantity = assignment.Charge.OverrideQuantity
  99. ? TimeSpan.FromHours(assignment.Charge.Quantity)
  100. : assignment.Actual.Duration;
  101. var activity = activities.GetValueOrDefault((invoice.Customer.ID, assignment.Activity.ID))?.FirstOrDefault()
  102. ?? activities.GetValueOrDefault((Guid.Empty, assignment.Activity.ID))?.FirstOrDefault()
  103. ?? new CustomerActivitySummary();
  104. double charge;
  105. if (assignment.Charge.OverrideCharge)
  106. {
  107. charge = quantity.TotalHours * assignment.Charge.Charge;
  108. }
  109. else
  110. {
  111. var chargeperiod = !activity.Charge.ChargePeriod.Equals(TimeSpan.Zero)
  112. ? activity.Charge.ChargePeriod
  113. : TimeSpan.FromHours(1);
  114. quantity = quantity.Ceiling(chargeperiod);
  115. // Rate is charge per hour, so we must divide by the charge period time, to get dollars per hour, rather than dollars per period
  116. // $/hr = ($/pd) * (pd/hr) = ($/pd) / (hr/pd)
  117. // where $/pd is ChargeRate and hr/pd = chargeperiod.TotalHours
  118. var rate = activity.Charge.ChargeRate / chargeperiod.TotalHours;
  119. charge = Math.Max(
  120. activity.Charge.FixedCharge + (quantity.TotalHours * rate),
  121. activity.Charge.MinimumCharge);
  122. }
  123. if(!timelines.TryGetValue(id, out var timeline))
  124. {
  125. timeline = new InvoiceLineDetail
  126. {
  127. Description = description
  128. };
  129. timeline.TaxCode.CopyFrom(activity.Charge.TaxCode);
  130. timelines.Add(id, timeline);
  131. }
  132. timeline.Quantity += quantity.TotalHours;
  133. timeline.Charge += charge;
  134. }
  135. return timelines.Values.ToArray(line =>
  136. {
  137. var update = new InvoiceLine();
  138. update.Invoice.ID = invoice.ID;
  139. update.Description = line.Description;
  140. update.TaxCode.CopyFrom(line.TaxCode);
  141. update.Quantity = timesummary != InvoiceTimeCalculation.Collapsed ? line.Quantity : 1;
  142. update.ExTax = line.Charge;
  143. return update;
  144. });
  145. }
  146. private static async Task<InvoiceLine[]> PartLines(Invoice invoice, InvoiceMaterialCalculation partsummary, InvoicingStrategy strategy)
  147. {
  148. var customerProductsTask = Task.Run(() =>
  149. {
  150. return Client.Query(
  151. Filter<CustomerProduct>.Where(x => x.Customer.ID).InList(invoice.Customer.ID, Guid.Empty),
  152. Columns.None<CustomerProduct>()
  153. .Add(x => x.Customer.ID)
  154. .Add(x => x.Product.ID)
  155. .Add(x => x.Discount))
  156. .ToObjects<CustomerProduct>()
  157. .GroupByDictionary(x => (CustomerID: x.Customer.ID, ProductID: x.Product.ID));
  158. });
  159. var stockMovementFilter = Filter<StockMovement>.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID)
  160. .And(x => x.Charge.Chargeable).IsEqualTo(true);
  161. var productInstancesTask = Task.Run(() =>
  162. {
  163. return Client.Query(
  164. Filter<ProductInstance>.Where(x => x.Charge.Chargeable).IsEqualTo(true)
  165. .And(x => x.Product.ID).InQuery(
  166. stockMovementFilter,
  167. x => x.Product.ID),
  168. Columns.None<ProductInstance>()
  169. .Add(x => x.Product.ID)
  170. .Add(x => x.Style.ID)
  171. .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
  172. .Add(x => x.Charge.PriceType)
  173. .Add(x => x.Charge.Price)
  174. .Add(x => x.Charge.Markup))
  175. .ToObjects<ProductInstance>()
  176. .GroupBy(x => (ProductID: x.Product.ID, StyleID: x.Style.ID, x.Dimensions))
  177. .ToDictionary(x => x.Key, x => x.First());
  178. });
  179. var movementsTask = Task.Run(() =>
  180. {
  181. return Client.Query(
  182. stockMovementFilter,
  183. Columns.None<StockMovement>()
  184. .Add(x => x.ID)
  185. .Add(x => x.Qty)
  186. .Add(x => x.Units)
  187. .Add(x => x.Cost)
  188. .Add(x => x.Product.ID)
  189. .Add(x => x.Product.Name)
  190. .Add(x => x.Product.CostCentre.ID)
  191. .Add(x => x.Product.CostCentre.Description)
  192. .Add(x => x.Product.DefaultMarkUp)
  193. .Add(x => x.Product.TaxCode.ID)
  194. .Add(x => x.Product.TaxCode.Rate)
  195. .Add(x => x.Product.DefaultMarkUp)
  196. .Add(x => x.Style.ID)
  197. .Add(x => x.Style.Code)
  198. .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
  199. .Add(x => x.Charge.OverrideCharge)
  200. .Add(x => x.Charge.Charge)
  201. .Add(x => x.Charge.OverrideQuantity)
  202. .Add(x => x.Charge.Quantity))
  203. .ToArray<StockMovement>();
  204. });
  205. var partlines = new Dictionary<Guid, InvoiceLineDetail>();
  206. var customerProducts = await customerProductsTask;
  207. var productInstances = await productInstancesTask;
  208. foreach (var item in await movementsTask)
  209. {
  210. var id = partsummary switch
  211. {
  212. InvoiceMaterialCalculation.Detailed => item.ID,
  213. InvoiceMaterialCalculation.Product => item.Product.ID,
  214. InvoiceMaterialCalculation.CostCentre => item.Product.CostCentre.ID,
  215. _ => Guid.Empty
  216. };
  217. var description = partsummary switch
  218. {
  219. InvoiceMaterialCalculation.Detailed => $"{item.Product.Name}: {item.Style.Code}; {item.Dimensions.UnitSize}",
  220. InvoiceMaterialCalculation.Product => item.Product.Name,
  221. InvoiceMaterialCalculation.CostCentre => item.Product.CostCentre.Description,
  222. _ => "Materials"
  223. };
  224. var instance =
  225. productInstances.GetValueOrDefault((item.Product.ID, item.Style.ID, item.Dimensions));
  226. var customerProduct =
  227. customerProducts.GetValueOrDefault((invoice.Customer.ID, item.Product.ID))?.FirstOrDefault();
  228. // Quantity only to be used for the actual invoice line quantity if in Detailed version, otherwise we just use '1' as the quantity for the invoice line.
  229. var quantity = item.Charge.OverrideQuantity
  230. ? item.Charge.Quantity
  231. : item.Units * (strategy == InvoicingStrategy.InvoiceOnIssue ? -1 : 1);
  232. var discountMultiplier = 1 - (customerProduct?.Discount ?? 0) / 100;
  233. double charge;
  234. if (item.Charge.OverrideCharge)
  235. {
  236. charge = quantity * item.Charge.Charge;
  237. }
  238. else
  239. {
  240. if(instance is not null)
  241. {
  242. charge = quantity * (instance.Charge.PriceType switch
  243. {
  244. ProductPriceType.CostPlus => item.Cost * (1 + instance.Charge.Markup / 100),
  245. _ => instance.Charge.Price
  246. }) * discountMultiplier;
  247. }
  248. else
  249. {
  250. charge = quantity * item.Cost * item.Product.DefaultMarkUp * discountMultiplier;
  251. }
  252. }
  253. if(!partlines.TryGetValue(id, out var partline))
  254. {
  255. partline = new InvoiceLineDetail
  256. {
  257. Description = description
  258. };
  259. if(partsummary != InvoiceMaterialCalculation.Detailed)
  260. {
  261. partline.Quantity = 1;
  262. }
  263. partline.TaxCode.CopyFrom(item.Product.TaxCode);
  264. partlines.Add(id, partline);
  265. }
  266. if(partsummary == InvoiceMaterialCalculation.Detailed)
  267. {
  268. partline.Quantity += quantity;
  269. }
  270. partline.Charge += charge;
  271. }
  272. return partlines.Values.ToArray(line =>
  273. {
  274. var update = new InvoiceLine();
  275. update.Invoice.ID = invoice.ID;
  276. update.Description = line.Description;
  277. update.TaxCode.CopyFrom(line.TaxCode);
  278. update.Quantity = new[] { InvoiceMaterialCalculation.Detailed, InvoiceMaterialCalculation.Product }.Contains(partsummary) ? line.Quantity : 1.0F;
  279. update.ExTax = line.Charge;
  280. return update;
  281. });
  282. }
  283. private static async Task<InvoiceLine[]> ExpenseLines(Invoice invoice, InvoiceExpensesCalculation expensesSummary)
  284. {
  285. var billLinesTask = Task.Run(() =>
  286. {
  287. return Client.Query(
  288. Filter<BillLine>.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true),
  289. Columns.None<BillLine>()
  290. .Add(x => x.ID)
  291. .Add(x => x.Description)
  292. .Add(x => x.ExTax)
  293. .Add(x => x.TaxCode.ID)
  294. .Add(x => x.TaxCode.Rate)
  295. .Add(x => x.Charge.OverrideCharge)
  296. .Add(x => x.Charge.Charge)
  297. .Add(x => x.Charge.OverrideQuantity)
  298. .Add(x => x.Charge.Quantity))
  299. .ToArray<BillLine>();
  300. });
  301. var expenselines = new Dictionary<Guid, InvoiceLineDetail>();
  302. foreach (var item in await billLinesTask)
  303. {
  304. var id = expensesSummary switch
  305. {
  306. InvoiceExpensesCalculation.Detailed => item.ID,
  307. _ => Guid.Empty
  308. };
  309. var description = expensesSummary switch
  310. {
  311. InvoiceExpensesCalculation.Detailed => $"{item.Description}",
  312. _ => "Expenses"
  313. };
  314. var quantity = item.Charge.OverrideQuantity
  315. ? item.Charge.Quantity
  316. : 1.0;
  317. double charge;
  318. if (item.Charge.OverrideCharge)
  319. {
  320. charge = quantity * item.Charge.Charge;
  321. }
  322. else
  323. {
  324. charge = quantity * item.ExTax * (1 + invoice.Customer.Markup / 100);
  325. }
  326. if(!expenselines.TryGetValue(id, out var expenseLine))
  327. {
  328. expenseLine = new InvoiceLineDetail
  329. {
  330. Description = description
  331. };
  332. expenseLine.TaxCode.CopyFrom(item.TaxCode);
  333. expenselines.Add(id, expenseLine);
  334. }
  335. expenseLine.Quantity += quantity;
  336. expenseLine.Charge += charge;
  337. }
  338. return expenselines.Values.ToArray(line =>
  339. {
  340. var update = new InvoiceLine();
  341. update.Invoice.ID = invoice.ID;
  342. update.Description = line.Description;
  343. update.TaxCode.CopyFrom(line.TaxCode);
  344. update.Quantity = expensesSummary != InvoiceExpensesCalculation.Collapsed ? line.Quantity : 1.0F;
  345. update.ExTax = line.Charge;
  346. return update;
  347. });
  348. }
  349. public static void GenerateInvoiceLines(
  350. Guid invoiceid,
  351. InvoiceTimeCalculation timesummary,
  352. InvoiceMaterialCalculation partsummary,
  353. InvoiceExpensesCalculation expensesSummary,
  354. InvoicingStrategy strategy,
  355. IProgress<String>? progress
  356. )
  357. {
  358. progress?.Report("Loading Invoice");
  359. var invoice = Client.Query(
  360. Filter<Invoice>.Where(x => x.ID).IsEqualTo(invoiceid))
  361. .ToObjects<Invoice>().FirstOrDefault();
  362. if(invoice is null)
  363. {
  364. Logger.Send(LogType.Error, "", $"Could not find invoice with ID {invoiceid}");
  365. return;
  366. }
  367. progress?.Report("Loading Detail Data");
  368. var deleteOldTask = Task.Run(() =>
  369. {
  370. var oldlines = new Client<InvoiceLine>().Query(
  371. Filter<InvoiceLine>.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID),
  372. Columns.None<InvoiceLine>().Add(x => x.ID)
  373. ).Rows.Select(x => x.ToObject<InvoiceLine>()).ToArray();
  374. new Client<InvoiceLine>().Delete(oldlines, "");
  375. });
  376. var timeLinesTask = TimeLines(invoice, timesummary);
  377. var partLinesTask = PartLines(invoice, partsummary, strategy);
  378. var expenseLinesTask = ExpenseLines(invoice, expensesSummary);
  379. progress?.Report("Calculating...");
  380. var updates = CoreUtils.Concatenate(
  381. timeLinesTask.Result,
  382. partLinesTask.Result,
  383. expenseLinesTask.Result);
  384. progress?.Report("Creating Invoice Lines");
  385. Client.Save(updates, "Recalculating Invoice from Time and Materials");
  386. }
  387. }