using Comal.Classes; using InABox.Configuration; using InABox.Core; using InABox.Database; using InABox.DynamicGrid; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace PRS.Shared.Database_Update_Scripts; internal class Update_8_58 : DatabaseUpdateScript { public override VersionNumber Version => new(8, 58); private static void UpdateTimeSheets(IProvider provider) { Logger.Send(LogType.Information, "", $"Migrating TimeSheet.Processed -> TimeSheet.Posted"); var timeSheets = provider.Query( Filter.Where(x => x.Processed).IsNotEqualTo(DateTime.MinValue) .And(x => x.Posted).IsEqualTo(DateTime.MinValue), Columns.None() .Add(x => x.ID) .Add(x => x.Processed) .Add(x => x.Posted)) .ToArray(); if(timeSheets.Length == 0) { Logger.Send(LogType.Information, "", $"Migrating TimeSheet.Processed -> TimeSheet.Posted: Done"); return; } Logger.Send(LogType.Information, "", $"Migrating TimeSheet.Processed -> TimeSheet.Posted: {timeSheets.Length} items"); Utils.Utils.ProcessInChunks( timeSheets, chunk => { foreach (var timeSheet in chunk) { timeSheet.Posted = timeSheet.Processed; } provider.Save(chunk); }, 200, percentage => Logger.Send(LogType.Information, "", $"Migrating TimeSheet.Processed: {percentage:F2}%")); } private static void ConvertLink(IProvider provider, string fromLink, string toLink) where T : Entity, new() { var fromColumn = new Column(fromLink).SubColumn(new Column(x => x.ID)); var toColumn = new Column(toLink).SubColumn(new Column(x => x.ID)); var items = provider.Query( Filter.Where(fromColumn).IsNotEqualTo(Guid.Empty) .And(toColumn).IsEqualTo(Guid.Empty), Columns.None() .Add(x => x.ID) .Add(toColumn) // The order here matters a lot; since I've made a lot of obsolete links just point directly to the new one, then we have to set the new before the old. .Add(fromColumn)) .ToArray(); if(items.Length == 0) { return; } Logger.Send(LogType.Information, "", $"Migrating {typeof(T).Name}.{fromColumn.Property} -> {typeof(T).Name}.{toColumn.Property}: {items.Length} items"); Utils.Utils.ProcessInChunks( items, chunk => { foreach (var item in chunk) { item.SetObserving(false); var originalValue = toColumn.PropertyDefinition.Getter()(item); toColumn.PropertyDefinition.Setter()( item, fromColumn.PropertyDefinition.Getter()(item)); item.SetOriginalValue(toColumn.Property, originalValue); item.SetObserving(true); } provider.Save(chunk); }, 1000, percentage => Logger.Send(LogType.Information, "", $"Migrating {typeof(T).Name}.{fromColumn.Property}: {percentage:F2}%")); } private static void ConvertLink(IProvider provider, Expression> fromLink, Expression> toLink) where T : Entity, new() where TFromLink : IEntityLink where TToLink : IEntityLink { ConvertLink(provider, CoreUtils.GetFullPropertyName(fromLink, "."), CoreUtils.GetFullPropertyName(toLink, ".")); } private static void ConvertLink(IProvider provider, Expression> fromLink, Expression> toLink) where T : Entity, new() where TLink : IEntityLink { ConvertLink(provider, fromLink, toLink); } private static string QuoteString(string str) { return $"\"{str.Replace("\\", "\\\\").Replace("\"", "\\\"")}\""; } private static void ConvertQAQuestions(IProvider provider) { var qaQuestions = provider.Query( Filter.Where(x => x.Converted).IsEqualTo(false), Columns.None() .Add(x => x.ID) .Add(x => x.Converted) .Add(x => x.Code) .Add(x => x.Question) .Add(x => x.Section) .Add(x => x.Description) .Add(x => x.Answer) .Add(x => x.Parameters) .Add(x => x.Sequence) .Add(x => x.Form.ID)) .ToObjects() .GroupBy(x => x.Form.ID) .Select(x => new { FormID = x.Key, Questions = x.OrderBy(x => x.Sequence).ToList() }) .ToList(); Logger.Send(LogType.Information, "", $"Converting {qaQuestions.Count} QA forms into DigitalForms"); var formSequences = provider.Query( Filter.Where(x => x.Form.ID) .InList(qaQuestions.ToArray(x => x.FormID)), Columns.None() .Add(x => x.Form.ID) .Add(x => x.Sequence)) .ToObjects() .GroupBy(x => x.Form.ID) .ToDictionary(x => x.Key, x => x.Max(x => x.Sequence)); var variableMappings = new Dictionary>(); var variables = new List(); var layouts = new List(); foreach(var item in qaQuestions) { if(formSequences.TryGetValue(item.FormID, out var sequence)) { sequence++; } else { sequence = 0; } var layout = new DFLayout(); layout.ColumnWidths.Add("Auto"); layout.ColumnWidths.Add("*"); var codes = new HashSet(); string GenerateCode(string code) { var originalCode = code; var i = 1; while (codes.Contains(code)) { code = $"{originalCode}{i}"; ++i; } return code; } var mappings = variableMappings.GetValueOrAdd(item.FormID); var nButtons = 0; var i = 1; foreach(var question in item.Questions) { layout.RowHeights.Add("Auto"); var row = layout.RowHeights.Count; if(question.Answer == QAAnswer.Comment) { var label = new DFLayoutLabel { Caption = question.Question, Row = row, Column = 1, ColumnSpan = 3 }; label.Style.HorizontalTextAlignment = DFLayoutAlignment.Middle; layout.Elements.Add(label); } else { var rowNum = new DFLayoutLabel { Caption = i.ToString(), Row = row, Column = 1 }; var label = new DFLayoutLabel { Caption = question.Question, Row = row, Column = 2 }; layout.Elements.Add(rowNum); layout.Elements.Add(label); var variable = new DigitalFormVariable(); variable.Form.CopyFrom(question.Form); variable.Sequence = sequence++; DFLayoutFieldProperties properties; Type fieldType; var code = GenerateCode(question.Code.NotWhiteSpaceOr(question.Answer.ToString())); var parameters = question.ParseParameters(); switch (question.Answer) { case QAAnswer.Choice: { // ColourExpression var buttons = parameters["Options"].Split(',').ToArray(x => x.Trim()); var colors = parameters["Colors"].Split(',').ToArray(x => x.Trim()); var defValue = parameters["Default"].Trim(); fieldType = typeof(DFLayoutOptionField); var optionProperties = new DFLayoutOptionFieldProperties(); properties = optionProperties; optionProperties.Default = defValue; optionProperties.OptionType = DFLayoutOptionType.Buttons; optionProperties.Options = DFLayoutOptionFieldProperties.WriteOptions(buttons); var colourExpression = "null"; foreach(var (option, colour) in buttons.Zip(colors)) { colourExpression = $"If([{code}] == {QuoteString(option)}, {QuoteString(colour)}, {colourExpression})"; } optionProperties.ColourExpression = colourExpression; nButtons = Math.Max(nButtons, buttons.Length); } break; case QAAnswer.Number: { var defValue = parameters["Default"]; fieldType = typeof(DFLayoutDoubleField); var doubleProperties = new DFLayoutDoubleFieldProperties(); properties = doubleProperties; doubleProperties.Default = double.TryParse(defValue, out var d) ? d : default; } break; case QAAnswer.Text: { var defValue = parameters["Default"]; fieldType = typeof(DFLayoutStringField); var stringProperties = new DFLayoutStringFieldProperties(); properties = stringProperties; stringProperties.Default = defValue; } break; case QAAnswer.Combo: { var buttons = parameters["Options"].Split(','); var defValue = parameters["Default"]; fieldType = typeof(DFLayoutOptionField); var optionProperties = new DFLayoutOptionFieldProperties(); properties = optionProperties; optionProperties.Default = defValue; optionProperties.OptionType = DFLayoutOptionType.Combo; optionProperties.Options = DFLayoutOptionFieldProperties.WriteOptions(buttons); } break; default: throw new Exception("Impossible"); } properties.Code = code; properties.Description = question.Description.NotWhiteSpaceOr(question.Question); properties.Required = parameters.GetValueOrDefault("Default").IsNullOrWhiteSpace(); variable.SaveProperties(fieldType, properties); mappings.Add(question.ID, variable.Code); codes.Add(variable.Code); var field = (Activator.CreateInstance(variable.FieldType()) as DFLayoutField)!; field.Name = variable.Code; field.Row = row; field.Column = 3; layout.Elements.Add(field); variables.Add(variable); ++i; } question.Converted = true; } layout.ColumnWidths.Add(Math.Max(150, nButtons * 80).ToString()); var dfLayout = new DigitalFormLayout(); dfLayout.Form.ID = item.FormID; dfLayout.Layout = layout.SaveLayout(); dfLayout.Description = "Generated from QA form"; dfLayout.Type = DFLayoutType.Desktop; dfLayout.Active = true; layouts.Add(dfLayout); } provider.Save(variables); provider.Save(layouts); provider.Save(qaQuestions.SelectMany(x => x.Questions)); FormUpdater.UpdateAllForms( (form, variables) => false, (formType, instance, form, variables) => { if (!variableMappings.TryGetValue(form.ID, out var mappings)) return false; var values = DigitalForm.DeserializeFormSaveData(instance) ?? new(); var items = values.ToLoadStorage().Items().ToArray(); foreach(var (key, value) in items) { if (!Guid.TryParse(key, out var id)) continue; if (!mappings.TryGetValue(id, out var code)) continue; values.AddValue(code, value?.ToString()?.Trim()); } DigitalForm.SerializeFormData(instance, values); return true; }, filter: Filter.Where(x => x.ID).InList(qaQuestions.ToArray(x => x.FormID))); } private static void RenameTable(IProvider provider, Dictionary>, Expression>>? extraMaps = null) where TFrom : Entity, new() where TTo : Entity, new() { var currentMaps = new HashSet(); var maps = new List<(IProperty from, IProperty to)>(); foreach(var (from, to) in extraMaps ?? []) { var fromProperty = DatabaseSchema.PropertyStrict(from); var toProperty = DatabaseSchema.PropertyStrict(to); currentMaps.Add(fromProperty.Name); maps.Add((fromProperty, toProperty)); } foreach(var fromProperty in DatabaseSchema.LocalProperties(typeof(TFrom))) { if (currentMaps.Contains(fromProperty.Name)) continue; if(DatabaseSchema.Property(typeof(TTo), fromProperty.Name) is IProperty toProperty) { if(fromProperty.PropertyType != toProperty.PropertyType) { throw new Exception($"Cannot migrate {typeof(TFrom).Name}.{fromProperty.Name} -> {typeof(TTo).Name}.{toProperty.Name}: type mismatch"); } maps.Add((fromProperty, toProperty)); } else { } } var items = provider.Query( Filter.Where(x => x.ID).NotInQuery(Filter.All(), x => x.ID), Columns.None() .Add(x => x.ID) .Add(maps.Select(x => new Column(x.from)))) .ToArray(); if (items.Length == 0) return; Logger.Send(LogType.Information, "", $"Migrating {typeof(TFrom).Name} -> {typeof(TTo).Name}: {items.Length} items"); Utils.Utils.ProcessInChunks( items, chunk => { var newItems = new List(); foreach (var item in chunk) { var newItem = new TTo(); newItem.SetObserving(false); newItem.ID = item.ID; foreach(var (from, to) in maps) { to.Setter()(newItem, from.Getter()(item)); } newItem.SetObserving(true); newItems.Add(newItem); } provider.Save(newItems); }, 1000, percentage => Logger.Send(LogType.Information, "", $"Migrating {typeof(TFrom).Name}: {percentage:F2}%")); } private static void ConvertLinks(IProvider provider) { ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.QAForm, x => x.DigitalForm); ConvertLink(provider, x => x.QAForm, x => x.DigitalForm); var method = typeof(Update_8_58).GetMethods(BindingFlags.Static | BindingFlags.NonPublic) .Where(x => x.Name == nameof(ConvertLink) && x.IsGenericMethod && x.GetGenericArguments().Length == 1) .First(); foreach(var entity in DbFactory.ProviderFactory.Types.Where(x => x.HasInterface(typeof(IEntityDocument)))) { var entityMethod = method.MakeGenericMethod(entity); var entityFromProp = DatabaseSchema.PropertyStrict(entity, x => x.EntityLink); var entityToProp = DatabaseSchema.PropertyStrict(entity, x => x.Entity); entityMethod.Invoke(null, [provider, entityFromProp.Name, entityToProp.Name]); var documentFromProp = DatabaseSchema.PropertyStrict(entity, x => x.DocumentLink); var documentToProp = DatabaseSchema.PropertyStrict(entity, x => x.Document); entityMethod.Invoke(null, [provider, documentFromProp.Name, documentToProp.Name]); } foreach(var entity in DbFactory.ProviderFactory.Types) { MethodInfo? entityMethod = null; foreach(var property in DatabaseSchema.LocalProperties(entity)) { if (property.Parent is null || property.Parent.PropertyType != typeof(LocalityLink) || property.Parent.Parent is null || property.Parent.Parent.PropertyType != typeof(Address)) continue; var addressProp = property.Parent.Parent; entityMethod ??= method.MakeGenericMethod(entity); var fromProp = DatabaseSchema.PropertyStrict(entity, addressProp.Name + "." + nameof(Address.LocalityLink)); var toProp = DatabaseSchema.PropertyStrict(entity, addressProp.Name + "." + nameof(Address.Locality)); entityMethod.Invoke(null, [provider, fromProp.Name, toProp.Name]); } } ConvertLink(provider, x => x.EmployeeLink, x => x.Employee); ConvertLink(provider, x => x.ManagerLink, x => x.Manager); ConvertLink(provider, x => x.EmployeeLink, x => x.Employee); ConvertLink(provider, x => x.ActivityLink, x => x.Activity); ConvertLink(provider, x => x.LeaveRequestLink, x => x.LeaveRequest); ConvertLink(provider, x => x.SupplierLink, x => x.Supplier); ConvertLink(provider, x => x.BillLink, x => x.Bill); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.ManufacturingPacketLink, x => x.ManufacturingPacket); ConvertLink(provider, x => x.RequisitionLink, x => x.PickingList); ConvertLink(provider, x => x.SetoutLink, x => x.Setout); ConvertLink(provider, x => x.ShipmentLink, x => x.Shipment); ConvertLink(provider, x => x.UserLink, x => x.User); ConvertLink(provider, x => x.OvertimeRuleLink, x => x.OvertimeRule); ConvertLink(provider, x => x.EmployeeLink, x => x.Employee); ConvertLink(provider, x => x.RoleLink, x => x.Role); ConvertLink(provider, x => x.GroupLink, x => x.Group); ConvertLink(provider, x => x.TrackerLink, x => x.Tracker); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.CustomerLink, x => x.Customer); ConvertLink(provider, x => x.InvoiceLink, x => x.Invoice); ConvertLink(provider, x => x.ScheduleLink, x => x.Schedule); ConvertLink(provider, x => x.ActivityLink, x => x.Activity); ConvertLink(provider, x => x.ActivityLink, x => x.Activity); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.EmployeeLink, x => x.Employee); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.EquipmentLink, x => x.Equipment); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.TrackerLink, x => x.Tracker); ConvertLink(provider, x => x.EmployeeLink, x => x.Employee); ConvertLink(provider, x => x.EmployeeLink, x => x.Employee); ConvertLink(provider, x => x.ManagerLink, x => x.Manager); ConvertLink(provider, x => x.ScheduleLink, x => x.Schedule); ConvertLink(provider, x => x.EmployeeLink, x => x.Employee); ConvertLink(provider, x => x.SetoutLink, x => x.Setout); ConvertLink(provider, x => x.ManufacturingTemplateLink, x => x.ManufacturingTemplate); ConvertLink(provider, x => x.StageLink, x => x.Stage); ConvertLink(provider, x => x.ManufacturingSectionLink, x => x.ManufacturingSection); ConvertLink(provider, x => x.Requisition, x => x.PickingList); ConvertLink(provider, x => x.BillLink, x => x.Bill); ConvertLink(provider, x => x.PaymentLink, x => x.Payment); ConvertLink(provider, x => x.SupplierLink, x => x.Supplier); ConvertLink(provider, x => x.PaymentTypeLink, x => x.PaymentType); ConvertLink(provider, x => x.SupplierLink, x => x.Supplier); ConvertLink(provider, x => x.PurchaseOrderLink, x => x.PurchaseOrder); ConvertLink(provider, x => x.InvoiceLink, x => x.Invoice); ConvertLink(provider, x => x.ReceiptLink, x => x.Receipt); ConvertLink(provider, x => x.CustomerLink, x => x.Customer); ConvertLink(provider, x => x.ReceiptTypeLink, x => x.ReceiptType); ConvertLink(provider, x => x.RequisitionLink, x => x.Requisition); ConvertLink(provider, x => x.TrackerLink, x => x.Tracker); ConvertLink(provider, x => x.SupplierLink, x => x.Supplier); ConvertLink(provider, x => x.Requisition, x => x.PickingList); ConvertLink(provider, x => x.EmployeeLink, x => x.Employee); ConvertLink(provider, x => x.TeamLink, x => x.Team); ConvertLink(provider, x => x.EmployeeLink, x => x.Employee); ConvertLink(provider, x => x.JobLink, x => x.Job); ConvertLink(provider, x => x.ActivityLink, x => x.Activity); ConvertLink(provider, x => x.LeaveRequestLink, x => x.LeaveRequest); ConvertLink(provider, x => x.StandardLeaveLink, x => x.StandardLeave); RenameTable(provider); RenameTable(provider, new() { { x => x.Requisition.ID, x => x.PickingList.ID } }); RenameTable(provider); RenameTable(provider); RenameTable(provider); } private static void UpdateColumns(IProvider provider) where T : Entity, IDatabaseStoredSettings, new() { var settings = provider.Query( Filter.Where(x => x.Section).IsEqualTo(nameof(DynamicGridColumns)), Columns.None() .Add(x => x.ID) .Add(x => x.Key) .Add(x => x.Contents)) .ToArray(); var changedSettings = new List(); foreach(var setting in settings) { if (setting.Key.IsNullOrWhiteSpace()) continue; var entityName = setting.Key.Split('.')[^1]; var entity = CoreUtils.Entities.Where(x => x.Name == entityName && x.IsSubclassOf(typeof(Entity))) .FirstOrDefault(); if (entity is null) continue; var columns = Serialization.Deserialize(setting.Contents); if(columns is not null) { var changed = false; foreach(var column in columns) { var i = 0; while(i < column.ColumnName.Length) { var index = column.ColumnName.IndexOf("Link.", i); if (index == -1) break; var columnName = column.ColumnName[0..index]; if(DatabaseSchema.Property(entity, columnName) is IProperty property) { column.ColumnName = $"{property.Name}.{column.ColumnName[(index + 5)..]}"; changed = true; } else { i = index + 5; } } } if (changed) { setting.Contents = Serialization.Serialize(columns); changedSettings.Add(setting); } } } provider.Save(changedSettings); } private static void UpdateColumns(IProvider provider) { UpdateColumns(provider); UpdateColumns(provider); } private static void UpdateSecurityToken(IProvider provider, string oldDescriptor, string newDescriptor) where T : Entity, ISecurityToken, new() { var tokens = provider.Query( Filter.Where(x => x.Descriptor).IsEqualTo(oldDescriptor), Columns.None() .Add(x => x.ID) .Add(x => x.Descriptor)) .ToArray(); foreach(var token in tokens) { token.Descriptor = newDescriptor; } provider.Save(tokens); } private static void UpdateSecurityToken(IProvider provider, string oldDescriptor, string newDescriptor) { UpdateSecurityToken(provider, oldDescriptor, newDescriptor); UpdateSecurityToken(provider, oldDescriptor, newDescriptor); UpdateSecurityToken(provider, oldDescriptor, newDescriptor); } private static void UpdateAutoSecurityToken(IProvider provider, Type token) { var toDescriptor = (Activator.CreateInstance(token.MakeGenericType(typeof(TTo))) as IAutoSecurityDescriptor)!; var overriden = Security.SecurityDescriptorOverride(toDescriptor); if (overriden != toDescriptor) return; // No point in updating these if the token has been overriden. var fromDescriptor = (Activator.CreateInstance(token.MakeGenericType(typeof(TFrom))) as IAutoSecurityDescriptor)!; UpdateSecurityToken(provider, Security.SecurityDescriptorOverride(fromDescriptor).Code, toDescriptor.Code); } private static void UpdateAutoSecurityTokens(IProvider provider) { var list = CoreUtils.Entities.Where( x => x.HasInterface(typeof(IAutoSecurityDescriptor))) .ToArray(); foreach(var T in CoreUtils.Entities.Where( x => x.HasInterface(typeof(IAutoSecurityDescriptor)) && x.IsGenericType && x.GetGenericArguments().Length == 1)) { UpdateAutoSecurityToken(provider, T); } } private static void UpdateSecurityTokens(IProvider provider) { UpdateSecurityToken(provider, "CanViewRequisitionsDock", nameof(CanViewPickingListDock)); UpdateSecurityToken(provider, "CanSkipRequisitionPhotos", nameof(CanSkipPickingListPhotos)); UpdateSecurityToken(provider, "CanUpdateRequisitionStockMovements", nameof(CanUpdatePickingListStockMovements)); UpdateSecurityToken(provider, "CanArchiveRequisitions", nameof(CanArchivePickingLists)); UpdateAutoSecurityTokens(provider); UpdateAutoSecurityTokens(provider); UpdateAutoSecurityTokens(provider); UpdateAutoSecurityTokens(provider); UpdateAutoSecurityTokens(provider); } private static void ConvertSettings(IProvider provider, string fromSection, string toSection) where T : Entity, IDatabaseStoredSettings, new() { var settings = provider.Query( Filter.Where(x => x.Section).IsEqualTo(fromSection), Columns.None() .Add(x => x.ID) .Add(x => x.Section)) .ToArray(); foreach(var setting in settings) { setting.Section = toSection; } provider.Save(settings); } public override bool Update() { var provider = DbFactory.NewProvider(Logger.Main); UpdateTimeSheets(provider); ConvertLinks(provider); UpdateColumns(provider); UpdateSecurityTokens(provider); ConvertSettings(provider, "RequisitionSettings", "PickingListSettings"); ConvertQAQuestions(provider); return true; } }