using System; using System.Collections; using System.Collections.Generic; using System.Data.Entity; using System.Globalization; using System.Linq; using EnVisage.Models; using EnVisage.Properties; namespace EnVisage.Code.BLL { /// /// Provides ability to manage fiscal calendar. /// public class FiscalCalendarManager : IDisposable { private readonly EnVisageEntities _dbContext; private readonly bool _isContexLocal = false; public FiscalCalendarManager(EnVisageEntities dbContext = null) { if (dbContext == null) { _dbContext = new EnVisageEntities(); _isContexLocal = true; } else { _dbContext = dbContext; } } #region Fiscal Calendar Settings /// /// Loads fiscal calendar setings from database. /// /// A object filled with data from SystemSettings DB table. public FiscalCalendarModel LoadFiscalCalendarSettings(bool isReadOnly = true) { var model = new FiscalCalendarModel(); var dbTable = isReadOnly ? _dbContext.SystemSettings.AsNoTracking() : _dbContext.SystemSettings; var settings = dbTable.Where( item => item.Type == (int) SystemSettingType.FiscalCalendarType || item.Type == (int) SystemSettingType.FiscalCalendarStartDate || item.Type == (int) SystemSettingType.FiscalCalendarWeekEnding || item.Type == (int) SystemSettingType.FiscalCalendarWeekEndingType); foreach (var systemSetting in settings) { switch ((SystemSettingType)systemSetting.Type) { case SystemSettingType.FiscalCalendarStartDate: long ticks; model.CurrentYearStartDate = long.TryParse(systemSetting.Value, out ticks) ? new DateTime(ticks).Date : (DateTime?)null; model.StartDateSettingId = systemSetting.Id; break; case SystemSettingType.FiscalCalendarType: model.Type = String.IsNullOrEmpty(systemSetting.Value) ? (FiscalCalendarModel.FiscalCalendarType?)null : (FiscalCalendarModel.FiscalCalendarType)Convert.ToInt16(systemSetting.Value); model.TypeSettingId = systemSetting.Id; break; case SystemSettingType.FiscalCalendarWeekEnding: model.WeekEnding = String.IsNullOrEmpty(systemSetting.Value) ? null : (DayOfWeek?)Convert.ToInt16(systemSetting.Value); model.WeekEndingSettingId = systemSetting.Id; break; case SystemSettingType.FiscalCalendarWeekEndingType: model.WeekEndingType = String.IsNullOrEmpty(systemSetting.Value) ? (FiscalCalendarModel.CalendarYearType?)null : (FiscalCalendarModel.CalendarYearType)Convert.ToInt16(systemSetting.Value); model.WeekEndingTypeSettingId = systemSetting.Id; break; } } return model; } /// /// Saves fiscal calendar settings to the database. /// /// A model with entered fiscal calendar settings. public void SaveFiscalCalendarSettings(FiscalCalendarModel model) { if (model == null) throw new ArgumentNullException("model"); #region Save Calendar Type if (model.TypeSettingId == Guid.Empty) { model.TypeSettingId = Guid.NewGuid(); _dbContext.SystemSettings.Add(new SystemSetting { Id = model.TypeSettingId, Type = (int)SystemSettingType.FiscalCalendarType, Value = model.Type.HasValue ? model.Type.GetHashCode().ToString(CultureInfo.InvariantCulture) : string.Empty }); } else { var setting = _dbContext.SystemSettings.Find(model.TypeSettingId); if (setting == null) { throw new BLLException("Unable to load type of calendar setting. Please, reload the page."); } setting.Value = model.Type.HasValue ? model.Type.GetHashCode().ToString(CultureInfo.InvariantCulture) : string.Empty; _dbContext.Entry(setting).State = EntityState.Modified; } #endregion #region Save Start Date if (model.StartDateSettingId == Guid.Empty) { model.StartDateSettingId = Guid.NewGuid(); _dbContext.SystemSettings.Add(new SystemSetting { Id = model.StartDateSettingId, Type = (int)SystemSettingType.FiscalCalendarStartDate, Value = model.CurrentYearStartDate == null ? string.Empty : model.CurrentYearStartDate.Value.Ticks.ToString(CultureInfo.InvariantCulture) }); } else { var setting = _dbContext.SystemSettings.Find(model.StartDateSettingId); if (setting == null) { throw new BLLException("Unable to load current year start date setting. Please, reload the page."); } setting.Value = model.CurrentYearStartDate == null ? string.Empty : model.CurrentYearStartDate.Value.Ticks.ToString(CultureInfo.InvariantCulture); _dbContext.Entry(setting).State = EntityState.Modified; } #endregion #region Save Week Ending Day if (model.WeekEndingSettingId == Guid.Empty) { model.WeekEndingSettingId = Guid.NewGuid(); _dbContext.SystemSettings.Add(new SystemSetting { Id = model.WeekEndingSettingId, Type = (int)SystemSettingType.FiscalCalendarWeekEnding, Value = model.WeekEnding.HasValue ? model.WeekEnding.GetHashCode().ToString(CultureInfo.InvariantCulture) : string.Empty }); } else { var setting = _dbContext.SystemSettings.Find(model.WeekEndingSettingId); if (setting == null) { throw new BLLException("Unable to load week ending setting. Please, reload the page."); } setting.Value = model.WeekEnding.HasValue ? model.WeekEnding.GetHashCode().ToString(CultureInfo.InvariantCulture) : string.Empty; _dbContext.Entry(setting).State = EntityState.Modified; } #endregion #region Save Week Ending Type if (model.WeekEndingTypeSettingId == Guid.Empty) { model.WeekEndingTypeSettingId = Guid.NewGuid(); _dbContext.SystemSettings.Add(new SystemSetting { Id = model.WeekEndingTypeSettingId, Type = (int)SystemSettingType.FiscalCalendarWeekEndingType, Value = model.WeekEndingType.HasValue ? model.WeekEndingType.GetHashCode().ToString(CultureInfo.InvariantCulture) : string.Empty }); } else { var setting = _dbContext.SystemSettings.Find(model.WeekEndingTypeSettingId); if (setting == null) { throw new BLLException("Unable to load week ending type setting. Please, reload the page."); } setting.Value = model.WeekEndingType.HasValue ? model.WeekEndingType.GetHashCode().ToString(CultureInfo.InvariantCulture) : string.Empty; _dbContext.Entry(setting).State = EntityState.Modified; } #endregion #region Update fiscal year records and scenario details records // TODO: finish and test different use cases before uncomment this code to prevent fiscal calendar data loss //var dictionary = new Dictionary>(); //var index = 0; //foreach (var item in _dbContext.FiscalCalendars.Where( // t => t.Type == (int?)FiscalCalendarModel.FiscalCalendarType.Week && t.NonWorking == 0).OrderBy(o => o.YearInt).ThenBy(o => o.PeriodInt) // .Select(week => new // { // week.Name, // week.Type, // week.YearInt, // week.QuarterInt, // week.PeriodInt, // week.EndDate, // }).ToArray().Distinct()) //{ // dictionary.Add(index++, new KeyValuePair(item.EndDate.Value, null)); //} //((System.Data.Entity.Infrastructure.IObjectContextAdapter)_dbContext).ObjectContext.ExecuteStoreCommand("TRUNCATE TABLE [FiscalCalendar]"); //UpdateFiscalCalendars(model); //_dbContext.SaveChanges(); //index = 0; //foreach ( // var item in // _dbContext.FiscalCalendars.Where( // t => t.Type == (int?)FiscalCalendarModel.FiscalCalendarType.Week && t.NonWorking == 0).OrderBy(o => o.YearInt).ThenBy(o => o.PeriodInt) // .Select(week => new // { // week.Name, // week.Type, // week.YearInt, // week.QuarterInt, // week.PeriodInt, // week.EndDate, // }).ToArray().Distinct()) //{ // var dt = dictionary[index].Key; // dictionary[index] = new KeyValuePair(dt, item.EndDate.Value); // index++; //} //foreach (var keyValuePair in dictionary) //{ // ((System.Data.Entity.Infrastructure.IObjectContextAdapter)_dbContext).ObjectContext.ExecuteStoreCommand // (string.Format("UPDATE [ScenarioDetail] SET WeekEndingDate='{0}' WHERE WeekEndingDate='{1}'", keyValuePair.Value.Value, keyValuePair.Value.Key)); //} #endregion if (_isContexLocal) _dbContext.SaveChanges(); } public void UpdateFiscalCalendars(FiscalCalendarModel model) { switch (model.Type) { case FiscalCalendarModel.FiscalCalendarType.Calendar445: case FiscalCalendarModel.FiscalCalendarType.Calendar454: case FiscalCalendarModel.FiscalCalendarType.Calendar544: Apply445Year(model); break; default: ApplyCalendarYear(model); break; } } /// /// Generate fiscal calendar periods by the Calendar Year algo. 52 weeks in the year. Last 1-2 days will be be adjusted. /// /// private void ApplyCalendarYear(FiscalCalendarModel model) { var startDate = model.CurrentYearStartDate ?? DateTime.Today; var periodEndDate = startDate.AddYears(Settings.Default.GenerateCalendarYears); var weekEndDate = startDate.AddDays(6); var weeksList = new List(periodEndDate.Subtract(startDate).Days / 7); // approx number of weeks var monthList = new List(periodEndDate.Subtract(startDate).Days / 30); // approx number of months var quarterList = new List(periodEndDate.Subtract(startDate).Days / 91); // approx number of quarters var yearList = new List(Settings.Default.GenerateCalendarYears); FiscalCalendar prevWeek = null, prevMonth = null, prevQuarter = null, prevYear = null; #region Load one latest record of each period type before startDate #endregion #region Create adjustment between last enddate from loaded record and startDate #endregion #region Generate records from startDate to periodEndDate int weekPeriodInt = 1, monthWeekNum = 1, monthNum = 1, quarterNum = 1; while (weekEndDate < periodEndDate) { #region Create a new year if new week is in the new calendar year var year = yearList.LastOrDefault(); if (year == null || year.EndDate < weekEndDate) { year = new FiscalCalendar { Id = Guid.NewGuid(), Name = Constants.FISCAL_YEAR_NAME_TEMPLATE, Type = (int)FiscalCalendarModel.FiscalYearType.Year, YearInt = weekEndDate.Year, QuarterInt = 4, PeriodInt = 1, StartDate = new DateTime(weekEndDate.Year, 1, 1), EndDate = new DateTime(weekEndDate.Year, 12, 31), SystemName = string.Format(Constants.FISCAL_YEAR_SYSTEMNAME_TEMPLATE, (weekEndDate.Year % 100).ToString("00")), AdjustingPeriod = false, NonWorking = 0 }; yearList.Add(year); quarterNum = 1; // reset quarter of the year number monthNum = 1; // reset month of the year number; weekPeriodInt = 1; // reset week of the year number; } #endregion #region Create a new quarter if new week is in the new quarter var quarter = quarterList.LastOrDefault(); if (quarter == null || quarter.EndDate < weekEndDate) { quarter = new FiscalCalendar { Id = Guid.NewGuid(), Name = string.Format(Constants.FISCAL_QUARTER_NAME_TEMPLATE, quarterNum), Type = (int)FiscalCalendarModel.FiscalYearType.Quarter, YearInt = year.YearInt, QuarterInt = quarterNum, PeriodInt = quarterNum, StartDate = (prevWeek == null) ? startDate : prevWeek.EndDate.AddDays(1), EndDate = ((prevWeek == null) ? startDate : prevWeek.EndDate.AddDays(1)).AddDays(90),// add 7 days * 13 weeks in quarter = 91 days SystemName = string.Format(Constants.FISCAL_QUARTER_SYSTEMNAME_TEMPLATE, quarterNum, (weekEndDate.Year % 100).ToString("00")), AdjustingPeriod = false, NonWorking = 0 }; if (quarterNum == 4) quarter.EndDate = new DateTime(quarter.EndDate.Year, 12, 31); quarterList.Add(quarter); quarterNum++; } #endregion #region Create a new month if new week is in the new month var month = monthList.LastOrDefault(); if (month == null || month.EndDate < weekEndDate) { month = new FiscalCalendar { Id = Guid.NewGuid(), Name = string.Format(Constants.FISCAL_MONTH_NAME_TEMPLATE, weekEndDate.ToString("MMM").ToUpper()), Type = (int)FiscalCalendarModel.FiscalYearType.Month, YearInt = year.YearInt, QuarterInt = quarterNum - 1, PeriodInt = monthNum, StartDate = (prevWeek == null ) ? startDate : prevWeek.EndDate.AddDays(1), EndDate = ((prevWeek == null ) ? startDate : prevWeek.EndDate.AddDays(1)).AddDays(27),// add 7 days * 4 weeks in month = 28 days SystemName = string.Format(Constants.FISCAL_MONTH_SYSTEMNAME_TEMPLATE, weekEndDate.ToString("MMM").ToUpper(), (weekEndDate.Year % 100).ToString("00")), AdjustingPeriod = false, NonWorking = 0 }; // add 5th week to the 1st month of the quarter if (monthNum % 3 == 0) { month.EndDate = month.EndDate.AddDays(7); } // if we can place a 14th quarter week before 1st of January of next year then place it into the last quarter if (quarter.QuarterInt == 4 && month.PeriodInt == 12) month.EndDate = new DateTime(month.EndDate.Year, 12, 31); monthList.Add(month); monthNum++; monthWeekNum = 1; // reset week of the month number } #endregion // create a new week record var week = new FiscalCalendar { Id = Guid.NewGuid(), Name = string.Format(Constants.FISCAL_WEEK_NAME_TEMPLATE, month.Name.ToUpper(), monthWeekNum), Type = (int)FiscalCalendarModel.FiscalYearType.Week, YearInt = year.YearInt, QuarterInt = quarterNum - 1, PeriodInt = weekPeriodInt, StartDate = (prevWeek == null) ? startDate : prevWeek.EndDate.AddDays(1), EndDate = weekEndDate, SystemName = string.Format(Constants.FISCAL_WEEK_SYSTEMNAME_TEMPLATE, month.Name.ToUpper(), monthWeekNum, (weekEndDate.Year % 100).ToString("00")), AdjustingPeriod = false, NonWorking = 0 }; weeksList.Add(week); // adjust remaining 1-2 days at the end of the year if (week.PeriodInt == 52) { monthWeekNum++; weekPeriodInt++; week = new FiscalCalendar() { Id = Guid.NewGuid(), Name = string.Format(Constants.FISCAL_WEEK_NAME_TEMPLATE, month.Name.ToUpper(), monthWeekNum), Type = (int)FiscalCalendarModel.FiscalYearType.Week, YearInt = year.YearInt, QuarterInt = quarterNum - 1, PeriodInt = weekPeriodInt, StartDate = week.EndDate.AddDays(1), EndDate = new DateTime(week.EndDate.Year, 12 , 31), SystemName = string.Format(Constants.FISCAL_WEEK_SYSTEMNAME_TEMPLATE, month.Name.ToUpper(), monthWeekNum, (weekEndDate.Year % 100).ToString("00")), AdjustingPeriod = false, NonWorking = 1 }; weeksList.Add(week); } monthWeekNum++; weekPeriodInt++; prevWeek = week; weekEndDate = week.EndDate.AddDays(7); } #endregion #region Save generated fiscal periods to the DB foreach (var FiscalCalendar in yearList) { _dbContext.FiscalCalendars.Add(FiscalCalendar); } foreach (var obj in quarterList) { _dbContext.FiscalCalendars.Add(obj); } foreach (var obj in monthList) { _dbContext.FiscalCalendars.Add(obj); } foreach (var obj in weeksList) { _dbContext.FiscalCalendars.Add(obj); } #endregion } /// /// Generate fiscal calendar periods by the 5-4-4 algo. 52/53 weeks in the year. 53rd weeks added only when it become a 7 day gap at the end of the year. /// /// private void Apply445Year(FiscalCalendarModel model) { var startDate = model.CurrentYearStartDate ?? DateTime.Today; var periodEndDate = startDate.AddYears(Settings.Default.GenerateCalendarYears); var weekEndDate = startDate.AddDays(6); var weeksList = new List(periodEndDate.Subtract(startDate).Days / 7); // approx number of weeks var monthList = new List(periodEndDate.Subtract(startDate).Days / 30); // approx number of months var quarterList = new List(periodEndDate.Subtract(startDate).Days / 91); // approx number of quarters var yearList = new List(Settings.Default.GenerateCalendarYears); FiscalCalendar prevWeek = null, prevMonth = null, prevQuarter = null, prevYear = null; #region Load one latest record of each period type before startDate #endregion #region Create adjustment between last enddate from loaded record and startDate #endregion #region Generate records from startDate to periodEndDate //TODO: initial version, maybe required some refactoring int weekPeriodInt = 1, monthWeekNum = 1, monthNum = 1, quarterNum = 1; var isAddExtraWeek = false; while (weekEndDate < periodEndDate) { #region Create a new year if new week is in the new calendar year var year = yearList.LastOrDefault(); if (year == null || year.EndDate < weekEndDate) { year = new FiscalCalendar { Id = Guid.NewGuid(), Name = Constants.FISCAL_YEAR_NAME_TEMPLATE, Type = (int)FiscalCalendarModel.FiscalYearType.Year, YearInt = weekEndDate.Year, QuarterInt = 4, PeriodInt = 1, StartDate = (prevWeek == null) ? startDate : prevWeek.EndDate.AddDays(1), EndDate = ((prevWeek == null) ? startDate : prevWeek.EndDate.AddDays(1)).AddDays(363),// add 7 days * 52 weeks in year = 364 days SystemName = string.Format(Constants.FISCAL_YEAR_SYSTEMNAME_TEMPLATE, (weekEndDate.Year % 100).ToString("00")), AdjustingPeriod = false, NonWorking = 0 }; // if we can place a 53rd week before 1st of January of next year then do it isAddExtraWeek = year.EndDate.AddDays(7) < new DateTime(weekEndDate.Year + 1, startDate.Month, startDate.Day); if (isAddExtraWeek) year.EndDate = year.EndDate.AddDays(7); yearList.Add(year); quarterNum = 1; // reset quarter of the year number monthNum = 1; // reset month of the year number; weekPeriodInt = 1; // reset week of the year number; } #endregion #region Create a new quarter if new week is in the new quarter var quarter = quarterList.LastOrDefault(); if (quarter == null || quarter.EndDate < weekEndDate) { quarter = new FiscalCalendar { Id = Guid.NewGuid(), Name = string.Format(Constants.FISCAL_QUARTER_NAME_TEMPLATE, quarterNum), Type = (int)FiscalCalendarModel.FiscalYearType.Quarter, YearInt = year.YearInt, QuarterInt = quarterNum, PeriodInt = quarterNum, StartDate = (prevWeek == null ) ? startDate : prevWeek.EndDate.AddDays(1), EndDate = ((prevWeek == null ) ? startDate : prevWeek.EndDate.AddDays(1)).AddDays(90),// add 7 days * 13 weeks in quarter = 91 days SystemName = string.Format(Constants.FISCAL_QUARTER_SYSTEMNAME_TEMPLATE, quarterNum, (weekEndDate.Year % 100).ToString("00")), AdjustingPeriod = false, NonWorking = 0 }; // if we can place a 14th week before 1st of January of next year then place it into the last quarter if (isAddExtraWeek && quarterNum == 4) quarter.EndDate = quarter.EndDate.AddDays(7); quarterList.Add(quarter); quarterNum++; } #endregion #region Create a new month if new week is in the new month var month = monthList.LastOrDefault(); if (month == null || month.EndDate < weekEndDate) { month = new FiscalCalendar { Id = Guid.NewGuid(), Name = string.Format(Constants.FISCAL_MONTH_NAME_TEMPLATE, weekEndDate.ToString("MMM").ToUpper()), Type = (int)FiscalCalendarModel.FiscalYearType.Month, YearInt = year.YearInt, QuarterInt = quarterNum - 1, PeriodInt = monthNum, StartDate = (prevWeek == null ) ? startDate : prevWeek.EndDate.AddDays(1), EndDate = ((prevWeek == null ) ? startDate : prevWeek.EndDate.AddDays(1)).AddDays(27),// add 7 days * 4 weeks in month = 28 days SystemName = string.Format(Constants.FISCAL_MONTH_SYSTEMNAME_TEMPLATE, weekEndDate.ToString("MMM").ToUpper(), (weekEndDate.Year % 100).ToString("00")), AdjustingPeriod = false, NonWorking = 0 }; // add 5th week to the 1st month of the quarter if (monthNum % 3 == 1) { month.EndDate = month.EndDate.AddDays(7); } // if we can place a 14th quarter week before 1st of January of next year then place it into the last quarter if (isAddExtraWeek && quarter.QuarterInt == 4 && month.PeriodInt == 12) month.EndDate = month.EndDate.AddDays(7); monthList.Add(month); monthNum++; monthWeekNum = 1; // reset week of the month number } #endregion // create a new week record var week = new FiscalCalendar { Id = Guid.NewGuid(), Name = string.Format(Constants.FISCAL_WEEK_NAME_TEMPLATE, weekEndDate.ToString("MMM").ToUpper(), monthWeekNum), Type = (int)FiscalCalendarModel.FiscalYearType.Week, YearInt = year.YearInt, QuarterInt = quarterNum - 1, PeriodInt = weekPeriodInt, StartDate = (prevWeek == null) ? startDate : prevWeek.EndDate.AddDays(1), EndDate = weekEndDate, SystemName = string.Format(Constants.FISCAL_WEEK_SYSTEMNAME_TEMPLATE, weekEndDate.ToString("MMM").ToUpper(), monthWeekNum, (weekEndDate.Year % 100).ToString("00")), AdjustingPeriod = false, NonWorking = 0 }; weeksList.Add(week); monthWeekNum++; weekPeriodInt++; prevWeek = week; weekEndDate = weekEndDate.AddDays(7); } #endregion #region Save generated fiscal periods to the DB foreach (var FiscalCalendar in yearList) { _dbContext.FiscalCalendars.Add(FiscalCalendar); } foreach (var obj in quarterList) { _dbContext.FiscalCalendars.Add(obj); } foreach (var obj in monthList) { _dbContext.FiscalCalendars.Add(obj); } foreach (var obj in weeksList) { _dbContext.FiscalCalendars.Add(obj); } #endregion } #endregion #region Holidays public Holiday LoadHoliday(Guid? value) { if (value == null || value == Guid.Empty) return new Holiday(); var holiday = _dbContext.Holidays.Find(value); if (holiday == null) return new Holiday(); return holiday; } public void SaveHoliday(HolidayModel model) { if (model == null) throw new ArgumentNullException("model"); #region Save Holiday data Holiday holiday = null; if (model.Id != Guid.Empty) holiday = _dbContext.Holidays.Find(model.Id); if (holiday == null) { holiday = new Holiday {Id = Guid.NewGuid()}; } model.CopyTo(holiday); if (model.Id == Guid.Empty) _dbContext.Holidays.Add(holiday); else _dbContext.Entry(holiday).State = EntityState.Modified; #endregion if (_isContexLocal) _dbContext.SaveChanges(); } #endregion public void Dispose() { if (_isContexLocal) _dbContext.Dispose(); } } }