Преглед изворни кода

Renamed BillType to BillApprovalSet and added bill Approval settings

Kenric Nugteren пре 6 месеци
родитељ
комит
390317b22d

+ 0 - 0
prs.classes/Entities/Bill/BillApproval.cs → prs.classes/Entities/Bill/Approvals/BillApproval.cs


+ 37 - 0
prs.classes/Entities/Bill/Approvals/BillApprovalSet.cs

@@ -0,0 +1,37 @@
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Comal.Classes
+{
+    public class BillApprovalSet : Entity, IRemotable, IPersistent, ILicense<AccountsPayableLicense>
+    {
+        [UniqueCodeEditor]
+        [EditorSequence(1)]
+        public string Code { get; set; }
+
+        [EditorSequence(2)]
+        public string Description { get; set; }
+
+        [EditorSequence(3)]
+        [Editable(Editable.Hidden)]
+        public bool IsDefault { get; set; }
+
+        static BillApprovalSet()
+        {
+            DefaultColumns.Add<BillApprovalSet>(x => x.Code);
+            DefaultColumns.Add<BillApprovalSet>(x => x.Description);
+        }
+    }
+
+    public class BillApprovalSetLink : EntityLink<BillApprovalSet>
+    {
+        [LookupEditor(typeof(BillApprovalSet))]
+        public override Guid ID { get; set; }
+
+        public string Code { get; set; }
+
+        public string Description { get; set; }
+    }
+}

+ 25 - 0
prs.classes/Entities/Bill/Approvals/BillApprovalSetEmployee.cs

@@ -0,0 +1,25 @@
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Comal.Classes
+{
+    public class BillApprovalSetEmployee : Entity, IRemotable, IPersistent, IManyToMany<BillApprovalSet, Employee>, ISequenceable, ILicense<AccountsPayableLicense>
+    {
+        public BillApprovalSetLink BillType { get; set; }
+
+        public EmployeeLink Employee { get; set; }
+
+        [NullEditor]
+        public long Sequence { get; set; }
+
+        static BillApprovalSetEmployee()
+        {
+            DefaultColumns.Add<BillApprovalSetEmployee>(x => x.BillType.Code);
+            DefaultColumns.Add<BillApprovalSetEmployee>(x => x.BillType.Description);
+            DefaultColumns.Add<BillApprovalSetEmployee>(x => x.Employee.Code);
+            DefaultColumns.Add<BillApprovalSetEmployee>(x => x.Employee.Name);
+        }
+    }
+}

+ 35 - 0
prs.classes/Entities/Bill/Approvals/BillApprovalSettings.cs

@@ -0,0 +1,35 @@
+using AutoProperties;
+using InABox.Configuration;
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Comal.Classes
+{
+    public class BillApprovalSettings : BaseObject, IGlobalConfigurationSettings
+    {
+        [EditorSequence(1)]
+        [ScriptEditor]
+        public string ApprovalSetUpdateScript { get; set; }
+
+        [InterceptIgnore]
+        [DoNotPersist]
+        [DoNotSerialize]
+        public static string UpdateBillApprovalsMethodName => "UpdateBillApprovals";
+
+        public static string DefaultApprovalSetUpdateScript()
+        {
+            return @"
+using Comal.Classes;
+
+public class Module
+{
+    public void " + UpdateBillApprovalsMethodName + @"(Bill bill, List<BillApproval> approvals)
+    {
+        // Update the 'approvals' list whenever the 'bill' is updated.
+    }
+}";
+        }
+    }
+}

+ 1 - 1
prs.classes/Entities/Bill/Bill.cs

@@ -54,7 +54,7 @@ namespace Comal.Classes
 
         [EditorSequence("Additional", 4)]
         [Editable(Editable.Disabled)]
-        public BillTypeLink BillType { get; set; }
+        public BillApprovalSetLink ApprovalSet { get; set; }
         
         private class ExTaxFormula : ComplexFormulaGenerator<Bill, double>
         {

+ 0 - 33
prs.classes/Entities/Bill/BillType.cs

@@ -1,33 +0,0 @@
-using InABox.Core;
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Comal.Classes
-{
-    public class BillType : Entity, IRemotable, IPersistent, ILicense<AccountsPayableLicense>
-    {
-        [UniqueCodeEditor]
-        [EditorSequence(1)]
-        public string Code { get; set; }
-
-        [EditorSequence(2)]
-        public string Description { get; set; }
-
-        static BillType()
-        {
-            DefaultColumns.Add<BillType>(x => x.Code);
-            DefaultColumns.Add<BillType>(x => x.Description);
-        }
-    }
-
-    public class BillTypeLink : EntityLink<BillType>
-    {
-        [LookupEditor(typeof(BillType))]
-        public override Guid ID { get; set; }
-
-        public string Code { get; set; }
-
-        public string Description { get; set; }
-    }
-}

+ 0 - 25
prs.classes/Entities/Bill/BillTypeEmployee.cs

@@ -1,25 +0,0 @@
-using InABox.Core;
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Comal.Classes
-{
-    public class BillTypeEmployee : Entity, IRemotable, IPersistent, IManyToMany<BillType, Employee>, ISequenceable, ILicense<AccountsPayableLicense>
-    {
-        public BillTypeLink BillType { get; set; }
-
-        public EmployeeLink Employee { get; set; }
-
-        [NullEditor]
-        public long Sequence { get; set; }
-
-        static BillTypeEmployee()
-        {
-            DefaultColumns.Add<BillTypeEmployee>(x => x.BillType.Code);
-            DefaultColumns.Add<BillTypeEmployee>(x => x.BillType.Description);
-            DefaultColumns.Add<BillTypeEmployee>(x => x.Employee.Code);
-            DefaultColumns.Add<BillTypeEmployee>(x => x.Employee.Name);
-        }
-    }
-}

+ 34 - 0
prs.desktop/Panels/Suppliers/Bills/BillApprovalSettingsGrid.cs

@@ -0,0 +1,34 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.DynamicGrid;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRSDesktop.Panels.Suppliers.Bills;
+
+public class BillApprovalSettingsGrid : DynamicItemsListGrid<BillApprovalSettings>
+{
+    protected override void CustomiseEditor(BillApprovalSettings[] items, DynamicGridColumn column, BaseEditor editor)
+    {
+        base.CustomiseEditor(items, column, editor);
+        if (column.ColumnName == nameof(BillApprovalSettings.ApprovalSetUpdateScript) && editor is ScriptEditor scriptEditor)
+        {
+            scriptEditor.Type = ScriptEditorType.TemplateEditor;
+            scriptEditor.OnEditorClicked += () =>
+            {
+                var script = items.FirstOrDefault()?.ApprovalSetUpdateScript.NotWhiteSpaceOr()
+                             ?? BillApprovalSettings.DefaultApprovalSetUpdateScript();
+
+                var editor = new ScriptEditorWindow(script, SyntaxLanguage.CSharp);
+                if (editor.ShowDialog() == true)
+                {
+                    foreach (var item in items)
+                        SetEditorValue(item, column.ColumnName, editor.Script);
+                }
+            };
+        }
+    }
+}

+ 10 - 2
prs.desktop/Panels/Suppliers/Bills/SupplierBillPanel.xaml.cs

@@ -75,11 +75,19 @@ public partial class SupplierBillPanel : UserControl, IPanel<Bill>, IPropertiesP
 
         host.CreateSetupSeparator();
 
-        host.CreateSetupActionIfCanView<BillType>("Bill Types", PRSDesktop.Resources.bill, (action) =>
+        host.CreateSetupActionIfCanView<BillApprovalSet>("Bill Approval Sets", PRSDesktop.Resources.bill, (action) =>
         {
-            var list = new MasterList(typeof(BillType));
+            var list = new MasterList(typeof(BillApprovalSet));
             list.ShowDialog();
         });
+        host.CreateSetupActionIf<ManageBillApprovals>("Bill Approval Settings", PRSDesktop.Resources.edit, (action) =>
+        {
+            var settings = new GlobalConfiguration<BillApprovalSettings>().Load();
+            if (DynamicGridUtils.EditEntity(settings))
+            {
+                new GlobalConfiguration<BillApprovalSettings>().Save(settings);
+            }
+        });
     }
 
     public string SectionName => "Supplier Bills";

+ 20 - 20
prs.desktop/Panels/Suppliers/Bills/SupplierBills.cs

@@ -29,7 +29,7 @@ namespace PRSDesktop
             HiddenColumns.Add(x => x.Approved);
             HiddenColumns.Add(x => x.DataEntered);
             HiddenColumns.Add(x => x.Checked);
-            HiddenColumns.Add(x => x.BillType.ID);
+            HiddenColumns.Add(x => x.ApprovalSet.ID);
 
             ActionColumns.Add(new DynamicImageColumn(DataEntered_Image, null) { ToolTip = DataEntered_ToolTip });
             ActionColumns.Add(new DynamicImageColumn(Checked_Image, null) { ToolTip = Checked_ToolTip });
@@ -45,7 +45,7 @@ namespace PRSDesktop
             if (row is null) return;
 
             var menu = column.GetMenu();
-            menu.AddItem("Set Type", null, row, SetBillType);
+            menu.AddItem("Change Approval Set", null, row, SetBillApprovalSet);
         }
 
         public override DynamicEditorPages LoadEditorPages(Bill item)
@@ -70,24 +70,24 @@ namespace PRSDesktop
             return pages;
         }
 
-        private void SetBillType(CoreRow row)
+        private void SetBillApprovalSet(CoreRow row)
         {
             var bill = row.ToObject<Bill>();
 
-            if(MultiSelectDialog<BillType>.SelectItem(
+            if(MultiSelectDialog<BillApprovalSet>.SelectItem(
                 out var billType,
-                filter: new Filter<BillType>(x => x.ID).IsNotEqualTo(bill.BillType.ID),
-                columns: Columns.None<BillType>().Add(x => x.ID),
-                title: "Select Bill Type:"))
+                filter: new Filter<BillApprovalSet>(x => x.ID).IsNotEqualTo(bill.ApprovalSet.ID),
+                columns: Columns.None<BillApprovalSet>().Add(x => x.ID),
+                title: "Select Approval Set:"))
             {
-                BillTypeEmployee[] oldEmployees;
-                if(bill.BillType.ID != Guid.Empty)
+                BillApprovalSetEmployee[] oldEmployees;
+                if(bill.ApprovalSet.ID != Guid.Empty)
                 {
                     oldEmployees = Client.Query(
-                        new Filter<BillTypeEmployee>(x => x.BillType.ID).IsEqualTo(bill.BillType.ID),
-                        Columns.None<BillTypeEmployee>().Add(x => x.Employee.ID),
-                        new SortOrder<BillTypeEmployee>(x => x.Sequence))
-                        .ToArray<BillTypeEmployee>();
+                        new Filter<BillApprovalSetEmployee>(x => x.BillType.ID).IsEqualTo(bill.ApprovalSet.ID),
+                        Columns.None<BillApprovalSetEmployee>().Add(x => x.Employee.ID),
+                        new SortOrder<BillApprovalSetEmployee>(x => x.Sequence))
+                        .ToArray<BillApprovalSetEmployee>();
                 }
                 else
                 {
@@ -95,10 +95,10 @@ namespace PRSDesktop
                 }
 
                 var newEmployees = Client.Query(
-                    new Filter<BillTypeEmployee>(x => x.BillType.ID).IsEqualTo(billType.ID),
-                    Columns.None<BillTypeEmployee>().Add(x => x.Employee.ID),
-                    new SortOrder<BillTypeEmployee>(x => x.Sequence))
-                    .ToArray<BillTypeEmployee>();
+                    new Filter<BillApprovalSetEmployee>(x => x.BillType.ID).IsEqualTo(billType.ID),
+                    Columns.None<BillApprovalSetEmployee>().Add(x => x.Employee.ID),
+                    new SortOrder<BillApprovalSetEmployee>(x => x.Sequence))
+                    .ToArray<BillApprovalSetEmployee>();
 
                 var approvals = Client.Query(
                     new Filter<BillApproval>(x => x.Bill.ID).IsEqualTo(bill.ID),
@@ -136,10 +136,10 @@ namespace PRSDesktop
                 {
                     approval.Sequence = i;
                 }
-                Client.Delete(toDelete, "Deleted by changing bill type.");
-                Client.Save(toSave, "Updated when changing bill type.");
+                Client.Delete(toDelete, "Deleted by changing approval set.");
+                Client.Save(toSave, "Updated when changing approval set.");
 
-                bill.BillType.CopyFrom(billType);
+                bill.ApprovalSet.CopyFrom(billType);
                 SaveItem(bill);
                 Refresh(false, true);
             }

+ 62 - 61
prs.desktop/Panels/Tasks/KanbanNotes.xaml.cs

@@ -6,86 +6,87 @@ using Comal.Classes;
 using InABox.Core;
 using InABox.DynamicGrid;
 
-namespace PRSDesktop
+namespace PRSDesktop;
+
+/// <summary>
+///     Interaction logic for KanbanNotes.xaml
+/// </summary>
+public partial class KanbanNotes : UserControl, IDynamicEditorPage
 {
-    /// <summary>
-    ///     Interaction logic for KanbanNotes.xaml
-    /// </summary>
-    public partial class KanbanNotes : UserControl, IDynamicEditorPage
-    {
-        private readonly string UserID = "";
+    private readonly string UserID = "";
 
-        private bool _readOnly;
-        public bool ReadOnly
+    private bool _readOnly;
+    public bool ReadOnly
+    {
+        get => _readOnly;
+        set
         {
-            get => _readOnly;
-            set
+            if (_readOnly != value)
             {
-                if (_readOnly != value)
-                {
-                    _readOnly = value;
-                    NotesList.IsEnabled = !_readOnly;
-                }
+                _readOnly = value;
+                NotesList.IsEnabled = !_readOnly;
             }
         }
+    }
 
-        public KanbanNotes(string userid)
-        {
-            UserID = userid;
-            InitializeComponent();
-        }
+    public bool Visible => true;
 
-        public string AdditionalNote => NewNote.Text;
+    public KanbanNotes(string userid)
+    {
+        UserID = userid;
+        InitializeComponent();
+    }
 
-        public DynamicEditorGrid EditorGrid { get; set; }
+    public string AdditionalNote => NewNote.Text;
 
-        public PageType PageType => PageType.Other;
+    public DynamicEditorGrid EditorGrid { get; set; }
 
-        public bool Ready { get; set; }
+    public PageType PageType => PageType.Other;
 
-        public void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
-        {
-            NotesList.ItemsSource = ((Kanban)item).Notes;
-            Ready = true;
-        }
+    public bool Ready { get; set; }
 
-        public void Cancel()
-        {
-            NotesList.ItemsSource = ((Kanban)(EditorGrid as IDynamicEditorHost).GetItems().First()).Notes;
-        }
+    public void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
+    {
+        NotesList.ItemsSource = ((Kanban)item).Notes;
+        Ready = true;
+    }
 
-        public void BeforeSave(object item)
-        {
-            if (!string.IsNullOrWhiteSpace(AdditionalNote))
-            {
-                var kanban = (Kanban)item;
-                var notes = kanban.Notes.ToList();
-                notes.Add(string.Format("{0:yyyy-MM-dd HH:mm:ss} {1}: {2}", DateTime.Now, UserID, AdditionalNote));
-                kanban.Notes = notes.ToArray();
-            }
-        }
+    public void Cancel()
+    {
+        NotesList.ItemsSource = ((Kanban)(EditorGrid as IDynamicEditorHost).GetItems().First()).Notes;
+    }
 
-        public string Caption()
+    public void BeforeSave(object item)
+    {
+        if (!string.IsNullOrWhiteSpace(AdditionalNote))
         {
-            return "Notes";
+            var kanban = (Kanban)item;
+            var notes = kanban.Notes.ToList();
+            notes.Add(string.Format("{0:yyyy-MM-dd HH:mm:ss} {1}: {2}", DateTime.Now, UserID, AdditionalNote));
+            kanban.Notes = notes.ToArray();
         }
+    }
 
-        public int Order { get; set; } = int.MinValue;
+    public string Caption()
+    {
+        return "Notes";
+    }
 
-        public void AfterSave(object item)
-        {
-            // no need to do anything here
-        }
+    public int Order { get; set; } = int.MinValue;
 
-        public Size MinimumSize()
-        {
-            return new Size(400, 600);
-        }
-        
-        public event EventHandler? OnChanged;
-        public void DoChanged()
-        {
-            OnChanged?.Invoke(this, EventArgs.Empty);
-        }
+    public void AfterSave(object item)
+    {
+        // no need to do anything here
+    }
+
+    public Size MinimumSize()
+    {
+        return new Size(400, 600);
+    }
+    
+    public event EventHandler? OnChanged;
+    public void DoChanged()
+    {
+        OnChanged?.Invoke(this, EventArgs.Empty);
     }
 }

+ 148 - 147
prs.server/Forms/SMSProviderGrid.cs

@@ -12,207 +12,208 @@ using System.Threading.Tasks;
 using System.Windows;
 using System.Windows.Controls;
 
-namespace PRSServer.Forms
+namespace PRSServer.Forms;
+
+internal class SMSProviderGrid : DynamicGrid<SMSProviderProperties>, IDynamicEditorPage
 {
-    internal class SMSProviderGrid : DynamicGrid<SMSProviderProperties>, IDynamicEditorPage
-    {
-        public DynamicEditorGrid EditorGrid { get; set; }
+    public DynamicEditorGrid EditorGrid { get; set; }
 
-        public List<SMSProviderProperties> WorkingList { get; set; }
+    public List<SMSProviderProperties> WorkingList { get; set; }
 
-        public bool Ready { get; set; }
+    public bool Ready { get; set; }
 
-        private bool _readOnly;
-        public bool ReadOnly
+    private bool _readOnly;
+    public bool ReadOnly
+    {
+        get => _readOnly;
+        set
         {
-            get => _readOnly;
-            set
+            if (_readOnly != value)
             {
-                if (_readOnly != value)
-                {
-                    _readOnly = value;
-                    Reconfigure();
-                }
+                _readOnly = value;
+                Reconfigure();
             }
         }
+    }
+
+    public bool Visible => true;
 
-        public PageType PageType => PageType.Other;
+    public PageType PageType => PageType.Other;
 
-        protected override void Init()
+    protected override void Init()
+    {
+    }
+
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        if (ReadOnly)
         {
+            options.EditRows = true;
         }
-
-        protected override void DoReconfigure(DynamicGridOptions options)
+        else
         {
-            if (ReadOnly)
-            {
-                options.EditRows = true;
-            }
-            else
-            {
-                options.AddRows = true;
-                options.EditRows = true;
-                options.DeleteRows = true;
-            }
+            options.AddRows = true;
+            options.EditRows = true;
+            options.DeleteRows = true;
         }
+    }
 
-        protected override DynamicGridColumns LoadColumns()
-        {
-            var result = new DynamicGridColumns();
+    protected override DynamicGridColumns LoadColumns()
+    {
+        var result = new DynamicGridColumns();
 
-            result.Add<SMSProviderProperties>(x => x.ProviderType, 100, "Type", "", Alignment.MiddleCenter);
+        result.Add<SMSProviderProperties>(x => x.ProviderType, 100, "Type", "", Alignment.MiddleCenter);
 
-            return result;
-        }
+        return result;
+    }
 
-        public void AfterSave(object item)
-        {
-            var serverProperties = item as DatabaseServerProperties;
-            serverProperties.SMSProviderProperties = WorkingList.ToDictionary(x => x.ProviderType, x => x.Properties);
-        }
+    public void AfterSave(object item)
+    {
+        var serverProperties = item as DatabaseServerProperties;
+        serverProperties.SMSProviderProperties = WorkingList.ToDictionary(x => x.ProviderType, x => x.Properties);
+    }
 
-        public void BeforeSave(object item)
-        {
-        }
+    public void BeforeSave(object item)
+    {
+    }
 
-        public string Caption()
-        {
-            return "2FA Providers";
-        }
+    public string Caption()
+    {
+        return "2FA Providers";
+    }
 
-        protected override void DoAdd(bool OpenEditorOnDirectEdit = false)
+    protected override void DoAdd(bool OpenEditorOnDirectEdit = false)
+    {
+        var menu = new ContextMenu();
+        foreach(var item in Enum.GetValues(typeof(SMSProviderType)))
         {
-            var menu = new ContextMenu();
-            foreach(var item in Enum.GetValues(typeof(SMSProviderType)))
-            {
-                if(!WorkingList.Any(x => x.ProviderType == (SMSProviderType)item))
-                {
-                    var menuItem = new MenuItem();
-                    menuItem.Header = item.ToString();
-                    menuItem.Tag = item;
-                    menuItem.Click += MenuItem_Click;
-                    menu.Items.Add(menuItem);
-                }
-            }
-            if(menu.Items.Count == 0)
+            if(!WorkingList.Any(x => x.ProviderType == (SMSProviderType)item))
             {
-                var emptyItem = new MenuItem();
-                emptyItem.Header = "No more provider types";
-                emptyItem.IsEnabled = false;
-                menu.Items.Add(emptyItem);
+                var menuItem = new MenuItem();
+                menuItem.Header = item.ToString();
+                menuItem.Tag = item;
+                menuItem.Click += MenuItem_Click;
+                menu.Items.Add(menuItem);
             }
-            menu.IsOpen = true;
         }
-
-        private bool EditProperties(BaseSMSProviderProperties properties)
+        if(menu.Items.Count == 0)
         {
-            var editor = new DynamicEditorForm(properties.GetType());
-            editor.ReadOnly = ReadOnly;
-            editor.Items = new BaseObject[] { properties };
-            return editor.ShowDialog() == true;
+            var emptyItem = new MenuItem();
+            emptyItem.Header = "No more provider types";
+            emptyItem.IsEnabled = false;
+            menu.Items.Add(emptyItem);
         }
+        menu.IsOpen = true;
+    }
 
-        private void MenuItem_Click(object sender, RoutedEventArgs e)
-        {
-            var smsProviderType = (SMSProviderType)(sender as MenuItem)!.Tag;
-            BaseSMSProviderProperties? properties = SMSProviderProperties.ToProperties(smsProviderType, null);
+    private bool EditProperties(BaseSMSProviderProperties properties)
+    {
+        var editor = new DynamicEditorForm(properties.GetType());
+        editor.ReadOnly = ReadOnly;
+        editor.Items = new BaseObject[] { properties };
+        return editor.ShowDialog() == true;
+    }
+
+    private void MenuItem_Click(object sender, RoutedEventArgs e)
+    {
+        var smsProviderType = (SMSProviderType)(sender as MenuItem)!.Tag;
+        BaseSMSProviderProperties? properties = SMSProviderProperties.ToProperties(smsProviderType, null);
 
-            if(properties != null && EditProperties(properties))
+        if(properties != null && EditProperties(properties))
+        {
+            SaveItem(new SMSProviderProperties()
             {
-                SaveItem(new SMSProviderProperties()
-                {
-                    ProviderType = smsProviderType,
-                    Properties = Serialization.Serialize(properties)
-                });
-                Refresh(false, true);
-            }
+                ProviderType = smsProviderType,
+                Properties = Serialization.Serialize(properties)
+            });
+            Refresh(false, true);
         }
+    }
 
-        public void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
+    public void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
+    {
+        var data = PageDataHandler?.Invoke(typeof(SMSProviderProperties));
+        if (data != null)
+        {
+            WorkingList = data.Rows.Select(x => x.ToObject<SMSProviderProperties>()).ToList();
+        }
+        else
         {
-            var data = PageDataHandler?.Invoke(typeof(SMSProviderProperties));
-            if (data != null)
+            var serverProperties = item as DatabaseServerProperties;
+            if(serverProperties?.SMSProviderProperties != null)
             {
-                WorkingList = data.Rows.Select(x => x.ToObject<SMSProviderProperties>()).ToList();
+                WorkingList = serverProperties.SMSProviderProperties.Select(
+                    x => new SMSProviderProperties() { ProviderType = x.Key, Properties = x.Value }).ToList();
             }
             else
             {
-                var serverProperties = item as DatabaseServerProperties;
-                if(serverProperties?.SMSProviderProperties != null)
-                {
-                    WorkingList = serverProperties.SMSProviderProperties.Select(
-                        x => new SMSProviderProperties() { ProviderType = x.Key, Properties = x.Value }).ToList();
-                }
-                else
-                {
-                    WorkingList = new();
-                }
+                WorkingList = new();
             }
-
-            Refresh(true, true);
-            Ready = true;
         }
 
-        public void Cancel()
-        {
-            // Not doing anything here, even though we really should.
-        }
+        Refresh(true, true);
+        Ready = true;
+    }
 
-        public Size MinimumSize()
-        {
-            return new Size(400, 400);
-        }
+    public void Cancel()
+    {
+        // Not doing anything here, even though we really should.
+    }
 
-        public int Order { get; set; } = int.MinValue;
+    public Size MinimumSize()
+    {
+        return new Size(400, 400);
+    }
 
-        public override void DeleteItems(params CoreRow[] rows)
-        {
-            foreach(var row in rows)
-            {
-                WorkingList.RemoveAll(x => x.ProviderType.ToString() == row["ProviderType"].ToString());
-            }
-        }
+    public int Order { get; set; } = int.MinValue;
 
-        public override bool EditItems(SMSProviderProperties[] items, Func<Type, CoreTable> PageDataHandler = null, bool PreloadPages = false)
+    public override void DeleteItems(params CoreRow[] rows)
+    {
+        foreach(var row in rows)
         {
-            var item = items.FirstOrDefault();
-            if (item == null)
-                return false;
-
-            BaseSMSProviderProperties? properties = item.ToProperties();
+            WorkingList.RemoveAll(x => x.ProviderType.ToString() == row["ProviderType"].ToString());
+        }
+    }
 
-            if (properties != null && EditProperties(properties))
-            {
-                item.Properties = Serialization.Serialize(properties);
-                return true;
-            }
+    public override bool EditItems(SMSProviderProperties[] items, Func<Type, CoreTable> PageDataHandler = null, bool PreloadPages = false)
+    {
+        var item = items.FirstOrDefault();
+        if (item == null)
             return false;
-        }
 
-        public override SMSProviderProperties LoadItem(CoreRow row)
-        {
-            return WorkingList[row.Index];
-        }
+        BaseSMSProviderProperties? properties = item.ToProperties();
 
-        protected override void Reload(
-        	Filters<SMSProviderProperties> criteria, Columns<SMSProviderProperties> columns, ref SortOrder<SMSProviderProperties>? sort,
-        	CancellationToken token, Action<CoreTable?, Exception?> action)
+        if (properties != null && EditProperties(properties))
         {
-            var table = new CoreTable();
-            table.LoadColumns(typeof(SMSProviderProperties));
+            item.Properties = Serialization.Serialize(properties);
+            return true;
+        }
+        return false;
+    }
 
-            foreach(var item in WorkingList)
-            {
-                table.LoadRow(item);
-            }
+    public override SMSProviderProperties LoadItem(CoreRow row)
+    {
+        return WorkingList[row.Index];
+    }
 
-            action(table, null);
-        }
+    protected override void Reload(
+    	Filters<SMSProviderProperties> criteria, Columns<SMSProviderProperties> columns, ref SortOrder<SMSProviderProperties>? sort,
+    	CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        var table = new CoreTable();
+        table.LoadColumns(typeof(SMSProviderProperties));
 
-        public override void SaveItem(SMSProviderProperties item)
+        foreach(var item in WorkingList)
         {
-            if (!WorkingList.Contains(item))
-                WorkingList.Add(item);
+            table.LoadRow(item);
         }
+
+        action(table, null);
+    }
+
+    public override void SaveItem(SMSProviderProperties item)
+    {
+        if (!WorkingList.Contains(item))
+            WorkingList.Add(item);
     }
 }

+ 98 - 0
prs.stores/BillStore.cs

@@ -1,11 +1,44 @@
 using Comal.Classes;
+using InABox.Configuration;
 using InABox.Core;
+using InABox.Database.Stores;
+using InABox.Scripting;
 using System;
 
 namespace Comal.Stores;
 
 internal class BillStore : BaseStore<Bill>
 {
+    private static ScriptDocument? _billApprovalScriptDocument;
+    private static string? _billApprovalScript;
+    public static string? BillApprovalScript
+    {
+        set
+        {
+            _billApprovalScript = value;
+            if(!value.IsNullOrWhiteSpace())
+            {
+                _billApprovalScriptDocument = new ScriptDocument(value);
+                if (!_billApprovalScriptDocument.Compile())
+                {
+                    InABox.Core.Logger.Send(LogType.Error, "", $"Bill Approval Script failed to compile: {_billApprovalScriptDocument.Result}");
+                    _billApprovalScriptDocument = null;
+                }
+            }
+            else
+            {
+                _billApprovalScriptDocument = null;
+            }
+        }
+    }
+
+    public override void Init()
+    {
+        base.Init();
+
+        GlobalSettingsStore.RegisterSubStore<BillApprovalSettings, BillApprovalSettingsStore>();
+    }
+
     protected override void BeforeSave(Bill entity)
     {
         base.BeforeSave(entity);
@@ -22,9 +55,53 @@ internal class BillStore : BaseStore<Bill>
                 throw new DuplicateCodeException(typeof(Bill), new Dictionary<string, object> {{ nameof(Bill.Number), entity.Number }});
             }
         }
+        UpdateBillApprovals(entity);
         //UpdateAggregate<Supplier>(entity, entity.SupplierLink, Sum<Supplier>(b => b.Balance, s => s.Balance));
     }
 
+    private void UpdateBillApprovals(Bill bill)
+    {
+        if (bill.ID == Guid.Empty || _billApprovalScriptDocument is null) return;
+
+        var approvals = Provider.Query(
+            new Filter<BillApproval>(x => x.Bill.ID).IsEqualTo(bill.ID),
+            Columns.Required<BillApproval>())
+            .ToList<BillApproval>();
+        try
+        {
+            var oldList = approvals.ToList();
+            if(_billApprovalScriptDocument.Execute(methodname: BillApprovalSettings.UpdateBillApprovalsMethodName, parameters: [bill, approvals]))
+            {
+                var newApprovals = new List<BillApproval>();
+                var removedApprovals = new List<BillApproval>();
+                foreach(var approval in approvals)
+                {
+                    if (!oldList.Contains(approval))
+                    {
+                        newApprovals.Add(approval);
+                    }
+                }
+                foreach(var approval in oldList)
+                {
+                    if (!approvals.Contains(approval))
+                    {
+                        removedApprovals.Add(approval);
+                    }
+                }
+                foreach(var approval in newApprovals)
+                {
+                    approval.Bill.ID = bill.ID;
+                }
+                Provider.Save(newApprovals);
+                Provider.Delete(removedApprovals, UserID);
+            }
+        }
+        catch(Exception e)
+        {
+            CoreUtils.LogException(UserID, e, "Error running bill approval script");
+        }
+    }
+
     protected override void BeforeDelete(Bill entity)
     {
         base.BeforeDelete(entity);
@@ -32,4 +109,25 @@ internal class BillStore : BaseStore<Bill>
         entity.SupplierLink.ID = Guid.Empty;
         //UpdateAggregate<Supplier>(entity, entity.SupplierLink, Sum<Supplier>(b => b.Balance, s => s.Balance));
     }
+}
+
+internal class BillApprovalSettingsStore : ISettingsStoreEventHandler<BillApprovalSettings>
+{
+    public void BeforeSave(BillApprovalSettings entity)
+    {
+    }
+    public void AfterSave(BillApprovalSettings entity)
+    {
+        ConfigurationCache.Add(ConfigurationCacheType.Global, "", entity);
+        BillStore.BillApprovalScript = entity.ApprovalSetUpdateScript;
+    }
+
+    public void BeforeDelete(BillApprovalSettings entity)
+    {
+    }
+    public void AfterDelete(BillApprovalSettings entity)
+    {
+        ConfigurationCache.Clear<BillApprovalSettings>(ConfigurationCacheType.Global, "");
+        BillStore.BillApprovalScript = null;
+    }
 }