using Comal.Classes; using InABox.Clients; using InABox.Core; using PRSDimensionUtils; namespace PRS.Shared; public enum InvoiceTimeCalculation { Detailed, Activity, Collapsed, } public enum InvoiceMaterialCalculation { Detailed, Product, CostCentre, Collapsed, } public enum InvoiceExpensesCalculation { Detailed, Collapsed, } public static class InvoiceUtilities { private class InvoiceLineDetail { public String Description { get; set; } public TaxCodeLink TaxCode { get; set; } public double Quantity { get; set; } public double Charge { get; set; } public InvoiceLineDetail() { TaxCode = new TaxCodeLink(); } } private static async Task TimeLines(Invoice invoice, InvoiceTimeCalculation timesummary) { var timelines = new Dictionary(); var activitiesTask = Task.Run(() => { return Client.Query( Filter.Where(x => x.Customer.ID).InList(invoice.CustomerLink.ID, Guid.Empty), Columns.None().Add(x => x.Customer.ID) .Add(x => x.Activity.ID) .Add(x => x.Activity.Code) .Add(x => x.Activity.Description) .Add(x => x.Charge.TaxCode.ID) .Add(x => x.Charge.TaxCode.Rate) .Add(x => x.Charge.Chargeable) .Add(x => x.Charge.FixedCharge) .Add(x => x.Charge.ChargeRate) .Add(x => x.Charge.ChargePeriod) .Add(x => x.Charge.MinimumCharge)) .ToObjects() .GroupByDictionary(x => (CustomerID: x.Customer.ID, ActivityID: x.Activity.ID)); }); var assignmentsTask = Task.Run(() => { return Client.Query( Filter.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true), Columns.None() .Add(x => x.ID) .Add(x => x.ActivityLink.ID) .Add(x => x.ActivityLink.Description) .Add(x => x.Date) .Add(x => x.Description) .Add(x => x.Charge.OverrideCharge) .Add(x => x.Charge.Charge) .Add(x => x.Charge.OverrideQuantity) .Add(x => x.Charge.Quantity) .Add(x => x.Actual.Duration), new SortOrder(x => x.Date)) .ToArray(); }); var activities = await activitiesTask; foreach (var assignment in await assignmentsTask) { var id = timesummary switch { InvoiceTimeCalculation.Detailed => assignment.ID, InvoiceTimeCalculation.Activity => assignment.ActivityLink.ID, _ => Guid.Empty }; var description = timesummary switch { InvoiceTimeCalculation.Detailed => string.Format("{0:dd MMM yy} - {1}", assignment.Date, assignment.Description), InvoiceTimeCalculation.Activity => assignment.ActivityLink.Description, _ => "Labour" }; var quantity = assignment.Charge.OverrideQuantity ? TimeSpan.FromHours(assignment.Charge.Quantity) : assignment.Actual.Duration; var activity = activities.GetValueOrDefault((invoice.CustomerLink.ID, assignment.ActivityLink.ID))?.FirstOrDefault() ?? activities.GetValueOrDefault((Guid.Empty, assignment.ActivityLink.ID))?.FirstOrDefault() ?? new CustomerActivitySummary(); double charge; if (assignment.Charge.OverrideCharge) { charge = quantity.TotalHours * assignment.Charge.Charge; } else { var fixedcharge = activity.Charge.FixedCharge; var chargeperiod = !activity.Charge.ChargePeriod.Equals(TimeSpan.Zero) ? activity.Charge.ChargePeriod : TimeSpan.FromHours(1); var rounded = quantity.Ceiling(chargeperiod); var multiplier = TimeSpan.FromHours(1).TotalHours / chargeperiod.TotalHours; var rate = activity.Charge.ChargeRate * multiplier; var mincharge = activity.Charge.MinimumCharge; charge = Math.Max(fixedcharge + (rounded.TotalHours * rate), mincharge); } if(!timelines.TryGetValue(id, out var timeline)) { timeline = new InvoiceLineDetail { Description = description }; timeline.TaxCode.CopyFrom(activity.Charge.TaxCode); timelines.Add(id, timeline); } timeline.Quantity += quantity.TotalHours; timeline.Charge += charge; } return timelines.Values.ToArray(line => { var update = new InvoiceLine(); update.InvoiceLink.ID = invoice.ID; update.Description = line.Description; update.TaxCode.CopyFrom(line.TaxCode); update.Quantity = timesummary != InvoiceTimeCalculation.Collapsed ? line.Quantity : 1; update.ExTax = line.Charge; return update; }); } private static async Task PartLines(Invoice invoice, InvoiceMaterialCalculation partsummary) { var productsTask = Task.Run(() => { return Client.Query( Filter.Where(x => x.Customer.ID).InList(invoice.CustomerLink.ID, Guid.Empty), Columns.None() .Add(x => x.Customer.ID) .Add(x => x.Product.ID) .Add(x => x.Product.Code) .Add(x => x.Product.Name) .Add(x => x.Product.TaxCode.ID) .Add(x => x.Product.TaxCode.Rate) .Add(x => x.Charge.Chargeable) .Add(x => x.Charge.PriceType) .Add(x => x.Charge.Price) .Add(x => x.Charge.Markup)) .ToObjects() .GroupByDictionary(x => (CustomerID: x.Customer.ID, ProductID: x.Product.ID)); }); var movementsTask = Task.Run(() => { return Client.Query( Filter.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true), Columns.None() .Add(x => x.ID) .Add(x => x.Qty) .Add(x => x.Product.ID) .Add(x => x.Product.Name) .Add(x => x.Product.CostCentre.ID) .Add(x => x.Product.CostCentre.Description) .Add(x => x.Style.Code) .Add(x => x.Dimensions.UnitSize) .Add(x => x.Charge.OverrideCharge) .Add(x => x.Charge.Charge) .Add(x => x.Charge.OverrideQuantity) .Add(x => x.Charge.Quantity)) .ToArray(); }); var partlines = new Dictionary(); var products = await productsTask; foreach (var item in await movementsTask) { var id = partsummary switch { InvoiceMaterialCalculation.Detailed => item.ID, InvoiceMaterialCalculation.Product => item.Product.ID, InvoiceMaterialCalculation.CostCentre => item.Product.CostCentre.ID, _ => Guid.Empty }; var description = partsummary switch { InvoiceMaterialCalculation.Detailed => $"{item.Product.Name}: {item.Style.Code}; {item.Dimensions.UnitSize}", InvoiceMaterialCalculation.Product => item.Product.Name, InvoiceMaterialCalculation.CostCentre => item.Product.CostCentre.Description, _ => "Materials" }; var quantity = item.Charge.OverrideQuantity ? item.Charge.Quantity : item.Qty; var product = products.GetValueOrDefault((invoice.CustomerLink.ID, item.Product.ID))?.FirstOrDefault() ?? products.GetValueOrDefault((Guid.Empty, item.Product.ID))?.FirstOrDefault() ?? new CustomerProductSummary(); double charge; if (item.Charge.OverrideCharge) { charge = quantity * item.Charge.Charge; } else { charge = quantity * (product.Charge.PriceType switch { ProductPriceType.CostPlus => 1 + product.Charge.Markup / 100, _ => product.Charge.Price }); } if(!partlines.TryGetValue(id, out var partline)) { partline = new InvoiceLineDetail { Description = description }; partline.TaxCode.CopyFrom(product.Product.TaxCode); partlines.Add(id, partline); } partline.Quantity += quantity; partline.Charge += charge; } return partlines.Values.ToArray(line => { var update = new InvoiceLine(); update.InvoiceLink.ID = invoice.ID; update.Description = line.Description; update.TaxCode.CopyFrom(line.TaxCode); update.Quantity = new[] { InvoiceMaterialCalculation.Detailed, InvoiceMaterialCalculation.Product }.Contains(partsummary) ? line.Quantity : 1.0F; update.ExTax = line.Charge; return update; }); } private static async Task ExpenseLines(Invoice invoice, InvoiceExpensesCalculation expensesSummary) { var billLinesTask = Task.Run(() => { return Client.Query( Filter.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true), Columns.None() .Add(x => x.ID) .Add(x => x.Description) .Add(x => x.ExTax) .Add(x => x.TaxCode.ID) .Add(x => x.TaxCode.Rate) .Add(x => x.Charge.OverrideCharge) .Add(x => x.Charge.Charge) .Add(x => x.Charge.OverrideQuantity) .Add(x => x.Charge.Quantity)) .ToArray(); }); var expenselines = new Dictionary(); foreach (var item in await billLinesTask) { var id = expensesSummary switch { InvoiceExpensesCalculation.Detailed => item.ID, _ => Guid.Empty }; var description = expensesSummary switch { InvoiceExpensesCalculation.Detailed => $"{item.Description}", _ => "Expenses" }; var quantity = item.Charge.OverrideQuantity ? item.Charge.Quantity : 1.0; double charge; if (item.Charge.OverrideCharge) { charge = quantity * item.Charge.Charge; } else { charge = quantity * item.ExTax * (1 + invoice.CustomerLink.Markup / 100); } if(!expenselines.TryGetValue(id, out var expenseLine)) { expenseLine = new InvoiceLineDetail { Description = description }; expenseLine.TaxCode.CopyFrom(item.TaxCode); expenselines.Add(id, expenseLine); } expenseLine.Quantity += quantity; expenseLine.Charge += charge; } return expenselines.Values.ToArray(line => { var update = new InvoiceLine(); update.InvoiceLink.ID = invoice.ID; update.Description = line.Description; update.TaxCode.CopyFrom(line.TaxCode); update.Quantity = expensesSummary != InvoiceExpensesCalculation.Collapsed ? line.Quantity : 1.0F; update.ExTax = line.Charge; return update; }); } public static void GenerateInvoiceLines( Guid invoiceid, InvoiceTimeCalculation timesummary, InvoiceMaterialCalculation partsummary, InvoiceExpensesCalculation expensesSummary, IProgress? progress ) { progress?.Report("Loading Invoice"); var invoice = Client.Query( Filter.Where(x => x.ID).IsEqualTo(invoiceid)) .ToObjects().FirstOrDefault(); if(invoice is null) { Logger.Send(LogType.Error, "", $"Could not find invoice with ID {invoiceid}"); return; } progress?.Report("Loading Detail Data"); var deleteOldTask = Task.Run(() => { var oldlines = new Client().Query( Filter.Where(x => x.InvoiceLink.ID).IsEqualTo(invoice.ID), Columns.None().Add(x => x.ID) ).Rows.Select(x => x.ToObject()).ToArray(); new Client().Delete(oldlines, ""); }); var timeLinesTask = TimeLines(invoice, timesummary); var partLinesTask = PartLines(invoice, partsummary); var expenseLinesTask = ExpenseLines(invoice, expensesSummary); progress?.Report("Calculating..."); var updates = CoreUtils.Concatenate( timeLinesTask.Result, partLinesTask.Result, expenseLinesTask.Result); progress?.Report("Creating Invoice Lines"); Client.Save(updates, "Recalculating Invoice from Time and Materials"); } }