Просмотр исходного кода

Obsoleted PurchaseOrder.Status and made PurchaseOrders mergeable
Fixed JRI Calculation when Receiving Purchase Order Items
Added Indicator columns to PurchaseOrder Grid
Tightened Up "Receive Items" on PurchaseOrderItem Grid
Re-enabled Purchase Link on Purchase Order tems
Added ReceievdQty Aggregate on Purchase Order
Added Code to Employee Lookup Columns
Improved Purchase Order Editor Layout

frogsoftware 11 месяцев назад
Родитель
Сommit
2aa5682903

+ 6 - 3
prs.classes/Entities/Employee/EmployeeLink.cs

@@ -5,14 +5,17 @@ namespace Comal.Classes
 {
     public class EmployeeLink : EntityLink<Employee>, IEmployee
     {
-        [LookupEditor(typeof(Employee))]
-        [RequiredColumn]
-        [LoggableProperty]
+        [EditorSequence(1)]
+        [CodePopupEditor(typeof(Employee))]
+        [RequiredColumn,LoggableProperty]
         public override Guid ID { get; set; }
 
+        [EditorSequence(2)]
+        [RequiredColumn]
         [CodeEditor(Visible = Visible.Default, Editable = Editable.Hidden)]
         public string Code { get; set; }
 
+        [RequiredColumn]
         [TextBoxEditor(Visible = Visible.Optional, Editable = Editable.Hidden)]
         public string Name { get; set; }
 

+ 1 - 0
prs.classes/Entities/Employee/EmployeeLookups.cs

@@ -10,6 +10,7 @@ namespace Comal.Classes
         {
             return Columns.None<Employee>().Add(
                 x => x.ID,
+                x => x.Code,
                 x => x.Name
             );
         }

+ 81 - 79
prs.classes/Entities/PurchaseOrder/PurchaseOrder.cs

@@ -2,6 +2,8 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq.Expressions;
+using System.Runtime.InteropServices;
+using InABox.Clients;
 using InABox.Core;
 
 namespace Comal.Classes
@@ -34,7 +36,22 @@ namespace Comal.Classes
             };
 
         public override Filter<PurchaseOrderItem>? Filter => new Filter<PurchaseOrderItem>(x => x.ReceivedDate).IsEqualTo(DateTime.MinValue);
-    }    
+    }
+    
+    public class PurchaseOrderReceivedQtyAggregate : CoreAggregate<PurchaseOrder, PurchaseOrderItem, double>
+    {
+        public override Expression<Func<PurchaseOrderItem, double>> Aggregate => x => x.Qty;
+
+        public override AggregateCalculation Calculation => AggregateCalculation.Sum;
+
+        public override Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>> Links =>
+            new Dictionary<Expression<Func<PurchaseOrderItem, object?>>, Expression<Func<PurchaseOrder, object?>>>()
+            {
+                { PurchaseOrderItem => PurchaseOrderItem.PurchaseOrderLink.ID, PurchaseOrder => PurchaseOrder.ID }
+            };
+        
+        public override Filter<PurchaseOrderItem>? Filter => new Filter<PurchaseOrderItem>(x => x.ReceivedDate).IsNotEqualTo(DateTime.MinValue);
+    }  
     
     public class PurchaseOrderExTax : CoreAggregate<PurchaseOrder, PurchaseOrderItem, double>
     {
@@ -90,7 +107,7 @@ namespace Comal.Classes
 
     [UserTracking(typeof(Bill))]
     public class PurchaseOrder : Entity, IRemotable, IPersistent, IStringAutoIncrement<PurchaseOrder>, IOneToMany<Supplier>,
-        ILicense<AccountsPayableLicense>, IDataEntryInstance, IPostable
+        ILicense<AccountsPayableLicense>, IDataEntryInstance, IPostable, IMergeable
     {
         private bool bChanging;
 
@@ -106,68 +123,83 @@ namespace Comal.Classes
         [MemoEditor]
         [EditorSequence(2)]
         public string Description { get; set; }
-
-        [NotesEditor]
-        [EditorSequence("Notes",1)]
-        public string[] Notes { get; set; }
         
         [EditorSequence(3)]
         public PurchaseOrderCategoryLink Category { get; set; }
-        
-        [DateTimeEditor]
+
         [EditorSequence(4)]
-        public DateTime IssuedDate { get; set; }
+        public EmployeeLink RaisedBy { get; set; }
 
         [DateEditor]
         [EditorSequence(5)]
         public DateTime DueDate { get; set; }
         
-        [EditorSequence("Additional",1)]
-        public EmployeeLink RaisedBy { get; set; }
+        private class IssuedDateInformation : EditorInformation<PurchaseOrder>
+        {
+            public override string GetInfo(PurchaseOrder item) => item.IssuedBy.Name;
+        }
         
-        [EditorSequence("Additional",2)]
-        [TimestampEditor]
-        public DateTime DataEntered { get; set; }
+        [DateTimeEditor(Information = typeof(IssuedDateInformation))]
+        [RequiredColumn,LoggableProperty]
+        [EditorSequence(6)]
+        public DateTime IssuedDate { get; set; }
         
-        [EditorSequence("Additional",3)]
+        // This information might be printed out on the PO report, so we store this in the database
+        // Other properties uch as Closed/Cancelled/Checked etc are simply logged in the Audit Trail
+        [RequiredColumn]
+        [NullEditor]
         public EmployeeLink IssuedBy { get; set; }
         
-        [TimestampEditor]
-        [EditorSequence("Additional",4)]
-        public DateTime ClosedDate { get; set; }
-        
-        [TimestampEditor]
-        [EditorSequence("Additional",5)]
-        public DateTime CancelledDate { get; set; }
-
-        [EditorSequence(10)]
         [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
         [Aggregate(typeof(PurchaseOrderExTax))]
         public double ExTax { get; set; }
-
-        [EditorSequence(11)]
+        
         [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
         [Aggregate(typeof(PurchaseOrderTax))]
         public double Tax { get; set; }
-
-        [EditorSequence(12)]
+        
         [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
         [Aggregate(typeof(PurchaseOrderIncTax))]
         public double IncTax { get; set; }
-
-        [EditorSequence(13)]
+        
         [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
         [Aggregate(typeof(PurchaseOrderBalance))]
         public double Balance { get; set; }
-
-        [EditorSequence(13)]
+        
+        [EditorSequence("Additional",2)]
+        [LoggableProperty]
+        [TimestampEditor]
+        public DateTime DataEntered { get; set; }
+        
+        [EditorSequence("Additional",3)]
+        [TimestampEditor]
+        [LoggableProperty]
+        public DateTime CheckedDate { get; set; }
+        
+        [RequiredColumn, LoggableProperty]
+        [TimestampEditor]
+        [EditorSequence("Additional",4)]
+        public DateTime ClosedDate { get; set; }
+        
+        [TimestampEditor]
+        [LoggableProperty]
+        [EditorSequence("Additional",5)]
+        public DateTime CancelledDate { get; set; }
+        
+        [NotesEditor]
+        [EditorSequence("Notes",1)]
+        public string[] Notes { get; set; }
+        
+        
         [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
         [Aggregate(typeof(PurchaseOrderUnreceivedQtyAggregate))]
         public double Unreceived { get; set; }
+        
+        [DoubleEditor(Editable = Editable.Hidden, Summary = Summary.Sum)]
+        [Aggregate(typeof(PurchaseOrderReceivedQtyAggregate))]
+        public double Received { get; set; }
+
 
-        [EditorSequence(14)]
-        [EnumLookupEditor(typeof(PurchaseOrderStatus), Editable = Editable.Hidden)]
-        public PurchaseOrderStatus Status { get; set; }
         
         //[CodeEditor(Visible = Visible.Default, Editable = Editable.Hidden)]
         [NullEditor]
@@ -195,7 +227,11 @@ namespace Comal.Classes
 
         [NullEditor]
         public string PostedReference { get; set; }
-
+        
+        [Obsolete("Replaced by multiple Datetime stamps", true)]
+        [NullEditor]
+        public PurchaseOrderStatus Status { get; set; }
+        
         public Expression<Func<PurchaseOrder, string>> AutoIncrementField() => x => x.PONumber;
         public Filter<PurchaseOrder> AutoIncrementFilter() => null;
         public string AutoIncrementPrefix() => PONumberPrefix;
@@ -207,51 +243,17 @@ namespace Comal.Classes
             if (bChanging)
                 return;
             bChanging = true;
-
-            if (name.Equals("Status"))
+            if (name.Equals(nameof(IssuedDate)) && before is DateTime _old && after is DateTime _new && _new.IsEmpty() != _old.IsEmpty())
             {
-                var status = (PurchaseOrderStatus)after;
-                if (status.Equals(PurchaseOrderStatus.Closed))
-                {
-                    if (ClosedDate == DateTime.MinValue)
-                        ClosedDate = DateTime.Now;
-                    if (IssuedDate == DateTime.MinValue)
-                        IssuedDate = DateTime.Now;
-                }
-                else if (status == PurchaseOrderStatus.Issued)
-                {
-                    if (ClosedDate != DateTime.MinValue)
-                        ClosedDate = DateTime.MinValue;
-                    if (IssuedDate == DateTime.MinValue)
-                        IssuedDate = DateTime.Now;
-                }
-                else if(status == PurchaseOrderStatus.Cancelled)
-                {
-                    CancelledDate = DateTime.Now;
-                }
-                else
-                {
-                    if (ClosedDate != DateTime.MinValue)
-                        ClosedDate = DateTime.MinValue;
-                    if (IssuedDate != DateTime.MinValue)
-                        IssuedDate = DateTime.MinValue;
-                }
+                Employee? _emp = _new.IsEmpty()
+                    ? new Employee()
+                    : Client.Query<Employee>(
+                        new Filter<Employee>(x => x.UserLink.ID).IsEqualTo(ClientFactory.UserGuid),
+                        Columns.None<Employee>().Add(x => x.ID).Add(x => x.Name)
+                    ).Rows.FirstOrDefault()?.ToObject<Employee>();
+                IssuedBy.ID = _emp.ID;
+                IssuedBy.Synchronise(_emp);
             }
-            else
-            {
-                var status = PurchaseOrderStatus.Closed;
-                if (CancelledDate != DateTime.MinValue)
-                    status = PurchaseOrderStatus.Cancelled;
-                else if (ClosedDate != DateTime.MinValue)
-                    status = PurchaseOrderStatus.Closed;
-                else if (IssuedDate != DateTime.MinValue)
-                    status = PurchaseOrderStatus.Issued;
-                else
-                    status = PurchaseOrderStatus.Draft;
-                if (Status != status)
-                    Status = status;
-            }
-
             bChanging = false;
         }
 

+ 0 - 1
prs.classes/Entities/PurchaseOrder/PurchaseOrderItem.cs

@@ -44,7 +44,6 @@ namespace Comal.Classes
         
         [RequiredColumn]
         [EntityRelationship(DeleteAction.Cascade)]
-        [NullEditor]
         public PurchaseOrderLink PurchaseOrderLink { get; set; }
         
         [EntityRelationship(DeleteAction.SetNull)]

+ 1 - 1
prs.desktop/Dashboards/Accounts/OpenPurchaseOrdersDashboard.cs

@@ -59,7 +59,7 @@ namespace PRSDesktop.Dashboards
 
         protected override void Reload(Filters<PurchaseOrder> criteria, Columns<PurchaseOrder> columns, ref SortOrder<PurchaseOrder>? sort, Action<CoreTable?, Exception?> action)
         {
-            criteria.Add(new Filter<PurchaseOrder>(x => x.Status).IsEqualTo(PurchaseOrderStatus.Issued));
+            criteria.Add(new Filter<PurchaseOrder>(x => x.IssuedDate).IsNotEqualTo(DateTime.MinValue));
             base.Reload(criteria, columns, ref sort, action);
         }
         

+ 30 - 13
prs.desktop/Panels/PurchaseOrders/SupplierPurchaseOrderItemOneToMany.cs

@@ -419,8 +419,9 @@ public class SupplierPurchaseOrderItemOneToMany : DynamicOneToManyGrid<PurchaseO
         {
             var items = LoadItems(rows);
             foreach (var item in items)
-                item.ReceivedDate = now;
-            new Client<PurchaseOrderItem>().Save(items, "Consignment Items Received");
+                item.ReceivedDate = item.ReceivedDate.IsEmpty() ? now : DateTime.MinValue;
+            now = items.Select(x => x.ReceivedDate).FirstOrDefault();
+            new Client<PurchaseOrderItem>().Save(items, now.IsEmpty() ? "Cleared Received Date" : $"Updated Received Date to {now:g}");
         }
 
         return true;
@@ -477,25 +478,41 @@ public class SupplierPurchaseOrderItemOneToMany : DynamicOneToManyGrid<PurchaseO
 
     protected override void SelectItems(CoreRow[]? rows)
     {
+        // Check if we can actually edit the selected lines
+        var _editable = !ReadOnly && Security.CanEdit<Consignment>();
+        
+        // Check to ensure NO selected lines are received
+        var _allunreceived = rows?.All(r => r.Get<PurchaseOrderItem, DateTime>(c => c.ReceivedDate).IsEmpty()) == true;
+        
+        // Check to ensure ALL select lines are received
+        var _allreceived = rows?.All(r => !r.Get<PurchaseOrderItem, DateTime>(c => c.ReceivedDate).IsEmpty()) == true;
+        
+        // Check to ensure NO selected lines are already linked to a PO
+        var _anyconsigmments = rows?.Any(r => r.Get<PurchaseOrderItem,Guid>(c=>c.Consignment.ID) != Guid.Empty) == true;
+        
+        // Check to Ensure NO selected lines are already linked to a Bill
+        var _anybills = rows?.Any(r => r.Get<PurchaseOrderItem,Guid>(c=>c.BillLine.ID) != Guid.Empty) == true;
+
         if (createConsignment != null)
         {
             createConsignment.IsEnabled =
-                rows != null
-                && !rows.Any(r => Entity.IsEntityLinkValid<PurchaseOrderItem, ConsignmentLink>(x => x.Consignment, r))
-                && !rows.Any(r => !r.Get<PurchaseOrderItem, DateTime>(c => c.ReceivedDate).IsEmpty())
-                && !ReadOnly && Security.CanEdit<Consignment>();
+                _anyconsigmments == false
+                && _allunreceived
+                && _editable;
         }
 
+        receive.Content = _allreceived
+            ? "Un-Receive Items"
+            : "Receive Items";
+        
         receive.IsEnabled =
-            rows != null
-            && !rows.Any(r => Entity.IsEntityLinkValid<PurchaseOrderItem, ConsignmentLink>(x => x.Consignment, r))
-            && !rows.Any(r => r.Get<PurchaseOrderItem, DateTime>(c => c.ReceivedDate).IsEmpty() == false)
-            && !ReadOnly && Security.CanEdit<PurchaseOrderItem>();
+            _anyconsigmments == false
+            && (_allreceived || _allunreceived)
+            && _editable;
 
         bill.IsEnabled =
-            rows != null
-            && !rows.Any(r => r.IsEntityLinkValid<PurchaseOrderItem, BillLineLink>(x => x.BillLine))
-            && !ReadOnly && Security.CanEdit<PurchaseOrderItem>();
+            _anybills == false
+            && _editable;
 
         assignLocation.IsEnabled =
             rows != null && !ReadOnly && Security.CanEdit<PurchaseOrderItem>();

+ 32 - 1
prs.desktop/Panels/PurchaseOrders/SupplierPurchaseOrders.cs

@@ -42,6 +42,10 @@ public class SupplierPurchaseOrders : DynamicDataGrid<PurchaseOrder>
     
     private SupplierPurchaseOrdersSettings _settings;
 
+    public BitmapImage _truck = PRSDesktop.Resources.truck.AsBitmapImage();
+    public BitmapImage _tick = PRSDesktop.Resources.tick.AsBitmapImage();
+    public BitmapImage _warning = PRSDesktop.Resources.warning.AsBitmapImage();
+
     public SupplierPurchaseOrders()
     {
         _settings = new UserConfiguration<SupplierPurchaseOrdersSettings>().Load();
@@ -51,14 +55,41 @@ public class SupplierPurchaseOrders : DynamicDataGrid<PurchaseOrder>
         OnEditorValueChanged += SupplierPurchaseOrders_OnEditorValueChanged;
         OnCustomiseEditor += SupplierPurchaseOrders_OnCustomiseEditor;
         
+        HiddenColumns.Add(x => x.IssuedDate);
         HiddenColumns.Add(x => x.ClosedDate);
         HiddenColumns.Add(x => x.Balance);
-
+        HiddenColumns.Add(x => x.Unreceived);
+        HiddenColumns.Add(x => x.Received);
+        
+        ActionColumns.Add(new DynamicImageColumn(IssuedStatus));
+        ActionColumns.Add(new DynamicImageColumn(ReceivedStatus));
+        
         PostUtils.AddPostColumn(this);
 
         close = AddButton("Close Order", null, CloseOrder);
         close.IsEnabled = false;
     }
+
+    private BitmapImage? IssuedStatus(CoreRow? row)
+    {
+        return row == null
+            ? _tick
+            : !row.Get<PurchaseOrder, DateTime>(x => x.IssuedDate).IsEmpty()
+                ? _tick
+                : null;
+    }
+
+    private BitmapImage? ReceivedStatus(CoreRow? row)
+    {
+        return row == null
+            ? _truck
+            : row.Get<PurchaseOrder, double>(x => x.Unreceived).IsEffectivelyEqual(0.0)
+                ? _tick
+                : !row.Get<PurchaseOrder, double>(x => x.Received).IsEffectivelyEqual(0.0)
+                    ? _warning
+                    : null;
+    }
+
     protected override void DoReconfigure(DynamicGridOptions options)
     {
         base.DoReconfigure(options);

+ 1 - 1
prs.desktop/prsdesktop.iss

@@ -8,7 +8,7 @@
 #define public Dependency_Path_NetCoreCheck "dependencies\"
 
 #define MyAppName "PRS Desktop"
-#define MyAppVersion "8.08b"
+#define MyAppVersion "8.08e"
 #define MyAppPublisher "PRS Digital"
 #define MyAppURL "https://www.prs-software.com.au"
 #define MyAppExeName "PRSDesktop.exe"

+ 1 - 1
prs.licensing/PRSLicensing.iss

@@ -8,7 +8,7 @@
 #define public Dependency_Path_NetCoreCheck "dependencies\"
 
 #define MyAppName "PRS Licensing"
-#define MyAppVersion "8.08b"
+#define MyAppVersion "8.08e"
 #define MyAppPublisher "PRS Digital"
 #define MyAppURL "https://www.prs-software.com.au"
 #define MyAppExeName "PRSLicensing.exe"

+ 1 - 1
prs.server/PRSServer.iss

@@ -8,7 +8,7 @@
 #define public Dependency_Path_NetCoreCheck "dependencies\"
 
 #define MyAppName "PRS Server"
-#define MyAppVersion "8.08b"
+#define MyAppVersion "8.08e"
 #define MyAppPublisher "PRS Digital"
 #define MyAppURL "https://www.prs-software.com.au"
 #define MyAppExeName "PRSServer.exe"

+ 12 - 2
prs.stores/PurchaseOrderItemStore.cs

@@ -324,11 +324,21 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
         { 
             poCost += entity.Cost * entity.Consignment.ExTax / consigntask.Result;
         }        
+        
         foreach (var jri in jris)
         {
-            CreateMovement(entity, locationid, movements, jri, jri.Qty, poCost);
-            _pototal -= jri.Qty;
+            // Going through each jri, make sure we don't allocate more than the po line allows
+            var jriQty = Math.Min(jri.Qty, _pototal);
+            // And reduce the po balance by the jri Allocation
+            _pototal -= jriQty;
+            
+            // Let's not make zero-quantity transactions
+            if (!jriQty.IsEffectivelyEqual(0.0))
+                CreateMovement(entity, locationid, movements, jri, jriQty, poCost);
         }
+        
+        // If there is any left over (ie over-ordered), now we can create a
+        // second transaction to receive the unallocated stock
         if (!_pototal.IsEffectivelyEqual(0.0F))
             CreateMovement(entity, locationid, movements, null, _pototal, poCost);
 

+ 2 - 7
prs.stores/PurchaseOrderStore.cs

@@ -14,11 +14,6 @@ namespace Comal.Stores
         {
             base.BeforeSave(entity);
             Guid? empid = null;
-            if (entity.HasOriginalValue(x => x.IssuedDate) && entity.GetOriginalValue(x => x.IssuedDate).IsEmpty())
-            {
-                empid ??= GetEmployeeIDFromUserGuid();
-                entity.IssuedBy.ID = empid.Value;
-            }
             if (entity.ID == Guid.Empty)
             {
                 empid ??= GetEmployeeIDFromUserGuid();
@@ -32,9 +27,9 @@ namespace Comal.Stores
 
             UpdateTrackingKanban<PurchaseOrderKanban, PurchaseOrder, PurchaseOrderLink>(entity, p =>
             {
-                return p.Status == PurchaseOrderStatus.Closed
+                return !p.ClosedDate.IsEmpty() 
                     ? KanbanStatus.Complete
-                    : p.Status.Equals(PurchaseOrderStatus.Issued)
+                    : !p.IssuedDate.IsEmpty()
                         ? KanbanStatus.Waiting
                         : KanbanStatus.Open;
             });