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();
}
}
}