Ver Fonte

Added aggregates for invoicing, and linked StockMovements to JobScopes. Added HourlyRate for employees

Kenric Nugteren há 10 meses atrás
pai
commit
c103af75bc

+ 12 - 0
prs.classes/EnclosedEntities/ActualCharge/ActualCharge.cs

@@ -25,5 +25,17 @@ namespace Comal.Classes
         [EditorSequence(4)]
         public double Charge { get; set; }
         
+        public void CopyFrom(ActualCharge source, bool observing = false)
+        {
+            if (!observing)
+                SetObserving(false);
+            Chargeable = source.Chargeable;
+            OverrideQuantity = source.OverrideQuantity;
+            Quantity = source.Quantity;
+            OverrideCharge = source.OverrideCharge;
+            Charge = source.Charge;
+            if (!observing)
+                SetObserving(true);
+        }
     }
 }

+ 0 - 1
prs.classes/Entities/Assignment/Assignment.cs

@@ -5,7 +5,6 @@ using InABox.Core;
 
 namespace Comal.Classes
 {
-
     /// <summary>
     ///     An assignment represents an anticipated booking for an employee, much like a diary entry
     ///     <exclude />

+ 5 - 0
prs.classes/Entities/Employee/Employee.cs

@@ -91,6 +91,11 @@ namespace Comal.Classes
         [CodeEditor(Editable = Editable.Enabled)]
         [EditorSequence("Payroll",6)]
         public string PayrollID { get; set; }
+
+        [CurrencyEditor]
+        [Security(typeof(CanViewHourlyRates))]
+        [EditorSequence("Payroll",7)]
+        public double HourlyRate { get; set; }
         
         [EditorSequence("Org Chart", 1)]
         public EmployeePositionLink Position { get; set; }

+ 4 - 0
prs.classes/Entities/Employee/EmployeeLink.cs

@@ -34,6 +34,10 @@ namespace Comal.Classes
 
         [CodeEditor(Visible = Visible.Optional, Editable = Editable.Hidden)]
         public string PayrollID { get; set; }
+
+        [CurrencyEditor]
+        [Security(typeof(CanViewHourlyRates))]
+        public double HourlyRate { get; set; }
         
         [NullEditor]
         public ImageDocumentLink Thumbnail { get; set; }

+ 19 - 0
prs.classes/Entities/Job/JobScopes/JobScope.cs

@@ -52,6 +52,21 @@ namespace Comal.Classes
         public FormulaType Type => FormulaType.Virtual;
     }
 
+    public class JobScopeUninvoicedMaterialExTax : CoreAggregate<JobScope, StockMovement, double>
+    {
+        public override Expression<Func<StockMovement, double>> Aggregate => x => x.Value;
+
+        public override Dictionary<Expression<Func<StockMovement, object?>>, Expression<Func<JobScope, object?>>> Links => new Dictionary<Expression<Func<StockMovement, object?>>, Expression<Func<JobScope, object?>>>()
+        {
+            { mvt => mvt.JobScope.ID, scope => scope.ID }
+        };
+
+        public override Filter<StockMovement>? Filter => new Filter<StockMovement>(x => x.Invoice.ID).IsEqualTo(Guid.Empty)
+            .And(x => x.Type).IsEqualTo(StockMovementType.Issue);
+
+        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
+    }
+
     public interface IJobScopedItem
     {
         JobLink JobLink { get; set; }
@@ -98,6 +113,10 @@ namespace Comal.Classes
         [Formula(typeof(JobFinancialUninvoicedExTax))]
         public double UninvoiceIncTax { get; set; }
 
+        [CurrencyEditor(Visible = Visible.Optional, Editable = Editable.Hidden, Summary = Summary.Sum)]
+        [Aggregate(typeof(JobScopeUninvoicedMaterialExTax))]
+        public double UninvoicedMaterialsExTax { get; set; }
+
         public Expression<Func<JobScope, string>> AutoIncrementField() => x => x.Number;
         public Filter<JobScope> AutoIncrementFilter() =>  new Filter<JobScope>(x => x.Job.ID).IsEqualTo(Job.ID);
         public String AutoIncrementPrefix() => "";

+ 1 - 4
prs.classes/Entities/Requisition/RequisitionItem.cs

@@ -13,7 +13,7 @@ namespace Comal.Classes
     }
 
     [UserTracking(typeof(Requisition))]
-    public class RequisitionItem : StockEntity, IPersistent, IRemotable, INumericAutoIncrement<RequisitionItem>, IOneToMany<Invoice>, IOneToMany<Requisition>
+    public class RequisitionItem : StockEntity, IPersistent, IRemotable, INumericAutoIncrement<RequisitionItem>, IOneToMany<Requisition>
         , ILicense<LogisticsLicense>, IJobMaterial, IJobScopedItem
     {
         [IntegerEditor(Editable = Editable.Hidden)]
@@ -110,9 +110,6 @@ namespace Comal.Classes
         
         [NullEditor]
         public string BarCode { get; set; }
-        
-        [NullEditor]
-        public InvoiceLink Invoice { get; set; }
 
         [NullEditor]
         public RequisitionItemEditType EditType { get; set; }

+ 29 - 1
prs.classes/Entities/Stock/StockMovement/StockMovement.cs

@@ -181,6 +181,29 @@ namespace Comal.Classes
         [RequiredColumn]
         public JobRequisitionItemLink JobRequisitionItem { get; set; }
 
+        [NullEditor]
+        public InvoiceLink Invoice { get; set; }
+
+        private class JobScopeLookup : LookupDefinitionGenerator<JobScope, StockMovement>
+        {
+            public override Filter<JobScope> DefineFilter(StockMovement[] items)
+            {
+                var item = items?.Length == 1 ? items[0] : null;
+                if (item != null)
+                    return new Filter<JobScope>(x => x.Job.ID).IsEqualTo(item.Job.ID).And(x => x.Status.Approved).IsEqualTo(true);
+                return new Filter<JobScope>(x => x.ID).None();
+            }
+            
+            public override Columns<StockMovement> DefineFilterColumns()
+                => Columns.None<StockMovement>().Add(x=>x.Job.ID);
+        }
+        [LookupDefinition(typeof(JobScopeLookup))]
+        [EditorSequence(5)]
+        [EntityRelationship(DeleteAction.SetNull)]
+        public JobScopeLink JobScope { get; set; }
+
+        public ActualCharge Charge { get; set; }
+
         [Aggregate(typeof(StockMovementDocumentCount))]
         [NullEditor]
         public int Documents { get; set; }
@@ -196,7 +219,9 @@ namespace Comal.Classes
         [Obsolete("Replaced with Dimensions", true)]
         public double UnitSize { get; set; }
         
-        
+        /// <summary>
+        /// Value of a stock movement, equal to <see cref="Units"/> * <see cref="Cost"/>.
+        /// </summary>
         [CurrencyEditor(Visible = Visible.Optional, Editable = Editable.Hidden, Summary=Summary.Sum)]
         [Formula(typeof(StockMovementValueFormula))]
         public double Value { get; set; } = 0.0;
@@ -221,6 +246,9 @@ namespace Comal.Classes
             StockEntity.LinkStockDimensions<StockMovement>();
             LinkedProperties.Register<StockMovement, ProductStyleLink, Guid>(x=>x.Product.DefaultInstance.Style, x => x.ID, x => x.Style.ID);
             //LinkedProperties.Register<StockMovement, ProductLink, double>(x => x.Product, x => x.AverageCost, x => x.Cost);
+            LinkedProperties.Register<StockMovement, JobScopeLink, Guid>(ass => ass.Job.DefaultScope, scope => scope.ID, ass => ass.JobScope.ID);
+            LinkedProperties.Register<StockMovement, JobScopeLink, String>(ass => ass.Job.DefaultScope, scope => scope.Number, ass => ass.JobScope.Number);
+            LinkedProperties.Register<StockMovement, JobScopeLink, String>(ass => ass.Job.DefaultScope, scope => scope.Description, ass => ass.JobScope.Description);
         }
 
         private static Column<StockMovement> unitsize = new Column<StockMovement>(x => x.Dimensions.Value);

+ 5 - 0
prs.classes/SecurityDescriptors/HumanResources_Descriptors.cs

@@ -32,6 +32,11 @@ namespace Comal.Classes
     {
     }
 
+    [Caption("View Hourly Rates")]
+    public class CanViewHourlyRates : DisabledSecurityDescriptor<HumanResourcesLicense, Employee>
+    {
+    }
+
     [Caption("Allow Timesheet Midnight Rollover")]
     public class AllowTimeSheetRollover : DisabledSecurityDescriptor<HumanResourcesLicense, TimeSheet>
     {

+ 0 - 1
prs.desktop/Panels/Invoices/InvoiceAssignmentGrid.cs

@@ -31,7 +31,6 @@ namespace PRSDesktop
             ColumnsTag = "InvoiceTimeSheets";
 
             HiddenColumns.Add(x => x.Invoice.ID);
-            HiddenColumns.Add(x => x.Invoice.Deleted);
             HiddenColumns.Add(x => x.Charge.Chargeable);
 
             ActionColumns.Add(new DynamicImageColumn(IncludeImage, IncludeOne));

+ 1 - 1
prs.desktop/Panels/Invoices/InvoicePanel.xaml

@@ -54,7 +54,7 @@
                     <local:InvoiceAssignmentGrid x:Name="Time"/>
                 </dynamicGrid:DynamicTabItem>
                 <dynamicGrid:DynamicTabItem Header="Materials">
-                    <local:InvoiceRequisitionItemGrid x:Name="Parts"/>
+                    <local:InvoiceStockMovementGrid x:Name="Parts"/>
                 </dynamicGrid:DynamicTabItem>
             </dynamicGrid:DynamicTabControl>
         </dynamicGrid:DynamicSplitPanel.SecondaryDetail>

+ 0 - 166
prs.desktop/Panels/Invoices/InvoiceRequisitionGrid.cs

@@ -1,166 +0,0 @@
-using System;
-using System.Linq;
-using System.Threading;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Media.Imaging;
-using Comal.Classes;
-using InABox.Clients;
-using InABox.Core;
-using InABox.DynamicGrid;
-using InABox.WPF;
-
-namespace PRSDesktop
-{
-    public class InvoiceRequisitionItemGrid : DynamicDataGrid<RequisitionItem>
-    {
-        private readonly BitmapImage chargeable_excluded = PRSDesktop.Resources.tick.Fade(0.8F).AsGrayScale().AsBitmapImage();
-        private readonly BitmapImage chargeable_included = PRSDesktop.Resources.tick.AsBitmapImage();
-        
-        private readonly BitmapImage unchargeable_excluded = PRSDesktop.Resources.warning.Fade(0.8F).AsGrayScale().AsBitmapImage();
-        private readonly BitmapImage unchargeable_included = PRSDesktop.Resources.warning.AsBitmapImage();
-        
-        private readonly Button IncludeButton;
-
-        private readonly Button ShowAllButton;
-        private bool _showall = false;
-
-        public InvoiceRequisitionItemGrid()
-        {
-            
-            ColumnsTag = "InvoiceParts";
-
-            HiddenColumns.Add(x => x.Invoice.ID);
-            HiddenColumns.Add(x => x.Invoice.Deleted);
-            HiddenColumns.Add(x => x.Charge.Chargeable);
-
-            ActionColumns.Add(new DynamicImageColumn(IncludeImage, IncludeOne));
-            AddButton("Exclude", chargeable_excluded, IncludeSelected, DynamicGridButtonPosition.Right);
-            IncludeButton = AddButton("Include", chargeable_included, IncludeSelected, DynamicGridButtonPosition.Right);
-            ShowAllButton = AddButton("Show All", null, ToggleShowAll);
-        }
-
-        protected override void DoReconfigure(DynamicGridOptions options)
-        {
-            base.DoReconfigure(options);
-
-            options.Clear();
-            options.EditRows = true;
-            options.RecordCount = true;
-            options.SelectColumns = true;
-            options.MultiSelect = true;
-            options.FilterRows = true;
-        }
-        
-        public Invoice Invoice { get; set; }
-
-        private bool IncludeSelected(Button sender, CoreRow[] rows)
-        {
-            if (Invoice.ID != Guid.Empty)
-            {
-                using (new WaitCursor())
-                {
-                    var bAdd = sender == IncludeButton;
-                    var items = rows.Select(r => r.ToObject<RequisitionItem>()).ToArray();
-                    foreach (var item in items)
-                    {
-                        if (bAdd)
-                        {
-                            item.Invoice.ID = Invoice.ID;
-                            item.Invoice.Synchronise(Invoice);
-                        }
-                        else
-                        {
-                            item.Invoice.ID = Guid.Empty;
-                            item.Invoice.Synchronise(new Invoice());
-                        }
-                    }
-
-                    new Client<RequisitionItem>().Save(items, bAdd ? "Added to Invoice" : "Removed From Invoice", (o, e) => { });
-                    foreach (var row in rows)
-                    {
-                        row.Set<RequisitionItem, Guid>(x => x.Invoice.ID, bAdd ? Invoice.ID : Guid.Empty);
-                        InvalidateRow(row);
-                    }
-                }
-            }
-            else
-                MessageBox.Show("Please Select or Create an Invoice First!");
-            return false;
-        }
-
-        private bool IncludeOne(CoreRow arg)
-        {
-            if (arg == null)
-                return false;
-            
-            if (Invoice.ID != Guid.Empty)
-            {
-                var id = arg.Get<RequisitionItem, Guid>(x => x.ID);
-                var item = arg.ToObject<RequisitionItem>();
-                if (item != null)
-                {
-                    if (!item.Invoice.IsValid())
-                    {
-                        item.Invoice.ID = Invoice.ID;
-                        item.Invoice.Synchronise(Invoice);
-                    }
-                    else
-                    {
-                        item.Invoice.ID = Guid.Empty;
-                        item.Invoice.Synchronise(new Invoice());
-                    }
-                    new Client<RequisitionItem>().Save(item, "Added to Invoice", (o,e) => { });
-                    arg.Set<RequisitionItem, Guid>(x=>x.Invoice.ID, item.Invoice.ID);
-                    InvalidateRow(arg);
-                }
-            }
-            else
-                MessageBox.Show("Please Select or Create an Invoice First!");
-            return false;
-        }
-
-        private BitmapImage IncludeImage(CoreRow arg)
-        {
-            if (arg == null)
-                return chargeable_included;
-            bool chargeable = arg.Get<RequisitionItem, bool>(x => x.Charge.Chargeable);
-            bool included = Entity.IsEntityLinkValid<RequisitionItem, InvoiceLink>(x => x.Invoice, arg);
-            return chargeable
-                ? included 
-                    ? chargeable_included 
-                    : chargeable_excluded
-                : included
-                    ? unchargeable_included
-                    : unchargeable_excluded;
-        }
-        
-        private bool ToggleShowAll(Button arg1, CoreRow[] rows)
-        {
-            _showall = !_showall;
-            UpdateButton(ShowAllButton, null, _showall ? "Selected Only" : "Show All");
-            return true;
-        }
-
-        protected override void Reload(
-        	Filters<RequisitionItem> criteria, Columns<RequisitionItem> columns, ref SortOrder<RequisitionItem>? sort,
-        	CancellationToken token, Action<CoreTable?, Exception?> action)
-        {
-            if (Invoice.ID  == Guid.Empty)
-                criteria.Add(new Filter<RequisitionItem>().None());
-            else
-            {
-                
-                criteria.Add(new Filter<RequisitionItem>(x => x.RequisitionLink.JobLink.ID).IsEqualTo(Invoice.JobLink.ID));
-                
-                if (_showall)
-                    criteria.Add(new Filter<RequisitionItem>(x => x.Invoice.ID).IsEqualTo(Invoice.ID).Or(x => x.Invoice).NotLinkValid());
-                else
-                    criteria.Add(new Filter<RequisitionItem>(x => x.Invoice.ID).IsEqualTo(Invoice.ID));
-                
-            }
-            criteria.Add(new Filter<RequisitionItem>(x => x.Done).IsEqualTo(true));
-            base.Reload(criteria, columns, ref sort, token, action);
-        }
-    }
-}

+ 159 - 0
prs.desktop/Panels/Invoices/InvoiceStockMovementGrid.cs

@@ -0,0 +1,159 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media.Imaging;
+using Comal.Classes;
+using InABox.Clients;
+using InABox.Core;
+using InABox.DynamicGrid;
+using InABox.WPF;
+
+namespace PRSDesktop;
+
+public class InvoiceStockMovementGrid : DynamicDataGrid<StockMovement>
+{
+    private readonly BitmapImage chargeable_excluded = PRSDesktop.Resources.tick.Fade(0.8F).AsGrayScale().AsBitmapImage();
+    private readonly BitmapImage chargeable_included = PRSDesktop.Resources.tick.AsBitmapImage();
+    
+    private readonly BitmapImage unchargeable_excluded = PRSDesktop.Resources.warning.Fade(0.8F).AsGrayScale().AsBitmapImage();
+    private readonly BitmapImage unchargeable_included = PRSDesktop.Resources.warning.AsBitmapImage();
+    
+    private readonly Button IncludeButton;
+
+    private readonly Button ShowAllButton;
+    private bool _showall = false;
+
+    public InvoiceStockMovementGrid()
+    {
+        
+        ColumnsTag = "InvoiceParts";
+
+        HiddenColumns.Add(x => x.Invoice.ID);
+
+        ActionColumns.Add(new DynamicImageColumn(IncludeImage, IncludeOne));
+        AddButton("Exclude", chargeable_excluded, IncludeSelected, DynamicGridButtonPosition.Right);
+        IncludeButton = AddButton("Include", chargeable_included, IncludeSelected, DynamicGridButtonPosition.Right);
+        ShowAllButton = AddButton("Show All", null, ToggleShowAll);
+    }
+
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        base.DoReconfigure(options);
+
+        options.Clear();
+        options.EditRows = true;
+        options.RecordCount = true;
+        options.SelectColumns = true;
+        options.MultiSelect = true;
+        options.FilterRows = true;
+    }
+    
+    public Invoice Invoice { get; set; }
+
+    private bool IncludeSelected(Button sender, CoreRow[] rows)
+    {
+        if (Invoice.ID != Guid.Empty)
+        {
+            using (new WaitCursor())
+            {
+                var bAdd = sender == IncludeButton;
+                var items = rows.ToArray<StockMovement>();
+                foreach (var item in items)
+                {
+                    if (bAdd)
+                    {
+                        item.Invoice.ID = Invoice.ID;
+                        item.Invoice.Synchronise(Invoice);
+                    }
+                    else
+                    {
+                        item.Invoice.ID = Guid.Empty;
+                        item.Invoice.Synchronise(new Invoice());
+                    }
+                }
+
+                Client.Save(items, bAdd ? "Added to Invoice" : "Removed From Invoice", (o, e) => { });
+                foreach (var row in rows)
+                {
+                    row.Set<StockMovement, Guid>(x => x.Invoice.ID, bAdd ? Invoice.ID : Guid.Empty);
+                    InvalidateRow(row);
+                }
+            }
+        }
+        else
+            MessageBox.Show("Please Select or Create an Invoice First!");
+        return false;
+    }
+
+    private bool IncludeOne(CoreRow? arg)
+    {
+        if (arg == null)
+            return false;
+        
+        if (Invoice.ID != Guid.Empty)
+        {
+            var id = arg.Get<StockMovement, Guid>(x => x.ID);
+            var item = arg.ToObject<StockMovement>();
+            if (item != null)
+            {
+                if (!item.Invoice.IsValid())
+                {
+                    item.Invoice.ID = Invoice.ID;
+                    item.Invoice.Synchronise(Invoice);
+                }
+                else
+                {
+                    item.Invoice.ID = Guid.Empty;
+                    item.Invoice.Synchronise(new Invoice());
+                }
+                Client.Save(item, "Added to Invoice", (o,e) => { });
+                arg.Set<StockMovement, Guid>(x => x.Invoice.ID, item.Invoice.ID);
+                InvalidateRow(arg);
+            }
+        }
+        else
+            MessageBox.Show("Please Select or Create an Invoice First!");
+        return false;
+    }
+
+    private BitmapImage IncludeImage(CoreRow? arg)
+    {
+        if (arg == null)
+            return chargeable_included;
+        bool chargeable = arg.Get<StockMovement, bool>(x => x.Charge.Chargeable);
+        bool included = Entity.IsEntityLinkValid<StockMovement, InvoiceLink>(x => x.Invoice, arg);
+        return chargeable
+            ? included 
+                ? chargeable_included 
+                : chargeable_excluded
+            : included
+                ? unchargeable_included
+                : unchargeable_excluded;
+    }
+    
+    private bool ToggleShowAll(Button arg1, CoreRow[] rows)
+    {
+        _showall = !_showall;
+        UpdateButton(ShowAllButton, null, _showall ? "Selected Only" : "Show All");
+        return true;
+    }
+
+    protected override void Reload(
+    	Filters<StockMovement> criteria, Columns<StockMovement> columns, ref SortOrder<StockMovement>? sort,
+    	CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        if (Invoice.ID  == Guid.Empty)
+            criteria.Add(new Filter<StockMovement>().None());
+        else
+        {
+            if (_showall)
+                criteria.Add(new Filter<StockMovement>(x => x.Invoice.ID).IsEqualTo(Invoice.ID).Or(x => x.Invoice).NotLinkValid());
+            else
+                criteria.Add(new Filter<StockMovement>(x => x.Invoice.ID).IsEqualTo(Invoice.ID));
+            
+        }
+        base.Reload(criteria, columns, ref sort, token, action);
+    }
+}

+ 25 - 3
prs.desktop/Panels/Invoices/ProgressClaimGrid.cs

@@ -20,6 +20,12 @@ public class ProgressClaim : BaseObject
 {
     public JobScopeLink JobScope { get; set; }
 
+    [CurrencyEditor]
+    public double Materials { get; set; }
+
+    [CurrencyEditor]
+    public double Labour { get; set; }
+
     public double PreviouslyClaimed { get; set; }
 
     public double PreviouslyClaimedPercent { get; set; }
@@ -27,7 +33,7 @@ public class ProgressClaim : BaseObject
     [DoubleEditor]
     public double PercentCost { get; set; }
 
-    [CurrencyEditor(Editable = Editable.Disabled)]
+    [CurrencyEditor]
     public double Cost { get; set; }
 }
 
@@ -59,7 +65,8 @@ public class ProgressClaimGrid : DynamicItemsListGrid<ProgressClaim>
                 .Add(x => x.Number)
                 .Add(x => x.ExTax).Add(x => x.InvoiceExTax)
                 .Add(x => x.TaxCode.ID).Add(x => x.TaxCode.Rate)
-                .Add(x => x.Description);
+                .Add(x => x.Description)
+                .Add(x => x.UninvoicedMaterialsExTax);
 
         var scopeColumn = new Column<ProgressClaim>(x => x.JobScope).Property + ".";
         foreach(var column in DataColumns().Where(x => x.Property.StartsWith(scopeColumn)))
@@ -69,9 +76,20 @@ public class ProgressClaimGrid : DynamicItemsListGrid<ProgressClaim>
 
         var scopes = Client.Query<JobScope>(
                 new Filter<JobScope>(x => x.Job.ID).IsEqualTo(JobID)
-                    .And(x => x.Status.Approved).IsEqualTo(true), columns)
+                    .And(x => x.Status.Approved).IsEqualTo(true),
+                columns)
             .ToArray<JobScope>();
 
+        var assignments = Client.Query(
+            new Filter<Assignment>(x => x.JobScope.ID).InList(scopes.ToArray(x => x.ID))
+                .And(x => x.Invoice.ID).IsEqualTo(Guid.Empty),
+            Columns.None<Assignment>().Add(x => x.JobScope.ID)
+                .Add(x => x.Actual.Duration)
+                .Add(x => x.EmployeeLink.HourlyRate))
+            .ToObjects<Assignment>()
+            .GroupBy(x => x.JobScope.ID)
+            .ToDictionary(x => x.Key, x => x.Sum(x => x.Actual.Duration.TotalHours * x.EmployeeLink.HourlyRate));
+
         var items = new List<ProgressClaim>();
         foreach(var scope in scopes)
         {
@@ -81,6 +99,8 @@ public class ProgressClaimGrid : DynamicItemsListGrid<ProgressClaim>
             newItem.PreviouslyClaimedPercent = scope.ExTax.IsEffectivelyEqual(0.0) ? 0.0 : scope.InvoiceExTax / scope.ExTax * 100;
             newItem.PercentCost = newItem.PreviouslyClaimedPercent;
             newItem.Cost = 0;
+            newItem.Materials = scope.UninvoicedMaterialsExTax;
+            newItem.Labour = assignments.GetValueOrDefault(scope.ID);
             items.Add(newItem);
         }
         Items = items;
@@ -110,6 +130,8 @@ public class ProgressClaimGrid : DynamicItemsListGrid<ProgressClaim>
 
         columns.Add<ProgressClaim, string>(x => x.JobScope.Number, 80, "Number", "", Alignment.MiddleCenter);
         columns.Add<ProgressClaim, string>(x => x.JobScope.Description, 0, "Description", "", Alignment.MiddleCenter);
+        columns.Add<ProgressClaim, double>(x => x.Materials, 80, "Material $", "", Alignment.MiddleCenter);
+        columns.Add<ProgressClaim, double>(x => x.Labour, 80, "Labour $", "", Alignment.MiddleCenter);
         columns.Add<ProgressClaim, double>(x => x.JobScope.ExTax, 100, "Quote $", "", Alignment.MiddleCenter);
         columns.Add<ProgressClaim, double>(x => x.PreviouslyClaimed, 100, "Prev $", "C2", Alignment.MiddleCenter);
         columns.Add<ProgressClaim, double>(x => x.PreviouslyClaimedPercent, 80, "Prev %", "", Alignment.MiddleCenter);

+ 0 - 3
prs.desktop/Panels/Requisitions/RequisitionItemEditor/RequisitionItemEditor.xaml.cs

@@ -306,7 +306,6 @@ public partial class RequisitionItemEditor : INotifyPropertyChanged
         var newItem = new RequisitionItem();
         newItem.Done = Item.Done;
         newItem.Image.CopyFrom(Item.Image);
-        newItem.Invoice.CopyFrom(Item.Invoice);
         newItem.RequisitionLink.ID = Item.RequisitionLink.ID;
         newItem.SourceJRI.ID = Item.SourceJRI.ID;
         newItem.Product.CopyFrom(Item.Product);
@@ -469,7 +468,6 @@ public partial class RequisitionItemEditor : INotifyPropertyChanged
                 newItem.Done = Item.Done;
                 newItem.Description = Item.Description;
                 newItem.Image.CopyFrom(Item.Image);
-                newItem.Invoice.CopyFrom(Item.Invoice);
                 newItem.RequisitionLink.ID = Item.RequisitionLink.ID;
                 newItem.JobRequisitionItem.ID = item.ID;
                 newItem.Product.CopyFrom(Item.Product);
@@ -494,7 +492,6 @@ public partial class RequisitionItemEditor : INotifyPropertyChanged
             newItem.Done = false;
             newItem.Description = Item.Description;
             newItem.Image.CopyFrom(Item.Image);
-            newItem.Invoice.CopyFrom(Item.Invoice);
             newItem.RequisitionLink.ID = Item.RequisitionLink.ID;
 
             newItem.JobRequisitionItem.ID = Guid.Empty;

+ 8 - 7
prs.shared/Utilities/InvoiceUtilities.cs

@@ -44,7 +44,7 @@ namespace PRS.Shared
             CustomerProductSummary[] products = new CustomerProductSummary[] { };
 
             Assignment[] assignments = new Assignment[] { };
-            RequisitionItem[] requisitionitems = new RequisitionItem[] { };
+            var movements = Array.Empty<StockMovement>();
 
             progress?.Report("Loading Invoice");
             var invoice = new Client<Invoice>().Load(new Filter<Invoice>(x => x.ID).IsEqualTo(invoiceid)).FirstOrDefault();
@@ -111,10 +111,10 @@ namespace PRS.Shared
 
                 Task.Run(() =>
                 {
-                    requisitionitems = new Client<RequisitionItem>().Query(
-                        new Filter<RequisitionItem>(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true),
+                    movements = new Client<StockMovement>().Query(
+                        new Filter<StockMovement>(x => x.Invoice.ID).IsEqualTo(invoice.ID).And(x => x.Charge.Chargeable).IsEqualTo(true),
                         null
-                    ).Rows.Select(x => x.ToObject<RequisitionItem>()).ToArray();
+                    ).ToArray<StockMovement>();
                 })
             };
             
@@ -202,7 +202,7 @@ namespace PRS.Shared
             
             var partlines = new List<InvoiceLineDetail>();
             
-            foreach (var item in requisitionitems)
+            foreach (var item in movements)
             {
 
                 var id = partsummary switch
@@ -215,7 +215,7 @@ namespace PRS.Shared
 
                 var description = partsummary switch
                 {
-                    InvoiceMaterialCalculation.Detailed => item.Description,
+                    InvoiceMaterialCalculation.Detailed => $"{item.Product.Name}: {item.Style.Code}; {item.Dimensions.UnitSize}",
                     InvoiceMaterialCalculation.Product => item.Product.Name,
                     InvoiceMaterialCalculation.CostCentre => item.Product.CostCentre.Description,
                     _ => "Materials"
@@ -223,7 +223,7 @@ namespace PRS.Shared
                 
                 var quantity = item.Charge.OverrideQuantity
                     ? item.Charge.Quantity
-                    : item.Quantity;
+                    : item.Qty;
                 
                 var product = 
                     products.FirstOrDefault(x => x.Customer.ID.Equals(invoice.CustomerLink.ID) && x.Product.ID.Equals(item.Product.ID))
@@ -247,6 +247,7 @@ namespace PRS.Shared
                 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);

+ 12 - 6
prs.stores/RequisitionStore.cs

@@ -4,6 +4,7 @@ using System.Linq.Expressions;
 using Comal.Classes;
 using InABox.Core;
 using System;
+using NPOI.Util;
 
 namespace Comal.Stores
 {
@@ -58,6 +59,8 @@ namespace Comal.Stores
                         .Add(x =>x.JobLink.ID)
                         .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
                         .Add(x => x.ActualQuantity)
+                        .AddSubColumns(x => x.Charge, Columns.All<ActualCharge>())
+                        .Add(x => x.JobScope.ID)
                         .AddDimensionsColumns(x => x.Product.DefaultInstance.Dimensions, Dimensions.ColumnsType.Local)
                 ).ToList<RequisitionItem>();
             return requisitionitems.Any();
@@ -236,7 +239,7 @@ namespace Comal.Stores
         #region StockMovements
 
         private StockMovement CreateStockMovement(IEmployee employee, DateTime date, IStockMovementBatch batch, IProduct product, IStockLocation location,
-            IProductStyle style, IJob? job, IJobRequisitionItem? jri, IDimensions dimensions, Guid txnid, bool system, string note)
+            IProductStyle style, IJob? job, IJobRequisitionItem? jri, IDimensions dimensions, Guid txnid, ActualCharge charge, JobScopeLink scope, bool system, string note)
         {
             var movement = new StockMovement();
             movement.Batch.ID = batch.ID;
@@ -247,6 +250,9 @@ namespace Comal.Stores
             movement.JobRequisitionItem.ID = jri?.ID ?? Guid.Empty;
             movement.Dimensions.CopyFrom(dimensions);
 
+            movement.Charge.CopyFrom(charge);
+            movement.JobScope.CopyFrom(scope);
+
             movement.System = system;
             movement.Transaction = txnid;
             movement.Notes = note;
@@ -311,13 +317,13 @@ namespace Comal.Stores
 
                     // Transfer the necessary balance from General Stock...
                     var from = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, null, null,
-                        dimensions, txnid, true, $"Requisition #{entity.Number} Internal Transfer");
+                        dimensions, txnid, item.Charge, item.JobScope, true, $"Requisition #{entity.Number} Internal Transfer");
                     from.Issued = extraRequired;
                     from.Type = StockMovementType.TransferOut;
                     timestamp = timestamp.AddTicks(1);
 
                     // ... to the job -  note this is the final (entity) job/JRI, not the holding job/JRI
-                    var to = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.SourceJRI, dimensions, txnid, true,
+                    var to = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.SourceJRI, dimensions, txnid, item.Charge, item.JobScope, true,
                         $"Requisition #{entity.Number} Internal Transfer");
                     to.Received = extraRequired;
                     to.Type = StockMovementType.TransferIn;
@@ -334,13 +340,13 @@ namespace Comal.Stores
                 {
                     // Transfer from the item job to the requisition job
                     var from = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, item.JobLink, item.JobRequisitionItem,
-                        dimensions, txnid, true, $"Requisition #{entity.Number} Internal Transfer");
+                        dimensions, txnid, item.Charge, item.JobScope, true, $"Requisition #{entity.Number} Internal Transfer");
                     from.Issued = qty;
                     from.Type = StockMovementType.TransferOut;
                     timestamp = timestamp.AddTicks(1);
 
                     // ... to the job.
-                    var to = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.SourceJRI, dimensions, txnid, true,
+                    var to = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.SourceJRI, dimensions, txnid, item.Charge, item.JobScope, true,
                         $"Requisition #{entity.Number} Internal Transfer");
                     to.Received = qty;
                     to.Type = StockMovementType.TransferIn;
@@ -351,7 +357,7 @@ namespace Comal.Stores
                     
                 }
 
-                var mvt = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.SourceJRI, dimensions, txnid,
+                var mvt = CreateStockMovement(entity.Employee, timestamp, batch, item.Product, item.Location, item.Style, entity.JobLink, item.SourceJRI, dimensions, txnid, item.Charge, item.JobScope,
                     false,
                     $"Requisition #{entity.Number}");
                 

+ 20 - 0
prs.stores/StockMovementStore.cs

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System;
+using InABox.Database;
 
 namespace PRSStores;
 
@@ -18,6 +19,25 @@ public class StockMovementStore : BaseStore<StockMovement>
         // we need to reduce the old stock holding before updating the new one
         if (sm.ID != Guid.Empty)
             StockHoldingStore.UpdateStockHolding(this, sm.ID,StockHoldingStore.Action.Decrease);
+
+        if(sm.Job.HasOriginalValue(x => x.ID) && sm.Job.ID != Guid.Empty && sm.JobScope.ID == Guid.Empty)
+        {
+            var scopeID = Guid.Empty;
+            if(sm.ID != Guid.Empty)
+            {
+                scopeID = Provider.Query(
+                    new Filter<StockMovement>(x => x.ID).IsEqualTo(sm.ID),
+                    Columns.None<StockMovement>().Add(x => x.JobScope.ID))
+                    .Rows.FirstOrDefault()?.Get<StockMovement, Guid>(x => x.JobScope.ID) ?? Guid.Empty;
+            }
+            if(scopeID == Guid.Empty)
+            {
+                sm.JobScope.ID = Provider.Query(
+                    new Filter<Job>(x => x.ID).IsEqualTo(sm.Job.ID),
+                    Columns.None<Job>().Add(x => x.DefaultScope.ID))
+                    .Rows.FirstOrDefault()?.Get<Job, Guid>(x => x.DefaultScope.ID) ?? Guid.Empty;
+            }
+        }
     }
 
     protected override void AfterSave(StockMovement sm)