using Comal.Classes; using InABox.Clients; using InABox.Core; namespace PRS.Shared { public enum InvoiceTimeCalculation { Detailed, Activity, Collapsed, } public enum InvoiceMaterialCalculation { Detailed, Product, CostCentre, Collapsed, } public static class InvoiceUtilities { private class InvoiceLineDetail { public Guid ID { get; set; } 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(); } } public static void GenerateInvoiceLines(Guid invoiceid, InvoiceTimeCalculation timesummary, InvoiceMaterialCalculation partsummary, IProgress? progress ) { CustomerActivitySummary[] activities = new CustomerActivitySummary[] { }; CustomerProductSummary[] products = new CustomerProductSummary[] { }; Assignment[] assignments = new Assignment[] { }; var movements = Array.Empty(); progress?.Report("Loading Invoice"); var invoice = new Client().Load(new Filter(x => x.ID).IsEqualTo(invoiceid)).FirstOrDefault(); progress?.Report("Loading Detail Data"); var setup = new Task[] { Task.Run(() => { var oldlines = new Client().Query( new Filter(x => x.InvoiceLink.ID).IsEqualTo(invoice.ID), Columns.None().Add(x => x.ID) ).Rows.Select(x => x.ToObject()).ToArray(); new Client().Delete(oldlines, ""); }), Task.Run(() => { activities = new Client().Query( new Filter(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) ).Rows.Select(r => r.ToObject()).ToArray(); }), Task.Run(() => { assignments = new Client().Query( new Filter(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true), null, new SortOrder(x => x.Date) ).Rows.Select(x => x.ToObject()).ToArray(); }), Task.Run(() => { products = new Client().Query( new Filter(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) ).Rows.Select(r => r.ToObject()).ToArray(); }), Task.Run(() => { movements = new Client().Query( new Filter(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true), null ).ToArray(); }) }; Task.WaitAll(setup); List updates = new List(); progress?.Report("Calculating..."); var timelines = new List(); foreach (var assignment in assignments) { 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.FirstOrDefault(x => x.Customer.ID.Equals(invoice.CustomerLink.ID) && x.Activity.ID.Equals(assignment.ActivityLink.ID)) ?? activities.FirstOrDefault(x => x.Customer.ID.Equals(Guid.Empty) && x.Activity.ID.Equals(assignment.ActivityLink.ID)) ?? new CustomerActivitySummary(); double charge = 0.0F; if (assignment.Charge.OverrideCharge) charge = quantity.TotalHours * assignment.Charge.Charge; else { double fixedcharge = activity.Charge.FixedCharge; TimeSpan chargeperiod = !activity.Charge.ChargePeriod.Equals(TimeSpan.Zero) ? activity.Charge.ChargePeriod : TimeSpan.FromHours(1); var rounded = quantity.Ceiling(chargeperiod); double multiplier = TimeSpan.FromHours(1).TotalHours / chargeperiod.TotalHours; double rate = activity.Charge.ChargeRate * multiplier; double mincharge = activity.Charge.MinimumCharge; charge = Math.Max(fixedcharge + (rounded.TotalHours * rate), mincharge); } var timeline = timelines.FirstOrDefault(x => x.ID == id); if (timeline == null) { timeline = new InvoiceLineDetail(); timeline.Description = description; timeline.TaxCode.ID = activity.Charge.TaxCode.ID; timeline.TaxCode.Synchronise(activity.Charge.TaxCode); timelines.Add(timeline); } timeline.Quantity += quantity.TotalHours; timeline.Charge += charge; } foreach (var line in timelines) { var update = new InvoiceLine(); update.InvoiceLink.ID = invoice.ID; update.Description = line.Description; update.TaxCode.ID = line.TaxCode.ID; update.TaxCode.Synchronise(line.TaxCode); update.Quantity = timesummary != InvoiceTimeCalculation.Collapsed ? line.Quantity : 1; update.ExTax = line.Charge; updates.Add(update); } var partlines = new List(); foreach (var item in movements) { 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.FirstOrDefault(x => x.Customer.ID.Equals(invoice.CustomerLink.ID) && x.Product.ID.Equals(item.Product.ID)) ?? products.FirstOrDefault(x => x.Customer.ID.Equals(Guid.Empty) && x.Product.ID.Equals(item.Product.ID)) ?? new CustomerProductSummary(); double charge = 0.0F; if (item.Charge.OverrideCharge) charge = quantity * item.Charge.Charge; else { charge = quantity * product.Charge.PriceType switch { ProductPriceType.CostPlus => 0.0F * product.Charge.Markup, _ => product.Charge.Price }; } var partline = partlines.FirstOrDefault(x => x.ID == id); if (partline == null) { partline = new InvoiceLineDetail(); partline.ID = id; partline.Description = description; partline.TaxCode.ID = product.Product.TaxCode.ID; partline.TaxCode.Synchronise(product.Product.TaxCode); partlines.Add(partline); } partline.Quantity += quantity; partline.Charge += charge; } foreach (var line in partlines) { var update = new InvoiceLine(); update.InvoiceLink.ID = invoice.ID; update.Description = line.Description; update.TaxCode.ID = line.TaxCode.ID; update.TaxCode.Synchronise(line.TaxCode); update.Quantity = new[] { InvoiceMaterialCalculation.Detailed, InvoiceMaterialCalculation.Product}.Contains(partsummary) ? line.Quantity : 1.0F; update.ExTax = line.Charge; updates.Add(update); } progress?.Report("Creating Invoice Lines"); if (updates.Any()) new Client().Save(updates, "Recalculating Invoice from Time and Materials"); } } }