| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- 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<InvoiceLine[]> TimeLines(Invoice invoice, InvoiceTimeCalculation timesummary)
- {
- var timelines = new Dictionary<Guid, InvoiceLineDetail>();
- var activitiesTask = Task.Run(() =>
- {
- return Client.Query(
- Filter<CustomerActivitySummary>.Where(x => x.Customer.ID).InList(invoice.CustomerLink.ID, Guid.Empty),
- Columns.None<CustomerActivitySummary>().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<CustomerActivitySummary>()
- .GroupByDictionary(x => (CustomerID: x.Customer.ID, ActivityID: x.Activity.ID));
- });
- var assignmentsTask = Task.Run(() =>
- {
- return Client.Query(
- Filter<Assignment>.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true),
- Columns.None<Assignment>()
- .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<Assignment>(x => x.Date))
- .ToArray<Assignment>();
- });
- 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 chargeperiod = !activity.Charge.ChargePeriod.Equals(TimeSpan.Zero)
- ? activity.Charge.ChargePeriod
- : TimeSpan.FromHours(1);
-
- var rounded = quantity.Ceiling(chargeperiod);
- // Rate is charge per hour, so we must divide by the charge period time, to get dollars per hour, rather than dollars per period
- // $/hr = ($/pd) * (pd/hr) = ($/pd) / (hr/pd)
- // where $/pd is ChargeRate and hr/pd = chargeperiod.TotalHours
- var rate = activity.Charge.ChargeRate / chargeperiod.TotalHours;
-
- charge = Math.Max(
- activity.Charge.FixedCharge + (rounded.TotalHours * rate),
- activity.Charge.MinimumCharge);
- }
- 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<InvoiceLine[]> PartLines(Invoice invoice, InvoiceMaterialCalculation partsummary)
- {
- var productsTask = Task.Run(() =>
- {
- return Client.Query(
- Filter<CustomerProductSummary>.Where(x => x.Customer.ID).InList(invoice.CustomerLink.ID, Guid.Empty),
- Columns.None<CustomerProductSummary>()
- .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<CustomerProductSummary>()
- .GroupByDictionary(x => (CustomerID: x.Customer.ID, ProductID: x.Product.ID));
- });
- var movementsTask = Task.Run(() =>
- {
- return Client.Query(
- Filter<StockMovement>.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true),
- Columns.None<StockMovement>()
- .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<StockMovement>();
- });
-
- var partlines = new Dictionary<Guid, InvoiceLineDetail>();
- 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<InvoiceLine[]> ExpenseLines(Invoice invoice, InvoiceExpensesCalculation expensesSummary)
- {
- var billLinesTask = Task.Run(() =>
- {
- return Client.Query(
- Filter<BillLine>.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true),
- Columns.None<BillLine>()
- .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<BillLine>();
- });
-
- var expenselines = new Dictionary<Guid, InvoiceLineDetail>();
- 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<String>? progress
- )
- {
- progress?.Report("Loading Invoice");
- var invoice = Client.Query(
- Filter<Invoice>.Where(x => x.ID).IsEqualTo(invoiceid))
- .ToObjects<Invoice>().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<InvoiceLine>().Query(
- Filter<InvoiceLine>.Where(x => x.InvoiceLink.ID).IsEqualTo(invoice.ID),
- Columns.None<InvoiceLine>().Add(x => x.ID)
- ).Rows.Select(x => x.ToObject<InvoiceLine>()).ToArray();
- new Client<InvoiceLine>().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");
- }
- }
|