InvoiceUtilities.cs 15 KB

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