1040 lines
37 KiB
C#
1040 lines
37 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data.Entity;
|
|
using System.Drawing;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Web.Mvc;
|
|
using EnVisage.Code.DAL;
|
|
using EnVisage.Code.DAL.Mongo;
|
|
using EnVisage.Models;
|
|
using MongoDB.Bson;
|
|
using MongoDB.Driver;
|
|
using NLog;
|
|
|
|
namespace EnVisage.Code.BLL
|
|
{
|
|
public class MongoMixManager : IMixManager<Mix>
|
|
{
|
|
protected Logger Logger = LogManager.GetCurrentClassLogger();
|
|
private readonly string _userId;
|
|
private readonly EnVisageEntities _dbContext;
|
|
protected EnVisageEntities DbContext => _dbContext;
|
|
// private bool _isContexLocal;
|
|
|
|
public MongoMixManager(EnVisageEntities dbContext, string userId)
|
|
{
|
|
if (dbContext == null)
|
|
{
|
|
_dbContext = new EnVisageEntities();
|
|
// _isContexLocal = true;
|
|
}
|
|
else
|
|
{
|
|
_dbContext = dbContext;
|
|
}
|
|
_userId = userId;
|
|
}
|
|
|
|
protected Mix InitInstance()
|
|
{
|
|
return new Mix();
|
|
}
|
|
|
|
protected Mix RetrieveReadOnlyById(string key)
|
|
{
|
|
return MongoDataContext.Mixes.Find(t => t.Key == new ObjectId(key)).FirstOrDefaultAsync().GetAwaiter().GetResult();
|
|
}
|
|
|
|
public void Delete(string mixKey)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(mixKey))
|
|
{
|
|
var objectId = ObjectId.Empty;
|
|
if (ObjectId.TryParse(mixKey, out objectId) && !ObjectId.Empty.Equals(objectId))
|
|
{
|
|
Delete(objectId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes Mix ffrom database by its Id (Mix.Id field used for identification)
|
|
/// </summary>
|
|
/// <param name="mixId">Mix.Id field value in the database</param>
|
|
protected void DeleteByMixId(ObjectId mixId)
|
|
{
|
|
if (!ObjectId.Empty.Equals(mixId))
|
|
{
|
|
var result = MongoDataContext.Mixes.DeleteOneAsync(t => t.Id == mixId).GetAwaiter().GetResult();
|
|
if (!result.IsAcknowledged)
|
|
Logger.Warn("An error occurred while removing Mix record");
|
|
}
|
|
|
|
var allocResult = MongoDataContext.ExpenditureAllocations.DeleteManyAsync(t => t.MixId == mixId).GetAwaiter().GetResult();
|
|
if (!allocResult.IsAcknowledged)
|
|
Logger.Warn("An error occurred while removing Mix expenditure allocations");
|
|
|
|
allocResult = MongoDataContext.TeamAllocations.DeleteManyAsync(t => t.MixId == mixId).GetAwaiter().GetResult();
|
|
if (!allocResult.IsAcknowledged)
|
|
Logger.Warn("An error occurred while removing Mix team allocations");
|
|
|
|
allocResult = MongoDataContext.ResourceAllocations.DeleteManyAsync(t => t.MixId == mixId).GetAwaiter().GetResult();
|
|
if (!allocResult.IsAcknowledged)
|
|
Logger.Warn("An error occurred while removing Mix resource allocations");
|
|
}
|
|
|
|
public string GetMixIdByKey(string mixKey, bool exceptionOnNotFound)
|
|
{
|
|
string mixId = String.Empty;
|
|
|
|
if (!String.IsNullOrWhiteSpace(mixKey))
|
|
try
|
|
{
|
|
var mix = RetrieveReadOnlyById(mixKey);
|
|
|
|
if ((mix == null) && exceptionOnNotFound)
|
|
{
|
|
string message = $"Mix not found (Key = {mixKey})";
|
|
Logger.Warn(message);
|
|
throw new Exception(message);
|
|
}
|
|
|
|
if (mix != null)
|
|
mixId = mix.Id.ToString();
|
|
}
|
|
catch
|
|
{
|
|
string message = $"An error occurred while trying to find Mix record: Mix not found (Key = {mixKey})";
|
|
Logger.Warn(message);
|
|
throw new Exception(message);
|
|
}
|
|
|
|
return mixId;
|
|
}
|
|
|
|
public void Delete(ObjectId mixKey)
|
|
{
|
|
string mixId = GetMixIdByKey(mixKey.ToString(), true);
|
|
DeleteByMixId(new ObjectId(mixId));
|
|
}
|
|
|
|
private Mix Save(Mix dbObj, string oldKey)
|
|
{
|
|
dbObj.CreatedAtUtc = DateTime.UtcNow;
|
|
dbObj.CreatedBy = _userId;
|
|
MongoDataContext.Mixes.InsertOneAsync(dbObj).GetAwaiter().GetResult();
|
|
ObjectId oKey;
|
|
dbObj.Key = ObjectId.TryParse(oldKey, out oKey) ? oKey : dbObj.Id;
|
|
var filter = Builders<Mix>.Filter.Eq(t => t.Id, dbObj.Id);
|
|
var update = Builders<Mix>.Update.Set(t => t.Key, dbObj.Key);
|
|
MongoDataContext.Mixes.UpdateOneAsync(filter, update).GetAwaiter().GetResult();
|
|
return dbObj;
|
|
}
|
|
|
|
public Mix SaveWithChildren(MixSaveModel model)
|
|
{
|
|
string previousMixId = String.Empty;
|
|
|
|
if (!String.IsNullOrEmpty(model.Id))
|
|
{
|
|
string mixKey = model.Id;
|
|
previousMixId = GetMixIdByKey(mixKey, false);
|
|
}
|
|
//_logger.Debug("SaveWithChildren method started at " + DateTime.Now);
|
|
|
|
#region step 0. Load old mix object
|
|
|
|
var savedMix = RetrieveWithChildren(model.Id);
|
|
|
|
#endregion
|
|
//_logger.Debug("SaveWithChildren method loaded old mix at " + DateTime.Now);
|
|
|
|
#region step 1. Save Mix object
|
|
|
|
Mix dbObj = InitInstance();
|
|
model.CopyTo(dbObj);
|
|
dbObj = Save(dbObj, model.Id);
|
|
|
|
#endregion
|
|
//_logger.Debug("SaveWithChildren method saved new mix at " + DateTime.Now);
|
|
|
|
#region step 2. Save Allocations
|
|
|
|
try
|
|
{
|
|
var expAllocations2Insert = new List<MixExpenditureAllocation>();
|
|
var teamAllocations2Insert = new List<MixTeamAllocation>();
|
|
var resAllocations2Insert = new List<MixResourceAllocation>();
|
|
|
|
// get all scenarios in mix
|
|
var scenariosInMix = model.Calendar.Projects.Where(x => x.Value?.Scenario != null)
|
|
.Select(x => x.Value.Scenario.Id).ToList();
|
|
|
|
// check which scenarios from mix still exist in the live database
|
|
var existingScenarios = DbContext.Scenarios.Where(x => scenariosInMix.Contains(x.Id)).Select(x => x.Id).ToList();
|
|
var scenarioIds2Load = new List<Guid>();
|
|
foreach (var projectId in model.Calendar.Projects.Keys)
|
|
{
|
|
var project = model.Calendar.Projects[projectId];
|
|
|
|
if (project.Scenario == null)
|
|
continue;
|
|
|
|
var currentScenario = project.Scenario;
|
|
|
|
#region Determining expenditures for saving
|
|
|
|
var expenditures = currentScenario.Expenditures;
|
|
|
|
// expenditures can be empty if user didn't edit this project befor saving this mix in current moment
|
|
if (expenditures == null || expenditures.Count < 1)
|
|
{
|
|
// if mix has been saved earlier and it contains expenditures we should save them for the current mix
|
|
if (savedMix?.Calendar?.Projects != null)
|
|
{
|
|
if (savedMix.Calendar.Projects.ContainsKey(projectId))
|
|
{
|
|
var savedProject = savedMix.Calendar.Projects[projectId];
|
|
if (savedProject.Scenario != null)
|
|
expenditures = savedProject.Scenario.Expenditures;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (expenditures == null || expenditures.Count < 1)
|
|
{
|
|
// TODO: review for ability to change GetFullAllocationInfoByScenario method for retrieving information for a couple of scenarios
|
|
if (existingScenarios.Contains(currentScenario.Id))
|
|
scenarioIds2Load.Add(currentScenario.Id);
|
|
//expenditures = (new ScenarioManager(DbContext)).GetFullAllocationInfoByScenario(currentScenario.Id, _userId);
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
var scenariosData = scenarioIds2Load.Any() ? (new ScenarioManager(DbContext)).GetFullAllocationInfoByScenario(scenarioIds2Load, _userId) : null;
|
|
|
|
foreach (var projectId in model.Calendar.Projects.Keys)
|
|
{
|
|
var project = model.Calendar.Projects[projectId];
|
|
if (project.Scenario == null)
|
|
continue;
|
|
var currentScenario = project.Scenario;
|
|
|
|
#region Determining expenditures for saving
|
|
var expenditures = currentScenario.Expenditures;
|
|
|
|
// expenditures can be empty if user didn't edit this project befor saving this mix in current moment
|
|
if (expenditures == null || expenditures.Count <= 0)
|
|
{
|
|
// if mix has been saved earlier and it contains expenditures we should to save them for the current mix
|
|
if (savedMix?.Calendar?.Projects != null)
|
|
{
|
|
if (savedMix.Calendar.Projects.ContainsKey(projectId))
|
|
{
|
|
var savedProject = savedMix.Calendar.Projects[projectId];
|
|
if (savedProject.Scenario != null)
|
|
expenditures = savedProject.Scenario.Expenditures;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (expenditures == null || expenditures.Count < 1)
|
|
{
|
|
// TODO: review for ability to change GetFullAllocationInfoByScenario method for retrieving information for a couple of scenarios
|
|
if (existingScenarios.Contains(currentScenario.Id))
|
|
expenditures = (scenariosData != null) && scenariosData.ContainsKey(currentScenario.Id) ? scenariosData[currentScenario.Id] : null;
|
|
}
|
|
#endregion
|
|
|
|
if (expenditures == null || expenditures.Count < 1)
|
|
continue;
|
|
|
|
foreach (var expCat in expenditures)
|
|
{
|
|
// insert expenditure allocations
|
|
var expenditureAllocation = new MixExpenditureAllocation
|
|
{
|
|
MixId = dbObj.Id,
|
|
ScenarioId = currentScenario.Id,
|
|
CreatedAtUtc = DateTime.UtcNow,
|
|
CreatedBy = _userId
|
|
};
|
|
expCat.Value.CopyTo(expenditureAllocation);
|
|
expAllocations2Insert.Add(expenditureAllocation);
|
|
|
|
// insert team allocations
|
|
foreach (var expenditureDetailsTeam in expCat.Value.Teams)
|
|
{
|
|
var teamAllocation = new MixTeamAllocation
|
|
{
|
|
MixId = dbObj.Id,
|
|
ScenarioId = currentScenario.Id,
|
|
ExpCatId = expenditureAllocation.ExpCatId,
|
|
CreatedAtUtc = DateTime.UtcNow,
|
|
CreatedBy = _userId
|
|
};
|
|
expenditureDetailsTeam.Value.CopyTo(teamAllocation);
|
|
teamAllocations2Insert.Add(teamAllocation);
|
|
|
|
foreach (var teamResource in expenditureDetailsTeam.Value.Resources)
|
|
{
|
|
var resAllocation = new MixResourceAllocation
|
|
{
|
|
MixId = dbObj.Id,
|
|
ScenarioId = currentScenario.Id,
|
|
ExpCatId = expenditureAllocation.ExpCatId,
|
|
TeamId = teamAllocation.TeamId,
|
|
CreatedAtUtc = DateTime.UtcNow,
|
|
CreatedBy = _userId
|
|
};
|
|
teamResource.Value.CopyTo(resAllocation);
|
|
resAllocations2Insert.Add(resAllocation);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SaveAllocations(expAllocations2Insert, teamAllocations2Insert, resAllocations2Insert);
|
|
|
|
// serialize all saved data to a file to compare them with other results
|
|
//var path = System.Web.HttpContext.Current.Server.MapPath("/output_new.txt");
|
|
//using (var writer = System.IO.File.CreateText(path))
|
|
//{
|
|
// writer.WriteLine("Mix object:");
|
|
// dbObj.DebugObjectProperties(writer);
|
|
// writer.WriteLine("Allocations:");
|
|
// expAllocations2Insert.DebugObjectProperties(writer);
|
|
// teamAllocations2Insert.DebugObjectProperties(writer);
|
|
// resAllocations2Insert.DebugObjectProperties(writer);
|
|
//}
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Logger.Fatal(exception);
|
|
// if any error occurred while saving new Mix then remove it and all it's children.
|
|
DeleteByMixId(dbObj.Id);
|
|
// Then throw exception to ask user to try again later
|
|
throw;
|
|
}
|
|
#endregion
|
|
//_logger.Debug("SaveWithChildren method saved mix allocations at " + DateTime.Now);
|
|
|
|
#region step 3. Delete previous Mix with children
|
|
|
|
try
|
|
{
|
|
if (!ObjectId.Empty.Equals(previousMixId) && !string.IsNullOrWhiteSpace(previousMixId))
|
|
// try to delete previous Mix record
|
|
DeleteByMixId(new ObjectId(previousMixId));
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
// if any error occurred while removing old Mix then do nothing except error logging
|
|
Logger.Fatal(exception);
|
|
}
|
|
|
|
#endregion
|
|
//_logger.Debug("SaveWithChildren method deleted old mix at " + DateTime.Now);
|
|
|
|
return dbObj;
|
|
}
|
|
|
|
private void SaveAllocations(List<MixExpenditureAllocation> expenditures, List<MixTeamAllocation> teamAllocations, List<MixResourceAllocation> resourceAllocations)
|
|
{
|
|
var tasks = new List<Task>();
|
|
|
|
if (expenditures != null && expenditures.Count > 0)
|
|
tasks.Add(MongoDataContext.ExpenditureAllocations.InsertManyAsync(expenditures));
|
|
if (teamAllocations != null && teamAllocations.Count > 0)
|
|
tasks.Add(MongoDataContext.TeamAllocations.InsertManyAsync(teamAllocations));
|
|
if (resourceAllocations != null && resourceAllocations.Count > 0)
|
|
tasks.Add(MongoDataContext.ResourceAllocations.InsertManyAsync(resourceAllocations));
|
|
|
|
Task.WaitAll(tasks.ToArray());
|
|
}
|
|
|
|
public Mix Retrieve(string mixKey)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(mixKey))
|
|
return null;
|
|
|
|
return MongoDataContext.Mixes.Find(t => t.Key == new ObjectId(mixKey)).FirstOrDefaultAsync().GetAwaiter().GetResult();
|
|
}
|
|
|
|
public MixSaveModel RetrieveModel(string mixKey)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(mixKey))
|
|
return null;
|
|
|
|
var mix = Retrieve(mixKey);
|
|
|
|
if (null == mix)
|
|
return null;
|
|
|
|
MixSaveModel model = new MixSaveModel
|
|
{
|
|
Id = mix.Key.ToString(),
|
|
Name = mix.Name,
|
|
StartDate = mix.StartDate,
|
|
EndDate = mix.EndDate
|
|
};
|
|
|
|
model.Filter.Selection.StartDate = mix.StartDate;
|
|
model.Filter.Selection.EndDate = mix.EndDate;
|
|
model.Filter.Selection.TeamsViews = mix.TeamsViews.Select(x => new MixTeamViewModel
|
|
{
|
|
Id = x.Id,
|
|
TVName = x.Name,
|
|
Group = new SelectListGroup { Disabled = false, Name = x.Group },
|
|
IsNew = x.IsNew,
|
|
CapacityTeamId = x.CapacityTeamId,
|
|
CompanyId = x.CompanyId,
|
|
CopyPlanned = x.CopyPlanned,
|
|
CostCenterId = x.CostCenterId,
|
|
Data = x.Data,
|
|
UserId = x.UserId
|
|
}).ToList();
|
|
|
|
model.Users = mix.Users;
|
|
model.Calendar.AssignFrom(mix);
|
|
|
|
return model;
|
|
}
|
|
|
|
public MixSaveModel RetrieveWithChildren(string mixKey)
|
|
{
|
|
var model = RetrieveModel(mixKey);
|
|
if (model == null)
|
|
return null;
|
|
|
|
#region Load Allocations
|
|
|
|
var mixId = GetMixIdByKey(mixKey, true);
|
|
var scenarios = model.Calendar.Projects.Where(x => x.Value?.Scenario != null)
|
|
.Select(x => x.Value.Scenario.Id)
|
|
.ToList();
|
|
|
|
var allocations = GetAllocations4Scenarios(mixId, scenarios);
|
|
|
|
if (allocations != null && allocations.Count > 0)
|
|
{
|
|
foreach (var projectId in model.Calendar.Projects.Keys)
|
|
{
|
|
var project = model.Calendar.Projects[projectId];
|
|
if (project.Scenario == null || !allocations.ContainsKey(project.Scenario.Id))
|
|
continue;
|
|
|
|
project.Scenario.Expenditures = allocations[project.Scenario.Id];
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
return model;
|
|
}
|
|
|
|
public List<MixProjectModel> GetProjectModel(List<Guid> projects)
|
|
{
|
|
List<MixProjectModel> result;
|
|
using (var scenarioManager = new ScenarioManager(DbContext))
|
|
{
|
|
result = new List<MixProjectModel>();
|
|
var availableProjects = DbContext.Projects.Where(x => projects.Contains(x.Id))
|
|
.Include(x => x.Team2Project)
|
|
.Include(x => x.ParentProject)
|
|
.Include(x => x.Scenarios)
|
|
.Include(x => x.Scenarios.Select(s => s.CostSavings1))
|
|
.ToList();
|
|
string defaultColor = "";
|
|
|
|
var settings = DbContext.SystemSettings.FirstOrDefault(item => item.Type == (int)SystemSettingType.DefaultProjectColorType);
|
|
if (settings != null)
|
|
{
|
|
defaultColor = settings.Value;
|
|
}
|
|
//for dependancies source project is parent, target is child
|
|
var projectsModel = availableProjects.Select(x => new
|
|
{
|
|
x.Id,
|
|
Name = x.ParentProjectId.HasValue ? x.Name + ": " + x.ParentProject.Name : x.Name,
|
|
Color = x.ParentProjectId.HasValue && (x.Color == null || x.Color.Trim() == "") ? ((x.ParentProject.Color == null || x.ParentProject.Color.Trim() == "") ? defaultColor : x.ParentProject.Color) : ((x.Color == null || x.Color.Trim() == "") ? defaultColor : x.Color),
|
|
x.Deadline,
|
|
Teams = x.Team2Project.Select(ts => ts.TeamId).Distinct().ToList(),
|
|
ActiveScenario = x.Scenarios.FirstOrDefault(s => s.StartDate.HasValue && s.EndDate.HasValue && s.Type == (int)ScenarioType.Portfolio && s.Status == (int)ScenarioStatus.Active),
|
|
InactiveScenarios = x.Scenarios.Where(s => s.StartDate.HasValue && s.EndDate.HasValue && s.Type == (int)ScenarioType.Portfolio && s.Status == (int)ScenarioStatus.Inactive).ToList(),
|
|
HasLink = DbContext.ProjectDependencies.Any(y => (y.SourceProjectId == x.Id || y.TargetProjectId == x.Id) && y.Type == (int)ProjectDependencyDisplayType.Link),
|
|
HasDependency = DbContext.ProjectDependencies.Any(y => (y.TargetProjectId == x.Id || y.SourceProjectId == x.Id) && y.Type < (int)ProjectDependencyDisplayType.Link),
|
|
AllLinksAndDependencies = GetAllLinksAndDependencies(x.Id)
|
|
}).ToList();
|
|
|
|
var activeScenariosIds = projectsModel.Where(x => x.ActiveScenario != null).Select(x => x.ActiveScenario.Id).ToList();
|
|
var inactiveScenariosIds = projectsModel.Where(x => x.InactiveScenarios != null).SelectMany(x => x.InactiveScenarios.Select(s => s.Id).ToList()).ToList();
|
|
var scenarios = activeScenariosIds.Union(inactiveScenariosIds).ToList();
|
|
var rates = new RateManager(_dbContext).Get4Parents(scenarios, RateModel.RateType.Derived);
|
|
var actualScenarios = scenarioManager.GetScenarios4Projects(projects, ScenarioType.Actuals, null).ToDictionary(x => x.ParentId.Value);
|
|
var scenariosWithActuals = scenarios.Union(actualScenarios.Select(x => x.Value.Id)).ToList();
|
|
var scenariosLMInfo = scenarioManager.GetLaborMaterialsSplit(scenariosWithActuals);
|
|
|
|
Parallel.ForEach(projectsModel, project =>
|
|
//foreach (var project in projectsModel)
|
|
{
|
|
var originalProject = availableProjects.FirstOrDefault(x => x.Id == project.Id);
|
|
var strColor = !String.IsNullOrEmpty(project.Color) ? $"#{project.Color}" : String.Empty;
|
|
var color = ColorTranslator.FromHtml(strColor);
|
|
|
|
var activeScenarioLMInfo = project.ActiveScenario != null && scenariosLMInfo.ContainsKey(project.ActiveScenario.Id)
|
|
? scenariosLMInfo[project.ActiveScenario.Id]
|
|
: new LaborMaterialsCostInfo();
|
|
var actualScenario = actualScenarios.ContainsKey(project.Id) ? actualScenarios[project.Id] : null;
|
|
var actualScenarioLMInfo = actualScenario != null && scenariosLMInfo.ContainsKey(actualScenario.Id)
|
|
? scenariosLMInfo[actualScenario.Id]
|
|
: new LaborMaterialsCostInfo();
|
|
var costSavingItems = project.ActiveScenario == null
|
|
? new List<CostSaving>()
|
|
: project.ActiveScenario.CostSavings1;
|
|
var activeMixScenario = GetScenarioCalendarMixModel(project.ActiveScenario, actualScenario, originalProject,
|
|
costSavingItems, activeScenarioLMInfo, actualScenarioLMInfo);
|
|
var inactiveScenarios = new Dictionary<string, ScenarioCalendarMixModel>();
|
|
|
|
|
|
foreach (var scenario in project.InactiveScenarios.OrderBy(z => z.Name))
|
|
{
|
|
if (inactiveScenarios.ContainsKey(scenario.Id.ToString()))
|
|
continue;
|
|
|
|
var inactiveScenarioLMInfo = scenariosLMInfo.ContainsKey(scenario.Id)
|
|
? scenariosLMInfo[scenario.Id]
|
|
: new LaborMaterialsCostInfo();
|
|
var inactiveScenarioModel = GetScenarioCalendarMixModel(scenario, actualScenario, originalProject,
|
|
scenario.CostSavings1, inactiveScenarioLMInfo, actualScenarioLMInfo);
|
|
|
|
inactiveScenarios.Add(scenario.Id.ToString(), inactiveScenarioModel);
|
|
}
|
|
|
|
|
|
if (activeMixScenario != null && rates.ContainsKey(activeMixScenario.Id))
|
|
{
|
|
var expRates = rates[activeMixScenario.Id]
|
|
.GroupBy(x => x.ExpenditureCategoryId)
|
|
.ToDictionary(x => x.Key, g => g.ToList());
|
|
activeMixScenario.Rates = scenarioManager.GetRatesModel(expRates);
|
|
}
|
|
if (inactiveScenarios != null && inactiveScenarios.Count > 0)
|
|
{
|
|
foreach (var scenario in inactiveScenarios)
|
|
{
|
|
if (rates.ContainsKey(scenario.Value.Id))
|
|
{
|
|
var expRates = rates[scenario.Value.Id]
|
|
.GroupBy(x => x.ExpenditureCategoryId)
|
|
.ToDictionary(x => x.Key, g => g.ToList());
|
|
scenario.Value.Rates = scenarioManager.GetRatesModel(expRates);
|
|
}
|
|
}
|
|
}
|
|
bool dependencyPinned = false;
|
|
string PinnedReason = string.Empty;
|
|
if (project.HasDependency || project.HasLink)
|
|
{
|
|
var notLoaded = project.AllLinksAndDependencies.Where(x => !projectsModel.Select(p => p.Id).ToList().Contains(x))
|
|
.ToList();
|
|
dependencyPinned = (notLoaded.Count > 0);
|
|
if (dependencyPinned)
|
|
{
|
|
PinnedReason = "One or more dependencies are not loaded in the mix for this project";
|
|
}
|
|
}
|
|
result.Add(new MixProjectModel
|
|
{
|
|
Id = project.Id,
|
|
Name = project.Name,
|
|
Color = strColor,
|
|
ColorRGB = !color.IsEmpty ? $"{color.R}, {color.G}, {color.B}" : String.Empty,
|
|
Deadline = project.Deadline.HasValue ? Utils.ConvertToUnixDate(project.Deadline.Value) : (long?) null,
|
|
Teams = project.Teams,
|
|
Scenario = activeMixScenario,
|
|
InactiveScenarios = inactiveScenarios,
|
|
HasDependency = project.HasDependency,
|
|
HasLink = project.HasLink,
|
|
AllLinksAndDependencies = project.AllLinksAndDependencies,
|
|
DependencyPinned = dependencyPinned,
|
|
DependencyToolTip = PinnedReason
|
|
});
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
private List<Guid> GetAllLinksAndDependencies(List<Guid> Ids)
|
|
{
|
|
var sources = DbContext.ProjectDependencies.Where(x => Ids.Contains(x.TargetProjectId)).Select(x => x.SourceProjectId).ToList();
|
|
var targets = DbContext.ProjectDependencies.Where(x => Ids.Contains(x.SourceProjectId)).Select(x => x.TargetProjectId).ToList();
|
|
sources.AddRange(targets);
|
|
var ls = Ids.Count;
|
|
Ids.AddRange(sources);
|
|
if (ls == Ids.Distinct().ToList().Count)
|
|
return Ids.Distinct().ToList();
|
|
return GetAllLinksAndDependencies(Ids.Distinct().ToList());
|
|
}
|
|
private List<Guid> GetAllLinksAndDependencies(Guid id)
|
|
{
|
|
var idList = new List<Guid> {id};
|
|
var list = GetAllLinksAndDependencies(idList);
|
|
list.Remove(id);
|
|
return list;
|
|
}
|
|
public List<MixScenarioTimestampModel> ActivateMix(MixSaveModel mix, string mixKey)
|
|
{
|
|
if (mix.Calendar == null)
|
|
return null;
|
|
|
|
var result = new List<MixScenarioTimestampModel>();
|
|
if (mix.Calendar.Teams != null && mix.Calendar.Teams.Any(x => x.IsNew))
|
|
{
|
|
ActivateTeams(mix.Calendar.Teams);
|
|
|
|
// we should to save all new teams in one separate transaction because every scenario will be saved in the own transaction
|
|
// and it need actual information abuout teams
|
|
DbContext.SaveChanges();
|
|
}
|
|
|
|
if (mix.Calendar.Projects != null && mix.Calendar.Projects.Count > 0)
|
|
{
|
|
// Get list of projects for activation
|
|
List<Guid> projectsToActivate = new List<Guid>();
|
|
|
|
if (mix.Calendar.ManagedProjects != null && mix.Calendar.ManagedProjects.Count > 0)
|
|
// All managed projects
|
|
projectsToActivate.AddRange(mix.Calendar.ManagedProjects);
|
|
|
|
if (mix.Calendar.QueuedProjects != null && mix.Calendar.QueuedProjects.Count > 0)
|
|
{
|
|
// Include queued projects, that have newly created scenarios in the Mix
|
|
projectsToActivate.AddRange(mix.Calendar.QueuedProjects.Where(x =>
|
|
mix.Calendar.Projects.ContainsKey(x.ToString()) &&
|
|
mix.Calendar.Projects[x.ToString()].Scenario != null &&
|
|
mix.Calendar.Projects[x.ToString()].Scenario.IsNew));
|
|
}
|
|
|
|
var savedTeamsInProjects = DbContext.Team2Project.Where(x => projectsToActivate.Contains(x.ProjectId))
|
|
.Select(x => new
|
|
{
|
|
x.ProjectId,
|
|
x.TeamId
|
|
})
|
|
.ToList()
|
|
.GroupBy(x => x.ProjectId)
|
|
.ToDictionary(x => x.Key, g => g.Select(s => s.TeamId).ToList());
|
|
|
|
foreach (var projectId in projectsToActivate)
|
|
{
|
|
if (!mix.Calendar.Projects.ContainsKey(projectId.ToString()))
|
|
continue;
|
|
|
|
var project = mix.Calendar.Projects[projectId.ToString()];
|
|
if (project?.Scenario == null)
|
|
continue;
|
|
|
|
if (project.Teams == null)
|
|
project.Teams = new List<Guid>();
|
|
|
|
// we should preserve all teams for project and all its scenarios, we can remove only team allocations
|
|
if (savedTeamsInProjects.ContainsKey(project.Id))
|
|
{
|
|
var requiredTeams = savedTeamsInProjects[projectId].Where(x => project.Teams.All(s => s != x)).ToList();
|
|
project.Teams.AddRange(requiredTeams);
|
|
}
|
|
|
|
// new scenario should be created with name equals to mix name
|
|
project.Scenario.Name = mix.Name;
|
|
var scenarioId = ActivateScenario(project.Scenario, project.Teams, mixKey, true);
|
|
DbContext.SaveChanges();
|
|
|
|
result.Add(new MixScenarioTimestampModel
|
|
{
|
|
ProjectId = projectId,
|
|
ScenarioId = scenarioId
|
|
});
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public void ActivateTeams(List<MixTeamModel> mixTeams)
|
|
{
|
|
if (mixTeams == null || mixTeams.Count <= 0 || !mixTeams.Any(x => x.IsNew))
|
|
return;
|
|
|
|
var teamManager = (new TeamManager(DbContext));
|
|
var rateManager = (new RateManager(DbContext));
|
|
|
|
var newTeams = mixTeams.FindAll(x => x.IsNew);
|
|
var expCatsInTeams = newTeams.Where(x => x.PlannedCapacity != null).SelectMany(x => x.PlannedCapacity.Select(t => t.ExpCatId)).ToList();
|
|
var expCatsInTeamsGuid = new List<Guid>();
|
|
foreach (var expCatString in expCatsInTeams)
|
|
{
|
|
Guid expCatId;
|
|
if (string.IsNullOrWhiteSpace(expCatString) || !Guid.TryParse(expCatString, out expCatId) || expCatId == Guid.Empty)
|
|
continue;
|
|
|
|
expCatsInTeamsGuid.Add(expCatId);
|
|
}
|
|
|
|
var rates = rateManager.GetRates(expCatsInTeamsGuid, RateModel.RateType.Global);
|
|
var currentUser = !string.IsNullOrWhiteSpace(_userId) ? new Guid(_userId) : Guid.Empty; // SA. ENV-1083
|
|
|
|
foreach (var team in newTeams)
|
|
{
|
|
// SA. ENV-1083. Automatically add current user to team contributors.
|
|
if (!currentUser.Equals(Guid.Empty))
|
|
{
|
|
if (team.UserId == null)
|
|
team.UserId = new Guid[1] { currentUser };
|
|
else
|
|
if (!team.UserId.Contains(currentUser))
|
|
{
|
|
List<Guid> tmpUsersList = team.UserId.ToList();
|
|
tmpUsersList.Add(currentUser);
|
|
team.UserId = tmpUsersList.ToArray();
|
|
}
|
|
}
|
|
|
|
var savedTeam = teamManager.Save((TeamModel)team, true);
|
|
if (savedTeam?.PlannedCapacityScenarioId == null)
|
|
continue;
|
|
|
|
if (team.PlannedCapacity == null || team.PlannedCapacity.Count <= 0)
|
|
continue;
|
|
|
|
foreach (var capacity in team.PlannedCapacity)
|
|
{
|
|
if (capacity.Values == null || capacity.Values.Count <= 0)
|
|
continue;
|
|
|
|
Guid expCatId;
|
|
if (string.IsNullOrWhiteSpace(capacity.ExpCatId) || !Guid.TryParse(capacity.ExpCatId, out expCatId) || expCatId == Guid.Empty)
|
|
continue;
|
|
|
|
foreach (var value in capacity.Values)
|
|
{
|
|
long weekEnding;
|
|
if (!long.TryParse(value.Key, out weekEnding) || weekEnding <= 0)
|
|
continue;
|
|
|
|
DbContext.ScenarioDetail.Add(new ScenarioDetail
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ParentID = savedTeam.PlannedCapacityScenarioId.Value,
|
|
ExpenditureCategoryId = expCatId,
|
|
WeekEndingDate = Utils.ConvertFromUnixDate(weekEnding),
|
|
Quantity = value.Value,
|
|
Cost = value.Value * rateManager.GetRateValue(rates, expCatId, Utils.ConvertFromUnixDate(weekEnding))
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public Guid ActivateScenario(ScenarioCalendarMixModel scenario, List<Guid> teams, string mixKey, bool isActive)
|
|
{
|
|
if (scenario == null)
|
|
throw new ArgumentNullException(nameof(scenario));
|
|
|
|
if ((scenario.Expenditures == null || scenario.Expenditures.Count <= 0) && scenario.Id != Guid.Empty)
|
|
scenario.Expenditures = GetFullAllocationInfoByScenario(mixKey, scenario.Id);
|
|
|
|
var scenarioManager = new ScenarioManager(DbContext);
|
|
var scenarioSaveModel = new ScenarioDetailsSnapshotSaveModel(scenario);
|
|
|
|
if (scenarioSaveModel.Calendar == null)
|
|
return Guid.Empty;
|
|
|
|
scenarioSaveModel.Scenario.IsActiveScenario = isActive;
|
|
|
|
if (teams != null && teams.Count > 0)
|
|
{
|
|
scenarioSaveModel.TeamsInScenario = teams.Select(x => new TeamInScenarioModel
|
|
{
|
|
TeamId = x
|
|
}).ToList();
|
|
}
|
|
|
|
var scenarioId = scenarioManager.Save(scenarioSaveModel, _userId);
|
|
if (scenarioId == Guid.Empty)
|
|
throw new InvalidOperationException("Save scenario method returned empty");
|
|
|
|
// save local rates for this scenario
|
|
if (scenario.Rates != null && scenario.Rates.Count > 0)
|
|
{
|
|
var rates = scenarioManager.GetRatesFromModel(scenario.Rates);
|
|
rates.ForEach(rate =>
|
|
{
|
|
rate.Id = Guid.NewGuid();
|
|
rate.ParentId = scenarioId;
|
|
rate.Type = (short) RateModel.RateType.Derived;
|
|
});
|
|
|
|
DbContext.Rates.AddRange(rates);
|
|
}
|
|
|
|
return scenarioId;
|
|
}
|
|
|
|
public Dictionary<string, ExpenditureDetail> GetFullAllocationInfoByScenario(string mixKey, Guid scenarioId)
|
|
{
|
|
var allocations = new Dictionary<string, ExpenditureDetail>();
|
|
if (scenarioId == Guid.Empty)
|
|
return allocations;
|
|
|
|
if (!string.IsNullOrWhiteSpace(mixKey))
|
|
{
|
|
var mixId = GetMixIdByKey(mixKey, true);
|
|
allocations = GetAllocations4Scenario(mixId, scenarioId);
|
|
}
|
|
|
|
if (allocations == null || allocations.Count <= 0)
|
|
allocations = (new ScenarioManager(DbContext)).GetFullAllocationInfoByScenario(scenarioId, _userId);
|
|
|
|
return allocations;
|
|
}
|
|
/// <summary>
|
|
/// Gets people resource allocations for the specified Mix.Id and scenarios.
|
|
/// </summary>
|
|
/// <param name="mixId">An unique identifier of the Mix.</param>
|
|
/// <param name="scenarios">A list of scenario Ids.</param>
|
|
/// <returns>Returns a list of resource allocations.</returns>
|
|
public List<MixResourceAllocation> GetResourceAllocations(string mixId, List<Guid> scenarios)
|
|
{
|
|
return GetResourceAllocationsAsync(mixId, scenarios).GetAwaiter().GetResult();
|
|
}
|
|
/// <summary>
|
|
/// Runs a task for loading people resource allocations for the specified Mix.Id and scenarios.
|
|
/// </summary>
|
|
/// <param name="mixId">An unique identifier of the Mix.</param>
|
|
/// <param name="scenarios">A list of scenario Ids.</param>
|
|
/// <returns>Returns an asynchrounous task.</returns>
|
|
public Task<List<MixResourceAllocation>> GetResourceAllocationsAsync(string mixId, List<Guid> scenarios)
|
|
{
|
|
return MongoDataContext.ResourceAllocations.Find(t => t.MixId == new ObjectId(mixId) && scenarios.Contains(t.ScenarioId)).ToListAsync();
|
|
}
|
|
public Dictionary<string, string> GetResourceNames(string mixId, List<Guid> resources)
|
|
{
|
|
var resIds = resources.Select(t => t.ToString());
|
|
return MongoDataContext.ResourceAllocations.Find(t => t.MixId == new ObjectId(mixId) && resIds.Contains(t.ResourceId)).ToListAsync().GetAwaiter().GetResult()
|
|
.Select(t => new
|
|
{
|
|
Id = t.ResourceId,
|
|
t.Name
|
|
}).Distinct().ToDictionary(key => key.Id, el => el.Name);
|
|
}
|
|
|
|
public List<MixModelBase> GetMixesByUser()
|
|
{
|
|
Guid userId;
|
|
if (!string.IsNullOrWhiteSpace(_userId) && Guid.TryParse(_userId, out userId) && userId != Guid.Empty)
|
|
{
|
|
return MongoDataContext.GetCollection<MixBase>(MongoDataContext.COLL_NAME_MIXES)
|
|
.Find(x => x.Users.Contains(userId))
|
|
.SortBy(o => o.Name)
|
|
.ToListAsync().GetAwaiter().GetResult()
|
|
.Select(t => new MixModelBase
|
|
{
|
|
Id = t.Key.ToString(),
|
|
Name = t.Name,
|
|
StartDate = t.StartDate,
|
|
EndDate = t.EndDate
|
|
}).ToList();
|
|
}
|
|
return new List<MixModelBase>();
|
|
}
|
|
|
|
public List<SelectListItem> GetMixesAvailable4User()
|
|
{
|
|
var foundMixes = GetMixesByUser();
|
|
|
|
if (foundMixes != null)
|
|
{
|
|
return foundMixes.Select(x => new SelectListItem
|
|
{
|
|
Value = x.Id.ToString(),
|
|
Text = x.Name
|
|
}).ToList();
|
|
}
|
|
return new List<SelectListItem>();
|
|
}
|
|
|
|
|
|
public Dictionary<Guid, PeopleResourceAllocationsInfoModel> GetResourceAllocationsInfo(List<Guid> resources)
|
|
{
|
|
if (resources == null)
|
|
throw new ArgumentNullException(nameof(resources));
|
|
|
|
if (resources.Count < 1)
|
|
return new Dictionary<Guid, PeopleResourceAllocationsInfoModel>();
|
|
var resourceIdsAsText = resources.Select(x => x.ToString()).ToList();
|
|
var foundResourceAllocations = LoadResourceAllocationsRecordCount(resourceIdsAsText);
|
|
var result = resourceIdsAsText.AsParallel().ToDictionary(k => new Guid(k), v =>
|
|
new PeopleResourceAllocationsInfoModel
|
|
{
|
|
HasAllocations = foundResourceAllocations.ContainsKey(v) && foundResourceAllocations[v] > 0
|
|
});
|
|
|
|
return result;
|
|
}
|
|
/// <summary>
|
|
/// Loads number of MixResourceAllocation records from database for each of the specified resources.
|
|
/// </summary>
|
|
/// <param name="resourceIds">A list of resource Ids.</param>
|
|
/// <returns>Dictionary with Key=ResourceId, Value=Count of resources.</returns>
|
|
private Dictionary<string, long> LoadResourceAllocationsRecordCount(IEnumerable<string> resourceIds)
|
|
{
|
|
if (resourceIds == null || !resourceIds.Any())
|
|
return null;
|
|
var filter = Builders<MixResourceAllocation>.Filter.In(x => x.ResourceId, resourceIds);
|
|
var query = MongoDataContext.ResourceAllocations.Aggregate()
|
|
.Match(filter)
|
|
.Group((new BsonDocument { { "_id", "$ResourceId" }, { "count", new BsonDocument("$sum", 1) } }));
|
|
var counts = query.ToListAsync().GetAwaiter().GetResult()
|
|
.ToDictionary(t => t.GetValue("_id").ToString(), el => el.GetValue("count").ToInt64());
|
|
return counts;
|
|
}
|
|
|
|
private Dictionary<string, ExpenditureDetail> GetAllocations4Scenario(string mixId, Guid scenarioId)
|
|
{
|
|
var allocations = GetAllocations4Scenarios(mixId, new List<Guid> { scenarioId });
|
|
if (allocations == null || !allocations.ContainsKey(scenarioId))
|
|
return new Dictionary<string, ExpenditureDetail>();
|
|
|
|
return allocations[scenarioId];
|
|
}
|
|
|
|
private Dictionary<Guid, Dictionary<string, ExpenditureDetail>> GetAllocations4Scenarios(string mixId, List<Guid> scenarios)
|
|
{
|
|
var allocations = new Dictionary<Guid, Dictionary<string, ExpenditureDetail>>();
|
|
if (string.IsNullOrWhiteSpace(mixId) || scenarios == null || scenarios.Count <= 0)
|
|
return allocations;
|
|
|
|
var mid = new ObjectId(mixId);
|
|
var tasks = new List<Task>();
|
|
// prepare async tasks and run them
|
|
var expTask = MongoDataContext.ExpenditureAllocations
|
|
.Find(t => t.MixId == mid && scenarios.Contains(t.ScenarioId)).ToListAsync();
|
|
var teamTask = MongoDataContext.TeamAllocations
|
|
.Find(t => t.MixId == mid && scenarios.Contains(t.ScenarioId)).ToListAsync();
|
|
var resTask = GetResourceAllocationsAsync(mixId, scenarios);
|
|
// wait for all data loaded
|
|
Task.WaitAll(tasks.ToArray());
|
|
// group loaded data
|
|
var expenditureAllocations = expTask.Result.GroupBy(x => x.ScenarioId)
|
|
.ToDictionary(x => x.Key, g => g.ToList());
|
|
var teamAllocations = teamTask.Result.GroupBy(x => x.ScenarioId)
|
|
.ToDictionary(x => x.Key, g => g.ToList());
|
|
var resourceAllocations = resTask.Result.GroupBy(x => x.ScenarioId)
|
|
.ToDictionary(x => x.Key, g => g.ToList());
|
|
|
|
foreach (var scenarioId in scenarios)
|
|
{
|
|
if (!expenditureAllocations.ContainsKey(scenarioId))
|
|
continue;
|
|
|
|
var expenditures = expenditureAllocations[scenarioId].ToDictionary(x => x.ExpCatId.ToString(), g => (ExpenditureDetail)g);
|
|
allocations.Add(scenarioId, expenditures);
|
|
|
|
if (!teamAllocations.ContainsKey(scenarioId))
|
|
continue;
|
|
|
|
foreach (var expCatId in allocations[scenarioId].Keys)
|
|
{
|
|
var ec = allocations[scenarioId][expCatId];
|
|
ec.Teams = teamAllocations[scenarioId].Where(x => x.ExpCatId.ToString() == expCatId)
|
|
.ToDictionary(x => x.TeamId, g => (ExpenditureDetailsTeam)g);
|
|
|
|
if (!resourceAllocations.ContainsKey(scenarioId))
|
|
continue;
|
|
|
|
foreach (var teamId in ec.Teams.Keys)
|
|
{
|
|
ec.Teams[teamId].Resources = resourceAllocations[scenarioId].Where(x => x.ExpCatId.ToString() == expCatId && x.TeamId == teamId)
|
|
.ToDictionary(x => x.ResourceId, g => (TeamResource)g);
|
|
}
|
|
}
|
|
}
|
|
|
|
return allocations;
|
|
}
|
|
|
|
private ScenarioCalendarMixModel GetScenarioCalendarMixModel(Scenario scenario, Scenario actualScenario, Project project, IEnumerable<CostSaving> costSavings, LaborMaterialsCostInfo scenarioLMInfo, LaborMaterialsCostInfo actualLMInfo)
|
|
{
|
|
if (scenario == null)
|
|
return null;
|
|
|
|
var model = new ScenarioCalendarMixModel
|
|
{
|
|
Id = scenario.Id,
|
|
StartDate = scenario.StartDate.HasValue ? Utils.ConvertToUnixDate(scenario.StartDate.Value) : 0,
|
|
EndDate = scenario.EndDate.HasValue ? Utils.ConvertToUnixDate(scenario.EndDate.Value) : 0,
|
|
TemplateId = scenario.TemplateId ?? Guid.Empty,
|
|
ParentId = scenario.ParentId,
|
|
GrowthScenario = scenario.GrowthScenario,
|
|
Type = (ScenarioType)scenario.Type,
|
|
Duration = scenario.Duration ?? 0,
|
|
Name = scenario.Name,
|
|
IsBottomUp = scenario.IsBottomUp,
|
|
VersionInfo = new ItemVersionInfo // SA. ENV-1085
|
|
{
|
|
SourceVersion = scenario.GetCurrentVersion()
|
|
}
|
|
};
|
|
|
|
using (var scenarioManager = new ScenarioManager(DbContext))
|
|
{
|
|
var finInfo = scenarioManager.GetScenarioFinInfoModel(scenario, actualScenario, project,
|
|
costSavings, scenarioLMInfo, actualLMInfo);
|
|
if (finInfo != null)
|
|
{
|
|
model.FinInfo = new ScenarioCalendarMixFinInfoModel
|
|
{
|
|
IsRevenueGenerating = finInfo.IsRevenueGenerating,
|
|
ProjectedRevenue = finInfo.ProjectedRevenue ?? 0,
|
|
RevenueAfterCost = finInfo.RevenueAfterCost,
|
|
ActualRevenueAfterCost = finInfo.ActualRevenueAfterCost,
|
|
GrossMargin = finInfo.GrossMargin,
|
|
LMMargin = finInfo.LMMargin,
|
|
UseLMMargin = finInfo.UseLMMargin,
|
|
CalculatedGrossMargin = finInfo.CalculatedGrossMargin,
|
|
CalculatedGrossMarginLM = finInfo.CalculatedGrossMarginLM,
|
|
CalculatedGrossMarginActuals = finInfo.CalculatedGrossMarginActuals,
|
|
CalculatedGrossMarginLMActuals = finInfo.CalculatedGrossMarginLMActuals,
|
|
TDDirectCosts = finInfo.TDDirectCosts,
|
|
BUDirectCosts = finInfo.BUDirectCosts,
|
|
ActualLabor = finInfo.ActualLabor,
|
|
LaborMaterialsSplit = finInfo.LaborMaterialsSplit,
|
|
ActualLaborMaterialsSplit = finInfo.ActualLaborMaterialsSplit,
|
|
ActualMaterials = finInfo.ActualMaterials,
|
|
ActualsTotal = finInfo.ActualsTotal,
|
|
EFXSplit = finInfo.EFXSplit,
|
|
LaborSplitPercentage = finInfo.LaborSplitPercentage,
|
|
CostSaving = finInfo.CostSaving == null
|
|
? null
|
|
: new CostSavingSnapshotModel
|
|
{
|
|
CostSavings = finInfo.CostSaving.CostSavings,
|
|
CostSavingStartDate = finInfo.CostSaving.CostSavingStartDate.HasValue
|
|
? Utils.ConvertToUnixDate(finInfo.CostSaving.CostSavingStartDate.Value)
|
|
: (long?) null,
|
|
CostSavingEndDate = finInfo.CostSaving.CostSavingEndDate.HasValue
|
|
? Utils.ConvertToUnixDate(finInfo.CostSaving.CostSavingEndDate.Value)
|
|
: (long?) null,
|
|
CostSavingType = finInfo.CostSaving.CostSavingType,
|
|
CostSavingDescription = finInfo.CostSaving.CostSavingDescription,
|
|
CostSavingItems = scenarioManager.GetScenarioCostSavingModelItems(scenario.CostSavings1)
|
|
}
|
|
};
|
|
}
|
|
}
|
|
return model;
|
|
}
|
|
}
|
|
} |