|
|
@@ -2,6 +2,7 @@ using Comal.Classes;
|
|
|
using InABox.Clients;
|
|
|
using InABox.Core;
|
|
|
using PRSDimensionUtils;
|
|
|
+using Syncfusion.Windows.Controls.Grid;
|
|
|
|
|
|
namespace PRS.Shared;
|
|
|
|
|
|
@@ -27,6 +28,12 @@ public enum InvoiceExpensesCalculation
|
|
|
Collapsed,
|
|
|
}
|
|
|
|
|
|
+public enum InvoicingStrategy
|
|
|
+{
|
|
|
+ InvoiceOnIssue,
|
|
|
+ InvoiceOnPurchase
|
|
|
+}
|
|
|
+
|
|
|
public static class InvoiceUtilities
|
|
|
{
|
|
|
|
|
|
@@ -120,12 +127,8 @@ public static class InvoiceUtilities
|
|
|
var chargeperiod = !activity.Charge.ChargePeriod.Equals(TimeSpan.Zero)
|
|
|
? activity.Charge.ChargePeriod
|
|
|
: TimeSpan.FromHours(1);
|
|
|
-
|
|
|
- // Here we adjust the period, essentially; should this update the actual 'quantity' we are using for this line?
|
|
|
- // It seems no, but just checking.
|
|
|
|
|
|
- // Yes, round up actual quantity.
|
|
|
- var rounded = quantity.Ceiling(chargeperiod);
|
|
|
+ quantity = 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)
|
|
|
@@ -133,7 +136,7 @@ public static class InvoiceUtilities
|
|
|
var rate = activity.Charge.ChargeRate / chargeperiod.TotalHours;
|
|
|
|
|
|
charge = Math.Max(
|
|
|
- activity.Charge.FixedCharge + (rounded.TotalHours * rate),
|
|
|
+ activity.Charge.FixedCharge + (quantity.TotalHours * rate),
|
|
|
activity.Charge.MinimumCharge);
|
|
|
}
|
|
|
|
|
|
@@ -162,39 +165,59 @@ public static class InvoiceUtilities
|
|
|
return update;
|
|
|
});
|
|
|
}
|
|
|
- private static async Task<InvoiceLine[]> PartLines(Invoice invoice, InvoiceMaterialCalculation partsummary)
|
|
|
+ private static async Task<InvoiceLine[]> PartLines(Invoice invoice, InvoiceMaterialCalculation partsummary, InvoicingStrategy strategy)
|
|
|
{
|
|
|
- var productsTask = Task.Run(() =>
|
|
|
+ var customerProductsTask = Task.Run(() =>
|
|
|
{
|
|
|
return Client.Query(
|
|
|
- Filter<CustomerProductSummary>.Where(x => x.Customer.ID).InList(invoice.Customer.ID, Guid.Empty),
|
|
|
- Columns.None<CustomerProductSummary>()
|
|
|
+ Filter<CustomerProduct>.Where(x => x.Customer.ID).InList(invoice.Customer.ID, Guid.Empty),
|
|
|
+ Columns.None<CustomerProduct>()
|
|
|
.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.Discount))
|
|
|
+ .ToObjects<CustomerProduct>()
|
|
|
+ .GroupByDictionary(x => (CustomerID: x.Customer.ID, ProductID: x.Product.ID));
|
|
|
+ });
|
|
|
+ var stockMovementFilter = Filter<StockMovement>.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID)
|
|
|
+ .And(x => x.Charge.Chargeable).IsEqualTo(true);
|
|
|
+ var productInstancesTask = Task.Run(() =>
|
|
|
+ {
|
|
|
+ return Client.Query(
|
|
|
+ Filter<ProductInstance>.Where(x => x.Charge.Chargeable).IsEqualTo(true)
|
|
|
+ .And(x => x.Product.ID).InQuery(
|
|
|
+ stockMovementFilter,
|
|
|
+ x => x.Product.ID),
|
|
|
+ Columns.None<ProductInstance>()
|
|
|
+ .Add(x => x.Product.ID)
|
|
|
+ .Add(x => x.Style.ID)
|
|
|
+ .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
|
|
|
.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));
|
|
|
+ .ToObjects<ProductInstance>()
|
|
|
+ .GroupBy(x => (ProductID: x.Product.ID, StyleID: x.Style.ID, x.Dimensions))
|
|
|
+ .ToDictionary(x => x.Key, x => x.First());
|
|
|
});
|
|
|
var movementsTask = Task.Run(() =>
|
|
|
{
|
|
|
return Client.Query(
|
|
|
- Filter<StockMovement>.Where(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true),
|
|
|
+ stockMovementFilter,
|
|
|
Columns.None<StockMovement>()
|
|
|
.Add(x => x.ID)
|
|
|
.Add(x => x.Qty)
|
|
|
+ .Add(x => x.Units)
|
|
|
+ .Add(x => x.Cost)
|
|
|
.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.Product.DefaultMarkUp)
|
|
|
+ .Add(x => x.Product.TaxCode.ID)
|
|
|
+ .Add(x => x.Product.TaxCode.Rate)
|
|
|
+ .Add(x => x.Product.DefaultMarkUp)
|
|
|
+ .Add(x => x.Style.ID)
|
|
|
.Add(x => x.Style.Code)
|
|
|
- .Add(x => x.Dimensions.UnitSize)
|
|
|
+ .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
|
|
|
.Add(x => x.Charge.OverrideCharge)
|
|
|
.Add(x => x.Charge.Charge)
|
|
|
.Add(x => x.Charge.OverrideQuantity)
|
|
|
@@ -204,7 +227,8 @@ public static class InvoiceUtilities
|
|
|
|
|
|
var partlines = new Dictionary<Guid, InvoiceLineDetail>();
|
|
|
|
|
|
- var products = await productsTask;
|
|
|
+ var customerProducts = await customerProductsTask;
|
|
|
+ var productInstances = await productInstancesTask;
|
|
|
foreach (var item in await movementsTask)
|
|
|
{
|
|
|
var id = partsummary switch
|
|
|
@@ -223,16 +247,17 @@ public static class InvoiceUtilities
|
|
|
_ => "Materials"
|
|
|
};
|
|
|
|
|
|
- // Quantity only to be used for the actual invoice line quantity if in Detailed version.
|
|
|
-
|
|
|
+ var instance =
|
|
|
+ productInstances.GetValueOrDefault((item.Product.ID, item.Style.ID, item.Dimensions));
|
|
|
+ var customerProduct =
|
|
|
+ customerProducts.GetValueOrDefault((invoice.Customer.ID, item.Product.ID))?.FirstOrDefault();
|
|
|
+
|
|
|
+ // 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.
|
|
|
var quantity = item.Charge.OverrideQuantity
|
|
|
? item.Charge.Quantity
|
|
|
- : 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.
|
|
|
-
|
|
|
- var product =
|
|
|
- products.GetValueOrDefault((invoice.Customer.ID, item.Product.ID))?.FirstOrDefault()
|
|
|
- ?? products.GetValueOrDefault((Guid.Empty, item.Product.ID))?.FirstOrDefault()
|
|
|
- ?? new CustomerProductSummary();
|
|
|
+ : item.Units * (strategy == InvoicingStrategy.InvoiceOnIssue ? -1 : 1);
|
|
|
+
|
|
|
+ var discountMultiplier = 1 - (customerProduct?.Discount ?? 0) / 100;
|
|
|
|
|
|
double charge;
|
|
|
if (item.Charge.OverrideCharge)
|
|
|
@@ -241,11 +266,18 @@ public static class InvoiceUtilities
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- charge = quantity * (product.Charge.PriceType switch
|
|
|
+ if(instance is not null)
|
|
|
+ {
|
|
|
+ charge = quantity * (instance.Charge.PriceType switch
|
|
|
+ {
|
|
|
+ ProductPriceType.CostPlus => item.Cost * (1 + instance.Charge.Markup / 100),
|
|
|
+ _ => instance.Charge.Price
|
|
|
+ }) * discountMultiplier;
|
|
|
+ }
|
|
|
+ else
|
|
|
{
|
|
|
- ProductPriceType.CostPlus => 1 + product.Charge.Markup / 100,
|
|
|
- _ => product.Charge.Price
|
|
|
- });
|
|
|
+ charge = quantity * item.Cost * item.Product.DefaultMarkUp * discountMultiplier;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
if(!partlines.TryGetValue(id, out var partline))
|
|
|
@@ -254,11 +286,18 @@ public static class InvoiceUtilities
|
|
|
{
|
|
|
Description = description
|
|
|
};
|
|
|
- partline.TaxCode.CopyFrom(product.Product.TaxCode);
|
|
|
+ if(partsummary != InvoiceMaterialCalculation.Detailed)
|
|
|
+ {
|
|
|
+ partline.Quantity = 1;
|
|
|
+ }
|
|
|
+ partline.TaxCode.CopyFrom(item.Product.TaxCode);
|
|
|
partlines.Add(id, partline);
|
|
|
}
|
|
|
|
|
|
- partline.Quantity += quantity;
|
|
|
+ if(partsummary == InvoiceMaterialCalculation.Detailed)
|
|
|
+ {
|
|
|
+ partline.Quantity += quantity;
|
|
|
+ }
|
|
|
partline.Charge += charge;
|
|
|
}
|
|
|
|
|
|
@@ -353,6 +392,7 @@ public static class InvoiceUtilities
|
|
|
InvoiceTimeCalculation timesummary,
|
|
|
InvoiceMaterialCalculation partsummary,
|
|
|
InvoiceExpensesCalculation expensesSummary,
|
|
|
+ InvoicingStrategy strategy,
|
|
|
IProgress<String>? progress
|
|
|
)
|
|
|
{
|
|
|
@@ -378,7 +418,7 @@ public static class InvoiceUtilities
|
|
|
});
|
|
|
|
|
|
var timeLinesTask = TimeLines(invoice, timesummary);
|
|
|
- var partLinesTask = PartLines(invoice, partsummary);
|
|
|
+ var partLinesTask = PartLines(invoice, partsummary, strategy);
|
|
|
var expenseLinesTask = ExpenseLines(invoice, expensesSummary);
|
|
|
|
|
|
progress?.Report("Calculating...");
|