Prechádzať zdrojové kódy

Merge remote-tracking branch 'origin/frank' into kenric

Kenric Nugteren 11 mesiacov pred
rodič
commit
534de6de47

+ 27 - 1
prs.classes/EnclosedEntities/Dimensions/DimensionUnit.cs

@@ -1,10 +1,12 @@
 using InABox.Core;
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Linq;
 
 namespace Comal.Classes
 {
+    
     public abstract class DimensionUnit : Entity, IRemotable, IPersistent, ISequenceable, IDimensionUnit
     {
         
@@ -43,10 +45,34 @@ namespace Comal.Classes
         [ExpressionEditor(null, typeof(DimensionsExpressionModelGenerator))]
         [EditorSequence(9)]
         public virtual string Format { get; set; } = "\"EACH\"";
-
+        
+        [ScriptEditor]
+        [EditorSequence(10)]
+        public virtual string Conversion { get; set; } = "";
+        
+                
         [NullEditor]
         public long Sequence { get; set; }
 
+        public bool HasDimensions() => HasHeight || HasWidth || HasLength || HasWeight || HasQuantity;
+        
+        public static string ConvertDimensionsMethodName() => "ConvertDimensions";
+
+        public static string DefaultConvertDimensionsScript()
+        {
+            return
+                "using Comal.Classes;\n"+
+                "\n"+
+                "public class Module\n"+
+                "{\n"+
+                "    public void " + ConvertDimensionsMethodName() + "(PurchaseOrderItem item)\n"+
+                "    {\n"+
+        
+                "    }\n"+
+                "}\n";
+        }
+
+
         public bool Validate(List<String> errors)
         {
             bool result = true;

+ 6 - 0
prs.classes/EnclosedEntities/Dimensions/DimensionUnitLink.cs

@@ -44,5 +44,11 @@ namespace Comal.Classes
         [RequiredColumn]
         public string Format { get; set; }
         
+        [NullEditor]
+        [RequiredColumn]
+        public virtual string Conversion { get; set; }
+        
+        public bool HasDimensions() => HasHeight || HasWidth || HasLength || HasWeight || HasQuantity;
+        
     }
 }

+ 2 - 0
prs.classes/EnclosedEntities/Dimensions/IDimensionUnit.cs

@@ -14,5 +14,7 @@ namespace Comal.Classes
         bool HasWeight { get; set; }
         String Formula { get; set; }
         String Format { get; set; }
+        
+        bool HasDimensions();
     }
 }

+ 31 - 82
prs.classes/Entities/Quote/QuoteCostSheet/QuoteCostSheet.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Linq.Expressions;
 using InABox.Core;
 
@@ -46,11 +47,12 @@ namespace Comal.Classes
 
     [UserTracking(typeof(Quote))]
     public class QuoteCostSheet : Entity, IRemotable, IPersistent, IQuoteCostSheet, ILicense<QuotesManagementLicense>, 
-        IStringAutoIncrement<QuoteCostSheet>,
-        IDigitalForm<Quote>, 
-        IDigitalFormInstance<QuoteLink>
+        IStringAutoIncrement<QuoteCostSheet>
     {
         
+        [NullEditor]
+        public long Sequence { get; set; }
+        
         #region IAutoIncrement
         
         public Expression<Func<QuoteCostSheet, String>> AutoIncrementField() => x => x.Number;
@@ -66,13 +68,34 @@ namespace Comal.Classes
         [NullEditor]
         [Obsolete("Replaced with Parent")]
         [EntityRelationship(DeleteAction.Cascade)]
-        public QuoteLink Quote
+        public QuoteLink Quote { get; set; }
+        
+        [CodeEditor]
+        [EditorSequence(1)]
+        public string Number { get; set; }
+        
+        [TextBoxEditor(Visible = Visible.Default)]
+        [EditorSequence(2)]
+        public string Description { get; set; }
+        
+        private class QuoteCostSheetFormLookup : LookupDefinitionGenerator<QuoteForm, QuoteCostSheet>
         {
-            get => Parent;
-            set => Parent = value;
-        }
+            public override Filter<QuoteForm> DefineFilter(QuoteCostSheet[] items)
+            {
+                if (items?.Any() != true)
+                    return LookupFactory.DefineFilter<QuoteForm>();
+                return new Filter<QuoteForm>(x => x.Parent.ID).IsEqualTo(items.First().Quote.ID);
+            }
 
+            public override Columns<QuoteCostSheet> DefineFilterColumns()
+                => new Columns<QuoteCostSheet>(ColumnTypeFlags.Required);
+        }
+        [LookupDefinition(typeof(QuoteCostSheetFormLookup))]
+        [EditorSequence(3)]
+        public QuoteFormLink Form { get; set; }
+        
         [EntityRelationship(DeleteAction.SetNull)]
+        [EditorSequence(4)]
         public CostSheetLink CostSheet { get; set; }
 
         [CurrencyEditor(Visible = Visible.Default, Editable = Editable.Hidden, Summary = Summary.Sum)]
@@ -86,80 +109,6 @@ namespace Comal.Classes
         [CurrencyEditor(Visible = Visible.Optional, Editable = Editable.Hidden, Summary = Summary.Sum)]
         [Aggregate(typeof(QuoteCostSheetIncTax))]
         public double IncTax { get; set; }
-
-
-
-        [TextBoxEditor(Visible = Visible.Default)]
-        public string Description { get; set; }
-
-
-
-        [NullEditor]
-        public long Sequence { get; set; }
-        
-        #region DigitalFormInstance stuff
-
-        
-        [NullEditor]
-        public QAFormLink QAForm { get; set; }
-        
-        [NullEditor]
-        public string QAData { get; set; }
-        
-        [NullEditor]
-        public string Number { get; set; }
         
-        [NullEditor]
-        public string FormData { get; set; }
-        
-        [NullEditor]
-        public string? BlobData { get; set; }
-        
-        [NullEditor]
-        public DateTime QACompleted { get; set; }
-        
-        [NullEditor]
-        public UserLink QACompletedBy { get; set; }
-        
-        [NullEditor]
-        public UserLink FormCompletedBy { get; set; }
-        
-        [NullEditor]
-        public Location Location { get; set; }
-        
-        [NullEditor]
-        public QuoteLink Parent { get; set; }
-        
-        [NullEditor]
-        public DigitalFormLink Form { get; set; }
-        
-        [NullEditor]
-        public DateTime FormStarted { get; set; }
-        
-        [NullEditor]
-        public TimeSpan FormOpen { get; set; }
-        
-        [NullEditor]
-        public DateTime FormCompleted { get; set; }
-        
-        [NullEditor]
-        public DateTime FormProcessed { get; set; }
-        
-        [NullEditor]
-        public DateTime FormCancelled { get; set; }
-
-        public Guid ParentID() => Quote.ID;
-        
-        public Type ParentType() => typeof(Quote);
-        
-        public IDigitalFormDataModel CreateDataModel(Entity? parent = null)
-        {
-            var t = typeof(DigitalFormDataModel<,,>).MakeGenericType(typeof(Quote), typeof(QuoteLink), GetType());
-            if (parent != null)
-                return (Activator.CreateInstance(t, parent, this) as IDigitalFormDataModel)!;
-            return (Activator.CreateInstance(t, Parent.ID, ID) as IDigitalFormDataModel)!;
-        }
-        
-        # endregion
-    }
+     }
 }

+ 6 - 0
prs.classes/Entities/Quote/QuoteForm.cs

@@ -7,4 +7,10 @@ namespace Comal.Classes
     {
         public override string AutoIncrementPrefix() => "QF";
     }
+
+    public class QuoteFormLink : EntityLink<QuoteForm>
+    {
+        [LookupEditor(typeof(QuoteForm))]
+        public override Guid ID { get; set; }
+    }
 }

+ 23 - 16
prs.desktop/Dashboards/Common/DigitalFormsDashboard.xaml.cs

@@ -1050,25 +1050,32 @@ public partial class DigitalFormsDashboard : UserControl,
 
     private static bool CategoryToType(string category, [NotNullWhen(true)] out Type? formType, [NotNullWhen(true)] out Type? parentType)
     {
-        FormInstanceTypes ??= CoreUtils.TypeList(
-            AppDomain.CurrentDomain.GetAssemblies(),
-            x => !x.IsAbstract && x.GetInterfaces().Contains(typeof(IDigitalFormInstance))
-        ).Select(x =>
-        {
-            var inter = x.GetInterfaces()
-                .Where(x => x.IsGenericType && x.GetGenericTypeDefinition().Equals(typeof(IDigitalFormInstance<>))).FirstOrDefault();
-            if (inter is not null)
+        if (FormInstanceTypes == null)
+        {
+            var instancetypes = CoreUtils.TypeList(
+                AppDomain.CurrentDomain.GetAssemblies(),
+                x => !x.IsAbstract && x.GetInterfaces().Contains(typeof(IDigitalFormInstance))
+            ).Select(x =>
             {
-                var link = inter.GenericTypeArguments[0];
-                var entityLinkDef = link.GetSuperclassDefinition(typeof(EntityLink<>));
-                if (entityLinkDef is not null)
+                var inter = x.GetInterfaces()
+                    .Where(x => x.IsGenericType && x.GetGenericTypeDefinition().Equals(typeof(IDigitalFormInstance<>)))
+                    .FirstOrDefault();
+                if (inter is not null)
                 {
-                    var entityType = entityLinkDef.GenericTypeArguments[0];
-                    return new Tuple<string, Type, Type>(entityType.Name, x, entityType);
+                    var link = inter.GenericTypeArguments[0];
+                    var entityLinkDef = link.GetSuperclassDefinition(typeof(EntityLink<>));
+                    if (entityLinkDef is not null)
+                    {
+                        var entityType = entityLinkDef.GenericTypeArguments[0];
+                        return new Tuple<string, Type, Type>(entityType.Name, x, entityType);
+                    }
                 }
-            }
-            return null;
-        }).Where(x => x is not null).ToDictionary(x => x!.Item1, x => new Tuple<Type, Type>(x!.Item2, x!.Item3));
+
+                return null;
+            }).Where(x => x is not null);
+            FormInstanceTypes =
+                instancetypes.ToDictionary(x => x!.Item1, x => new Tuple<Type, Type>(x!.Item2, x!.Item3));
+        }
 
         if (!FormInstanceTypes.TryGetValue(category, out var result))
         {

+ 23 - 0
prs.desktop/Grids/ProductDimensionUnitGrid.cs

@@ -1,5 +1,7 @@
 using System.Collections.Generic;
+using System.Linq;
 using Comal.Classes;
+using InABox.Core;
 using InABox.DynamicGrid;
 
 namespace PRSDesktop.Grids
@@ -12,5 +14,26 @@ namespace PRSDesktop.Grids
             foreach (var item in items)
                 item.Validate(errors);
         }
+
+        protected override void CustomiseEditor(ProductDimensionUnit[] items, DynamicGridColumn column, BaseEditor editor)
+        {
+            base.CustomiseEditor(items, column, editor);
+            if (column.ColumnName == nameof(ProductDimensionUnit.Conversion) && editor is ScriptEditor scriptEditor)
+            {
+                scriptEditor.Type = ScriptEditorType.TemplateEditor;
+                scriptEditor.OnEditorClicked += () =>
+                {
+                    var script = items.FirstOrDefault()?.Conversion.NotWhiteSpaceOr()
+                                 ?? DimensionUnit.DefaultConvertDimensionsScript();
+
+                    var editor = new ScriptEditorWindow(script, SyntaxLanguage.CSharp);
+                    if (editor.ShowDialog() == true)
+                    {
+                        foreach (var item in items)
+                            SetEditorValue(item, column.ColumnName, editor.Script);
+                    }
+                };
+            }
+        }
     }
 }

+ 6 - 6
prs.desktop/Panels/Jobs/ProjectsPanel.cs

@@ -100,12 +100,12 @@ public class ProjectsPanel : MasterDetailPanel<Job,ProjectsGrid,ProjectsPanelSet
     {
         if (page is null)
             return;
-        var isvisible = _settings.VisiblePanels.TryGetValue(type.EntityName().Split('.').Last(), out bool visible)
-                ? visible
-                : true;
-        page.Tab.Visibility = isvisible
-            ? Visibility.Visible
-            : Visibility.Collapsed;
+        
+        page.Tab.Visibility = _settings.VisiblePanels.TryGetValue(type.EntityName().Split('.').Last(), out bool visible)
+            ? visible 
+                ? Visibility.Visible 
+                : Visibility.Collapsed
+            : Visibility.Visible;
     }
 
     private void SetStockRelease()

+ 32 - 3
prs.desktop/Panels/Products/Locations/StockHoldingGrid.cs

@@ -246,10 +246,39 @@ public class StockHoldingGrid : DynamicDataGrid<StockHolding>
             column.AddItem("View Requisition Items", null, ViewRequisitions_Click);
         
         column.AddSeparator();
-
-        var requiitems = holding.LoadRequisitionItems(true).ToList();
         
-        column.AddItem("Relocate Items", null, r => RelocateItems(holding, requiitems.ToArray()));
+        column.AddItem("Relocate Items", null, r =>
+        {
+            var requiitems = holding.LoadRequisitionItems(true).ToList();
+            RelocateItems(holding, requiitems.ToArray());
+        });
+
+        if (holding.Dimensions.Unit.HasDimensions() && holding.Available.IsEffectivelyGreaterThan(0.0))
+            column.AddItem("Convert Dimensions", null, r =>
+            {
+                var calculator = new StockTransformWindow(holding);
+                if (calculator.ShowDialog() == true)
+                {
+                    var transferout = holding.CreateMovement();
+                    transferout.Date = DateTime.Now;
+                    transferout.Issued = calculator.OldAvailable;
+                    transferout.Transaction = Guid.NewGuid();
+                    transferout.Type = StockMovementType.TransferOut;
+
+                    var transferin = holding.CreateMovement();
+                    transferin.Date = transferout.Date.AddTicks(1);
+                    transferin.Dimensions.CopyFrom(calculator.Dimensions);
+                    transferin.Received = calculator.NewAvailable;
+                    transferin.Transaction = transferout.Transaction;
+                    transferin.Type = StockMovementType.TransferIn;
+                    
+                    Client.Save([transferout,transferin], "Converted Dimensions");
+                    
+                    Refresh(false,true);
+                }
+                    
+            });
+
     }
 
     private class StockIssue : BaseObject

+ 283 - 0
prs.desktop/Panels/Products/Locations/StockTransformWindow.xaml

@@ -0,0 +1,283 @@
+<Window x:Class="PRSDesktop.StockTransformWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:local="clr-namespace:PRSDesktop"
+        xmlns:wpf="clr-namespace:InABox.WPF;assembly=InABox.Wpf"
+        xmlns:syncfusion="http://schemas.syncfusion.com/wpf"
+        mc:Ignorable="d"
+        Title="Convert Dimensions" Height="400" Width="300" SizeToContent="Height" WindowStartupLocation="CenterScreen">
+    <Window.DataContext>
+        <local:StockTransformWindowViewModel x:Name="_viewModel"/>
+    </Window.DataContext>
+    <Window.Resources>
+        <wpf:BooleanToGridLengthConverter x:Key="BooleanToGridLengthConverter" TrueValue="40" FalseValue="0" />
+    </Window.Resources>
+    <Grid Margin="5">
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="*"/>
+            <ColumnDefinition Width="80"/>
+            <ColumnDefinition Width="80"/>
+        </Grid.ColumnDefinitions>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="40"/>
+            <RowDefinition Height="{Binding OldDimensions.Unit.HasHeight, Converter={StaticResource BooleanToGridLengthConverter}}"/>
+            <RowDefinition Height="{Binding OldDimensions.Unit.HasWidth, Converter={StaticResource BooleanToGridLengthConverter}}"/>
+            <RowDefinition Height="{Binding OldDimensions.Unit.HasLength, Converter={StaticResource BooleanToGridLengthConverter}}"/>
+            <RowDefinition Height="{Binding OldDimensions.Unit.HasQuantity, Converter={StaticResource BooleanToGridLengthConverter}}"/>
+            <RowDefinition Height="{Binding OldDimensions.Unit.HasWeight, Converter={StaticResource BooleanToGridLengthConverter}}"/>
+            <RowDefinition Height="10"/>
+            <RowDefinition Height="40"/>
+            <RowDefinition Height="40"/>
+            <RowDefinition Height="10"/>
+            <RowDefinition Height="40"/>
+        </Grid.RowDefinitions>
+        
+        <syncfusion:SfTextBoxExt
+            Grid.Row="0"
+            Grid.Column="1"
+            Text="Current"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Margin="5,5,0,0"
+            Background="WhiteSmoke"/>
+        <syncfusion:SfTextBoxExt
+            Grid.Row="0"
+            Grid.Column="2"
+            Text="New"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Margin="5,5,0,0"
+            Background="WhiteSmoke"
+            IsReadOnly="True"/>
+        
+        <syncfusion:SfTextBoxExt
+            Grid.Row="1"
+            Grid.Column="0"
+            Text="Height"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            Margin="5,5,0,0"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding OldDimensions.Height}"
+            Grid.Row="1"
+            Grid.Column="1"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding NewDimensions.Height, Mode=TwoWay}"
+            Grid.Row="1"
+            Grid.Column="2"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="LightYellow"/>
+        
+        <syncfusion:SfTextBoxExt
+            Grid.Row="2"
+            Grid.Column="0"
+            Text="Width"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            Margin="5,5,0,0"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding OldDimensions.Width}"
+            Grid.Row="2"
+            Grid.Column="1"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding NewDimensions.Width, Mode=TwoWay}"
+            Grid.Row="2"
+            Grid.Column="2"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="LightYellow"/>
+        
+        <syncfusion:SfTextBoxExt
+            Grid.Row="3"
+            Grid.Column="0"
+            Text="Length"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            Margin="5,5,0,0"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding OldDimensions.Length}"
+            Grid.Row="3"
+            Grid.Column="1"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding NewDimensions.Length, Mode=TwoWay}"
+            Grid.Row="3"
+            Grid.Column="2"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="LightYellow"/>
+        
+        <syncfusion:SfTextBoxExt
+            Grid.Row="4"
+            Grid.Column="0"
+            Text="Quantity"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            Margin="5,5,0,0"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding OldDimensions.Quantity}"
+            Grid.Row="4"
+            Grid.Column="1"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding NewDimensions.Quantity, Mode=TwoWay}"
+            Grid.Row="4"
+            Grid.Column="2"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="LightYellow"/>
+        
+        <syncfusion:SfTextBoxExt
+            Grid.Row="5"
+            Grid.Column="0"
+            Text="Weight"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            Margin="5,5,0,0"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding OldDimensions.Weight}"
+            Grid.Row="5"
+            Grid.Column="1"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding NewDimensions.Weight, Mode=TwoWay}"
+            Grid.Row="5"
+            Grid.Column="2"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="LightYellow"/>
+        
+        <syncfusion:SfTextBoxExt
+            Grid.Row="7"
+            Grid.Column="0"
+            Text="Unit Size"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            Margin="5,5,0,0"
+            IsReadOnly="True"/>
+        <syncfusion:SfTextBoxExt
+
+            Text="{Binding OldUnitSize}"
+            Grid.Row="7"
+            Grid.Column="1"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            IsReadOnly="True"/>
+        <syncfusion:SfTextBoxExt
+            Text="{Binding NewUnitSize}"
+            Grid.Row="7"
+            Grid.Column="2"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            IsReadOnly="True"/>
+
+        <syncfusion:SfTextBoxExt
+            Grid.Row="8"
+            Grid.Column="0"
+            Text="Available"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            Margin="5,5,0,0"
+            IsReadOnly="True"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding OldAvailable, Mode=TwoWay}"
+            MaxValue="{Binding MaxAvailable}"
+            Grid.Row="8"
+            Grid.Column="1"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="LightYellow"/>
+        <syncfusion:DoubleTextBox
+            Value="{Binding NewAvailable}"
+            Grid.Row="8"
+            Grid.Column="2"
+            Margin="5,5,0,0"
+            VerticalAlignment="Stretch"
+            HorizontalAlignment="Stretch"
+            HorizontalContentAlignment="Center"
+            VerticalContentAlignment="Center"
+            Background="WhiteSmoke"
+            IsReadOnly="True"/>
+
+
+        <Button
+            Grid.Row="10"
+            Grid.Column="1"
+            Content="OK"
+            Margin="5,5,0,0"
+            Click="OK_Click"/>
+        <Button
+            Grid.Row="10"
+            Grid.Column="2"
+            Content="Cancel"
+            Margin="5,5,0,0"
+            Click="Cancel_Click"/>
+
+    </Grid>
+</Window>

+ 112 - 0
prs.desktop/Panels/Products/Locations/StockTransformWindow.xaml.cs

@@ -0,0 +1,112 @@
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using Comal.Classes;
+
+namespace PRSDesktop;
+
+public class StockTransformWindowViewModel : INotifyPropertyChanged
+{
+    private double _oldAvailable;
+    private double _newAvailable;
+
+    public void Load(StockHolding holding)
+    {
+        OldDimensions.CopyFrom(holding.Dimensions, true);
+        NewDimensions.CopyFrom(holding.Dimensions, true);
+        MaxAvailable = holding.Available;
+        OldAvailable = holding.Available;
+        NewAvailable = holding.Available;
+    }
+
+    public void Unload(StockHolding holding)
+    {
+        holding.Dimensions.CopyFrom(NewDimensions, true);
+        holding.Units = NewAvailable;
+    }
+    
+    public StockDimensions OldDimensions { get; private set; }
+
+    public StockDimensions NewDimensions { get; private set; }
+
+    public string OldUnitSize { get; set; }
+    
+    public string NewUnitSize { get; set; }
+
+    public double MaxAvailable { get; private set; }
+    
+    public double OldAvailable
+    {
+        get => _oldAvailable;
+        set
+        {
+            if (value.Equals(_oldAvailable)) return;
+            _oldAvailable = value;
+            NewAvailable = value * OldDimensions.Value / NewDimensions.Value;
+            OnPropertyChanged();
+        }
+    }
+
+    public double NewAvailable
+    {
+        get => _newAvailable;
+        set
+        {
+            if (value.Equals(_newAvailable)) return;
+            _newAvailable = value;
+            OnPropertyChanged();
+        }
+    }
+
+    public StockTransformWindowViewModel()
+    {
+        OldDimensions = new StockDimensions();
+        NewDimensions = new StockDimensions();
+        NewDimensions.PropertyChanged += (sender, args) =>
+        {
+            NewAvailable = OldAvailable * OldDimensions.Value / NewDimensions.Value;
+            OldUnitSize = OldDimensions.UnitSize;
+            NewUnitSize = NewDimensions.UnitSize;
+        };
+    }
+
+    public event PropertyChangedEventHandler? PropertyChanged;
+
+    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
+
+    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
+    {
+        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
+        field = value;
+        OnPropertyChanged(propertyName);
+        return true;
+    }
+}
+
+public partial class StockTransformWindow : Window
+{
+    
+    public StockDimensions Dimensions => _viewModel.NewDimensions;
+    public double NewAvailable => _viewModel.NewAvailable;
+    public double OldAvailable => _viewModel.OldAvailable;
+    
+    public StockTransformWindow(StockHolding holding)
+    {
+        InitializeComponent();
+        _viewModel.Load(holding);
+    }
+
+    private void OK_Click(object sender, RoutedEventArgs e)
+    {
+        DialogResult = true;
+    }
+
+    private void Cancel_Click(object sender, RoutedEventArgs e)
+    {
+        DialogResult = false;
+    }
+}

+ 7 - 0
prs.desktop/Panels/Products/Master List/ProductsGrid.cs

@@ -298,5 +298,12 @@ namespace PRSDesktop
                 }
             }
         }
+
+        protected override void DoValidate(Product[] items, List<string> errors)
+        {
+            base.DoValidate(items, errors);
+            if (items.Any(x=>x.UnitOfMeasure.ID == Guid.Empty))
+                errors.Add("Unit of Measure may not be blank!");
+        }
     }
 }

+ 1 - 1
prs.desktop/prsdesktop.iss

@@ -8,7 +8,7 @@
 #define public Dependency_Path_NetCoreCheck "dependencies\"
 
 #define MyAppName "PRS Desktop"
-#define MyAppVersion "8.03b"
+#define MyAppVersion "8.04b"
 #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.03b"
+#define MyAppVersion "8.04b"
 #define MyAppPublisher "PRS Digital"
 #define MyAppURL "https://www.prs-software.com.au"
 #define MyAppExeName "PRSLicensing.exe"

+ 6 - 4
prs.server/Engines/GPS/GPSEngine.cs

@@ -109,8 +109,9 @@ public class GPSUpdateQueue
                 using var fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);
                 deviceUpdate = Serialization.ReadBinary<GPSDeviceUpdate>(fileStream, BinarySerializationSettings.Latest);
             }
-            catch
+            catch (Exception e)
             {
+                Logger.Send(LogType.Error, "", string.Format("Unable to retrieve file: {0}", filename));
                 // File is probably in use.
             }
             if(deviceUpdate is not null)
@@ -160,7 +161,7 @@ public class GPSEngine : Engine<GPSServerProperties>
 {
     private Listener<SigfoxHandler, SigfoxHandlerProperties> sigfoxListener;
     private OEMListener oemListener;
-    private RpcClientPipeTransport transport;
+    
 
     private GPSDeviceCache DeviceCache = new();
 
@@ -234,7 +235,7 @@ public class GPSEngine : Engine<GPSServerProperties>
 
     private void UpdateServer()
     {
-        if (!transport.IsConnected()) return;
+        if (Transport?.IsConnected() != true) return;
 
         // Cache a set of fifty, so that we're not running baack and forth to the filesystem all the time.
         if(LocationQueueCache.Count == 0)
@@ -306,7 +307,8 @@ public class GPSEngine : Engine<GPSServerProperties>
     public override void Stop()
     {
         oemListener.Stop();
-        sigfoxListener.Stop();
+        if (sigfoxListener != null)
+            sigfoxListener.Stop();
         UpdateServerTimer.Stop();
         RefreshDevicesTimer.Stop();
     }

+ 1 - 1
prs.server/PRSServer.iss

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

+ 5 - 3
prs.services/Engine.cs

@@ -28,6 +28,8 @@ public abstract class Engine<TProperties> : IEngine where TProperties : ServerPr
 {
     
     private RpcServerPipeTransport _enginemanager;
+    
+    protected  RpcClientPipeTransport? Transport;
 
     public TProperties Properties { get; private set; }
     public abstract void Run();
@@ -88,9 +90,9 @@ public abstract class Engine<TProperties> : IEngine where TProperties : ServerPr
     /// </summary>
     protected void InitialiseConnection(string serverKey, Platform clientPlatform)
     {
-        var transport = new RpcClientPipeTransport(DatabaseServerProperties.GetPipeName(serverKey, true));
-        ClientFactory.SetClientType(typeof(RpcClient<>), clientPlatform, Version, transport);
-        transport.OnClose += Transport_OnClose;
+        Transport = new RpcClientPipeTransport(DatabaseServerProperties.GetPipeName(serverKey, true));
+        ClientFactory.SetClientType(typeof(RpcClient<>), clientPlatform, Version, Transport);
+        Transport.OnClose += Transport_OnClose;
         CheckConnection();
     }
 

+ 1 - 4
prs.shared/PRS.Shared.csproj

@@ -26,9 +26,6 @@
       <AutoGen>True</AutoGen>
       <DependentUpon>Resources.resx</DependentUpon>
     </Compile>
-    <Compile Include="..\prs.stores\StockHoldingStore.cs">
-      <Link>prs.stores\StockHoldingStore.cs</Link>
-    </Compile>
   </ItemGroup>
 
   <ItemGroup>
@@ -57,5 +54,5 @@
   </ItemGroup>
 
   <Import Project="..\prs.stores\PRSStores.projitems" Label="Shared" />
-
+  
 </Project>

+ 44 - 1
prs.stores/PurchaseOrderItemStore.cs

@@ -5,6 +5,8 @@ using Comal.Classes;
 using InABox.Core;
 using PRSStores;
 using System;
+using InABox.Database;
+using InABox.Scripting;
 using NPOI.SS.Formula.Functions;
 using Columns = InABox.Core.Columns;
 
@@ -13,6 +15,44 @@ namespace Comal.Stores;
 internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
 {
 
+    static PurchaseOrderItemStore()
+    {
+        RegisterListener<ProductDimensionUnit>(ReloadProductDimensionUnitCache);
+    }
+    
+    private static Dictionary<Guid, ScriptDocument>? _productdimensionunitcache = null;
+    
+    private static void ReloadProductDimensionUnitCache(Guid[]? ids)
+    {
+        if (_productdimensionunitcache == null)
+            _productdimensionunitcache = new Dictionary<Guid, ScriptDocument>();
+        
+        var scripts = DbFactory.Provider.Query(
+            ids != null
+                ? new Filter<ProductDimensionUnit>(x => x.ID).InList(ids)
+                : null,
+            Columns.None<ProductDimensionUnit>()
+                .Add(x => x.ID)
+                .Add(x => x.Conversion)
+        ).ToDictionary<ProductDimensionUnit, Guid, String>(x => x.ID, x => x.Conversion);
+        
+        foreach (var id in scripts.Keys)
+        {
+                var doc = !String.IsNullOrWhiteSpace(scripts[id]) ? new ScriptDocument(scripts[id]) : null;
+                if (doc?.Compile() == true)
+                    _productdimensionunitcache[id] = doc;
+                else
+                    _productdimensionunitcache.Remove(id);
+        }
+    }
+    
+    private void TransformDimensions(PurchaseOrderItem item)
+    {
+        if (_productdimensionunitcache == null)
+            ReloadProductDimensionUnitCache(null);
+        if (_productdimensionunitcache?.TryGetValue(item.Dimensions.Unit.ID, out ScriptDocument? script) == true)
+            script.Execute("Module",DimensionUnit.ConvertDimensionsMethodName(), [item]);
+    }
 
     private void UpdateStockMovements(PurchaseOrderItem entity)
     {
@@ -26,7 +66,7 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
         }
         FindSubStore<StockMovement>().Save(movements, "Updated by purchase order modification");
     }
-
+    
     private void CreateStockMovements(PurchaseOrderItem entity)
     {
         if (!entity.Product.IsValid())
@@ -216,6 +256,8 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
             Logger.Send(LogType.Information, UserID, "PurchaseOrderItem.Unit Size is zero!");
             entity.Dimensions.CopyFrom(productrow.ToObject<Product>().DefaultInstance.Dimensions);
         }
+        
+        TransformDimensions(entity);
 
         if (entity.Job.ID == Guid.Empty)
         {
@@ -297,6 +339,7 @@ internal class PurchaseOrderItemStore : BaseStore<PurchaseOrderItem>
         }
         
         FindSubStore<StockMovement>().Save(movements, "Updated by Purchase Order Modification");
+        entity.CancelChanges();
     }
 
     private static void CreateMovement(PurchaseOrderItem entity, Guid locationid, List<StockMovement> movements, JobRequisitionItem jri, double qty, double cost)

+ 26 - 4
prs.stores/RequisitionStore.cs

@@ -53,6 +53,7 @@ namespace Comal.Stores
                         .Add(x => x.Style.ID)
                         .Add(x => x.Product.ID)
                         .Add(x => x.JobRequisitionItem.ID)
+                        .Add(x =>x.JobLink.ID)
                         .AddDimensionsColumns(x => x.Dimensions, Dimensions.ColumnsType.Local)
                         .Add(x => x.ActualQuantity)
                         .AddDimensionsColumns(x => x.Product.DefaultInstance.Dimensions, Dimensions.ColumnsType.Local)
@@ -274,14 +275,14 @@ namespace Comal.Stores
             foreach (var item in items)
             {
                 var holdingQty = 0.0;
-                if(entity.JobLink.ID != Guid.Empty)
+                if(item.JobLink.ID != Guid.Empty)
                 {
                     var holdings = Provider.Query<StockHolding>(
                         new Filter<StockHolding>(x => x.Location.ID).IsEqualTo(item.Location.ID)
                             .And(x => x.Product.ID).IsEqualTo(item.Product.ID)
                             .And(x => x.Style.ID).IsEqualTo(item.Style.ID)
                             .And(x => x.Dimensions).DimensionEquals(item.Dimensions)
-                            .And(x => x.Job.ID).IsEqualTo(entity.JobLink.ID)
+                            .And(x => x.Job.ID).IsEqualTo(item.JobLink.ID)
                             .And(x => x.Qty).IsGreaterThan(0.0),
                         Columns.None<StockHolding>().Add(x => x.Qty)
                     );
@@ -308,7 +309,7 @@ namespace Comal.Stores
                     from.Type = StockMovementType.TransferOut;
 
                     // ... to the job.
-                    var to = CreateStockMovement(entity.Employee, entity.Filled, batch, item.Product, item.Location, item.Style, entity.JobLink, item.JobRequisitionItem, dimensions, txnid, true,
+                    var to = CreateStockMovement(entity.Employee, entity.Filled, batch, item.Product, item.Location, item.Style, item.JobLink, item.JobRequisitionItem, dimensions, txnid, true,
                         $"Requisition #{entity.Number} Internal Transfer");
                     to.Received = extraRequired;
                     to.Type = StockMovementType.TransferIn;
@@ -319,7 +320,28 @@ namespace Comal.Stores
                     // Now we have a full qty in the job holding, and we can issue to site.
                 }
 
-                var mvt = CreateStockMovement(entity.Employee, entity.Filled, batch, item.Product, item.Location, item.Style, entity.JobLink, item.JobRequisitionItem, dimensions, txnid,
+                JobRequisitionItemLink? link = item.JobRequisitionItem;
+                if (entity.JobLink.ID != item.JobLink.ID)
+                {
+                    // Transfer from the item job to the requisition job
+                    var from = CreateStockMovement(entity.Employee, entity.Filled, batch, item.Product, item.Location, item.Style, item.JobLink, link,
+                        dimensions, txnid, true, $"Requisition #{entity.Number} Internal Transfer");
+                    from.Issued = qty;
+                    from.Type = StockMovementType.TransferOut;
+
+                    // ... to the job.
+                    var to = CreateStockMovement(entity.Employee, entity.Filled, batch, item.Product, item.Location, item.Style, entity.JobLink, null, dimensions, txnid, true,
+                        $"Requisition #{entity.Number} Internal Transfer");
+                    to.Received = qty;
+                    to.Type = StockMovementType.TransferIn;
+
+                    updates.Add(from);
+                    updates.Add(to);
+
+                    link = null;
+                }
+
+                var mvt = CreateStockMovement(entity.Employee, entity.Filled, batch, item.Product, item.Location, item.Style, entity.JobLink, link, dimensions, txnid,
                     false,
                     $"Requisition #{entity.Number}");
                 mvt.Issued = qty;