using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore.Storage; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Models; using System.Linq; using System; using Newtonsoft.Json.Linq; using System.Collections.Generic; namespace AyaNova.Biz { internal class PMBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject { internal PMBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles, UserType currentUserType) { ct = dbcontext; UserId = currentUserId; UserTranslationId = userTranslationId; CurrentUserRoles = UserRoles; BizType = AyaType.PM; CurrentUserType = currentUserType; //Sub-role rights flags UserIsTechRestricted = CurrentUserRoles.HasFlag(AuthorizationRoles.TechRestricted); UserIsSubContractorFull = CurrentUserType == UserType.ServiceContractor && CurrentUserRoles.HasFlag(AuthorizationRoles.SubContractor); UserIsSubContractorRestricted = CurrentUserType == UserType.ServiceContractor && CurrentUserRoles.HasFlag(AuthorizationRoles.SubContractorRestricted); UserIsRestrictedType = UserIsTechRestricted || UserIsSubContractorFull || UserIsSubContractorRestricted; UserCanViewPartCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.InventoryRestricted) || CurrentUserRoles.HasFlag(AuthorizationRoles.Inventory) || CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin) || CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting); UserCanViewLaborOrTravelRateCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.Service) || CurrentUserRoles.HasFlag(AuthorizationRoles.ServiceRestricted) || CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin) || CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting); UserCanViewLoanerCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.Service) || CurrentUserRoles.HasFlag(AuthorizationRoles.ServiceRestricted) || CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin) || CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting); } internal static PMBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) { if (httpContext != null) return new PMBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items), UserTypeFromContext.Type(httpContext.Items)); else return new PMBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin, UserType.NotService);//picked not service arbitrarily, probably a non-factor } private VizCache vc = new VizCache(); private ObjectCache oc = new ObjectCache(); /* ██████╗ ███╗ ███╗ ██╔══██╗████╗ ████║ ██████╔╝██╔████╔██║ ██╔═══╝ ██║╚██╔╝██║ ██║ ██║ ╚═╝ ██║ ╚═╝ ╚═╝ ╚═╝ */ #region PM level //////////////////////////////////////////////////////////////////////////////////////////////// // SUBRIGHTS / RESTRICTIONS FOR WORK ORDER // //Note: these restrictions and rights are in addition to the basic fundamental role access rights (layer 1) //and are considered after role rights have already been consulted first (usually at the controller level) internal UserType CurrentUserType { get; set; } internal bool UserIsRestrictedType { get; set; } internal bool UserIsTechRestricted { get; set; } internal bool UserIsSubContractorFull { get; set; } internal bool UserIsSubContractorRestricted { get; set; } internal bool UserCanViewPartCosts { get; set; } internal bool UserCanViewLaborOrTravelRateCosts { get; set; } internal bool UserCanViewLoanerCosts { get; set; } //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task PMExistsAsync(long id) { return await ct.PM.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task PMCreateAsync(PM newObject, bool populateViz = true) { using (var transaction = await ct.Database.BeginTransactionAsync()) { await PMValidateAsync(newObject, null); if (HasErrors) return null; else { await PMBizActionsAsync(AyaEvent.Created, newObject, null, null); newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.PM.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); await PMSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); //# NOTE: only internal code can post an entire quote graph, no external user can as controller will reject right up front //however, internally seeder will post entire workorders if (newObject.Items.Count > 0) { await GetCurrentContractFromContractIdAsync(newObject.ContractId); //GRANDCHILD BIZ ACTIONS foreach (PMItem wi in newObject.Items) { foreach (PMItemPart wip in wi.Parts) await PartBizActionsAsync(AyaEvent.Created, wip, null, null); foreach (PMItemLoan wil in wi.Loans) await LoanBizActionsAsync(AyaEvent.Created, wil, null, null); } await ct.SaveChangesAsync(); //NOTE: not running individual notification here for children, seeder won't require it and that's all that posts an entire wo currently } await transaction.CommitAsync(); if (populateViz) await PMPopulateVizFields(newObject, true, false); if (newObject.GenCopyAttachmentsFrom != null && !newObject.GenCopyAttachmentsFrom.IsEmpty) { //copy attachment from existing object await AttachmentBiz.DuplicateAttachments(newObject.GenCopyAttachmentsFrom, new AyaTypeId(AyaType.PM, newObject.Id), this.UserId, ct); newObject.GenCopyAttachmentsFrom = null;//so it doesn't get returned } await PMHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } } //needs to be fetched internally from several places for rule checking etc //this just gets it raw and lets others process private async Task PMGetFullAsync(long id) { //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections return await ct.PM.AsSplitQuery().AsNoTracking() .Include(w => w.Items.OrderBy(item => item.Sequence)) .ThenInclude(wi => wi.Expenses.OrderBy(z => z.Id)) .Include(w => w.Items) .ThenInclude(wi => wi.Labors.OrderBy(l => l.ServiceStartDate)) .Include(w => w.Items) .ThenInclude(wi => wi.Loans.OrderBy(z => z.OutDate)) .Include(w => w.Items) .ThenInclude(wi => wi.Parts.OrderBy(z => z.Id)) .Include(w => w.Items) .ThenInclude(wi => wi.ScheduledUsers.OrderBy(z => z.StartDate)) .Include(w => w.Items) .ThenInclude(wi => wi.Tasks.OrderBy(t => t.Sequence)) .Include(w => w.Items) .ThenInclude(wi => wi.Travels.OrderBy(z => z.TravelStartDate)) .Include(w => w.Items) .ThenInclude(wi => wi.Units.OrderBy(z => z.Id)) .Include(w => w.Items) .ThenInclude(wi => wi.OutsideServices.OrderBy(z => z.SentDate)) .SingleOrDefaultAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task PMGetAsync(long id, bool populateDisplayFields, bool logTheGetEvent = true, bool populateForReporting = false) { var ret = await PMGetFullAsync(id); if (ret != null) { var userIsTechRestricted = UserIsTechRestricted; var userIsSubContractorFull = UserIsSubContractorFull; var userIsSubContractorRestricted = UserIsSubContractorRestricted; var userIsRestricted = (userIsTechRestricted || userIsSubContractorFull || userIsSubContractorRestricted); if (userIsRestricted) { //Restricted users can only work with quote items they are scheduled on List removeItems = new List(); //gather list of items to remove by checking if they are scheduled on them or not foreach (PMItem wi in ret.Items) { var userIsSelfScheduledOnThisItem = false; foreach (PMItemScheduledUser su in wi.ScheduledUsers) { if (su.UserId == UserId) { userIsSelfScheduledOnThisItem = true; break; } } if (!userIsSelfScheduledOnThisItem) removeItems.Add(wi); } foreach (var removeitem in removeItems) { ret.Items.Remove(removeitem); ret.IsCompleteRecord = false; } //Restricted users may have further restrictions foreach (PMItem wi in ret.Items) { //all restricted types wi.ScheduledUsers.RemoveAll(x => x.UserId != UserId); wi.Labors.RemoveAll(x => x.UserId != UserId); wi.Travels.RemoveAll(x => x.UserId != UserId); if (userIsTechRestricted) { wi.Expenses.RemoveAll(x => x.UserId != UserId); } if (userIsSubContractorFull) { wi.Expenses.RemoveAll(x => true); wi.OutsideServices.RemoveAll(x => true); } if (userIsSubContractorRestricted) { wi.Units.RemoveAll(x => true); wi.Parts.RemoveAll(x => true); wi.Expenses.RemoveAll(x => true); wi.Loans.RemoveAll(x => true); wi.OutsideServices.RemoveAll(x => true); } //tasks are allowed to be viewed and update the task completion types } } if (populateDisplayFields) await PMPopulateVizFields(ret, false, populateForReporting); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct); } return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PMPutAsync(PM putObject) { //## PUT HEADER ONLY, NO ALLOWANCE FOR PUT OF ENTIRE WORKORDER //Note: this is intentionally not using the getasync because //doing so would invoke the children which would then get deleted on save since putobject has no children PM dbObject = await ct.PM.AsNoTracking().FirstOrDefaultAsync(z => z.Id == putObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } putObject.Tags = TagBiz.NormalizeTags(putObject.Tags); putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields); await PMValidateAsync(putObject, dbObject); if (HasErrors) return null; await PMBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null); long? newContractId = null; if (putObject.ContractId != dbObject.ContractId)//manual change of contract { newContractId = putObject.ContractId; await GetCurrentContractFromContractIdAsync(newContractId); } ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await PMExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); await PMSearchIndexAsync(putObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); await PMPopulateVizFields(putObject, true, false); await PMHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task PMDeleteAsync(long id) { using (var transaction = await ct.Database.BeginTransactionAsync()) { PM dbObject = await ct.PM.AsNoTracking().Where(z => z.Id == id).FirstOrDefaultAsync();// PMGetAsync(id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND); return false; } await PMValidateCanDelete(dbObject); if (HasErrors) return false; //collect the child id's to delete { var IDList = await ct.Review.AsNoTracking().Where(x => x.AType == AyaType.PM && x.ObjectId == id).Select(x => x.Id).ToListAsync(); if (IDList.Count() > 0) { ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles); foreach (long ItemId in IDList) if (!await b.DeleteAsync(ItemId, transaction)) { AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}"); return false; } } } var ItemIds = await ct.PMItem.AsNoTracking().Where(z => z.PMId == id).Select(z => z.Id).ToListAsync(); //Delete children foreach (long ItemId in ItemIds) if (!await ItemDeleteAsync(ItemId, transaction)) return false; ct.PM.Remove(dbObject); await ct.SaveChangesAsync(); await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, dbObject.Serial.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct); await transaction.CommitAsync(); await PMHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } } //////////////////////////////////////////////////////////////////////////////////////////////// //BIZ ACTIONS // // private async Task PMBizActionsAsync(AyaEvent ayaEvent, PM newObj, PM oldObj, IDbContextTransaction transaction) { //automatic actions on record change, called AFTER validation and BEFORE save //so changes here will be saved by caller //currently no processing required except for created or modified at this time if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified) return; //CREATED OR MODIFIED if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified) { //no db query required, just set regardless if anything relevant has changed or not as it's less //time consuming to do it than to do all the checks to see if it is relevant to do it or not SetGenerateDate(newObj); } //CREATION ACTIONS if (ayaEvent == AyaEvent.Created) { await AutoSetContractAsync(newObj); await AutoSetAddressAsync(newObj); } //MODIFIED ACTIONS if (ayaEvent == AyaEvent.Modified) { //if customer changed then contractId must be re-checked if (newObj.CustomerId != oldObj.CustomerId) { await AutoSetContractAsync(newObj); await AutoSetAddressAsync(newObj); } } } /// /// Calculate generate date based on service date and /// generate before span and unit /// internal static void SetGenerateDate(PM p) { p.GenerateDate = CalculateNewDateFromSpanAndUnit(p.NextServiceDate, p.GenerateBeforeUnit, -System.Math.Abs(p.GenerateBeforeInterval)); } internal static DateTime CalculateNewDateFromSpanAndUnit(DateTime StartDate, PMTimeUnit unit, int interval) { if (interval == 0) return StartDate; switch (unit) { case PMTimeUnit.Minutes: return StartDate.AddMinutes(interval); case PMTimeUnit.Hours: return StartDate.AddHours(interval); case PMTimeUnit.Days: return StartDate.AddDays(interval); case PMTimeUnit.Months: return StartDate.AddMonths(interval); case PMTimeUnit.Years: return StartDate.AddYears(interval); } //default return StartDate; } private async Task AutoSetAddressAsync(PM newObj) { if (!string.IsNullOrWhiteSpace(newObj.PostAddress) || !string.IsNullOrWhiteSpace(newObj.Address)) return; if (newObj.CustomerId == 0) return; var cust = await ct.Customer.AsNoTracking().Where(z => z.Id == newObj.CustomerId).FirstOrDefaultAsync(); if (cust == null) return; newObj.PostAddress = cust.PostAddress; newObj.PostCity = cust.PostCity; newObj.PostRegion = cust.PostRegion; newObj.PostCountry = cust.PostCountry; newObj.PostCode = cust.PostCode; newObj.Address = cust.Address; newObj.City = cust.City; newObj.Region = cust.Region; newObj.Country = cust.Country; newObj.AddressPostal = cust.AddressPostal; newObj.Latitude = cust.Latitude; newObj.Longitude = cust.Longitude; if (cust.BillHeadOffice == true && cust.HeadOfficeId != null) { var head = await ct.HeadOffice.AsNoTracking().Where(z => z.Id == cust.HeadOfficeId).FirstOrDefaultAsync(); if (head == null) return; newObj.PostAddress = head.PostAddress; newObj.PostCity = head.PostCity; newObj.PostRegion = head.PostRegion; newObj.PostCountry = head.PostCountry; newObj.PostCode = head.PostCode; } } private async Task AutoSetContractAsync(PM newObj) { //first reset contract fetched flag so a fresh copy is taken //in case it was set already by other operations mFetchedContractAlready = false; //CONTRACT AUTO SET //failsafe newObj.ContractId = null; if (newObj.CustomerId != 0) { //precedence: unit->customer->headoffice var cust = await ct.Customer.AsNoTracking().Where(z => z.Id == newObj.CustomerId).Select(z => new { headofficeId = z.HeadOfficeId, contractId = z.ContractId, contractExpires = z.ContractExpires }).FirstOrDefaultAsync(); //first set it to the customer one if available in case the ho one has expired then set the ho if applicable if (cust.contractId != null && cust.contractExpires > DateTime.UtcNow) newObj.ContractId = cust.contractId; else if (cust.headofficeId != null) { var head = await ct.HeadOffice.AsNoTracking().Where(z => z.Id == cust.headofficeId).Select(z => new { contractId = z.ContractId, contractExpires = z.ContractExpires }).FirstOrDefaultAsync(); if (head.contractId != null && head.contractExpires > DateTime.UtcNow) newObj.ContractId = head.contractId; } } } //////////////////////////////////////////////////////////////////////////////////////////////// //GET WORKORDER ID FROM DESCENDANT TYPE AND ID // internal static async Task GetPMIdFromRelativeAsync(AyaType ayaType, long id, AyContext ct) { ParentAndChildItemId w = new ParentAndChildItemId(); long itemid = 0; switch (ayaType) { case AyaType.PM: w.ParentId = id; w.ChildItemId = 0; return w; case AyaType.PMItem: itemid = id; break; case AyaType.PMItemExpense: itemid = await ct.PMItemExpense.AsNoTracking().Where(z => z.Id == id).Select(z => z.PMItemId).SingleOrDefaultAsync(); break; case AyaType.PMItemLabor: itemid = await ct.PMItemLabor.AsNoTracking().Where(z => z.Id == id).Select(z => z.PMItemId).SingleOrDefaultAsync(); break; case AyaType.PMItemLoan: itemid = await ct.PMItemLoan.AsNoTracking().Where(z => z.Id == id).Select(z => z.PMItemId).SingleOrDefaultAsync(); break; case AyaType.PMItemPart: itemid = await ct.PMItemPart.AsNoTracking().Where(z => z.Id == id).Select(z => z.PMItemId).SingleOrDefaultAsync(); break; case AyaType.PMItemScheduledUser: itemid = await ct.PMItemScheduledUser.AsNoTracking().Where(z => z.Id == id).Select(z => z.PMItemId).SingleOrDefaultAsync(); break; case AyaType.PMItemTask: itemid = await ct.PMItemTask.AsNoTracking().Where(z => z.Id == id).Select(z => z.PMItemId).SingleOrDefaultAsync(); break; case AyaType.PMItemTravel: itemid = await ct.PMItemTravel.AsNoTracking().Where(z => z.Id == id).Select(z => z.PMItemId).SingleOrDefaultAsync(); break; case AyaType.PMItemOutsideService: itemid = await ct.PMItemOutsideService.AsNoTracking().Where(z => z.Id == id).Select(z => z.PMItemId).SingleOrDefaultAsync(); break; case AyaType.PMItemUnit: itemid = await ct.PMItemUnit.AsNoTracking().Where(z => z.Id == id).Select(z => z.PMItemId).SingleOrDefaultAsync(); break; default: throw new System.NotSupportedException($"PMBiz::GetPMIdFromRelativeAsync -> AyaType {ayaType.ToString()} is not supported"); } w.ParentId = await ct.PMItem.AsNoTracking() .Where(z => z.Id == itemid) .Select(z => z.PMId) .SingleOrDefaultAsync(); w.ChildItemId = itemid; return w; } //////////////////////////////////////////////////////////////////////////////////////////////// //SEARCH // private async Task PMSearchIndexAsync(PM obj, bool isNew) { var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType); DigestSearchText(obj, SearchParams); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task GetSearchResultSummary(long id, AyaType specificType) { switch (specificType) { case AyaType.PM: var obj = await ct.PM.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);//# NOTE intentionally not calling quote get async here, don't need the whole graph var SearchParams = new Search.SearchIndexProcessObjectParameters(); DigestSearchText(obj, SearchParams); return SearchParams; case AyaType.PMItem: return await ItemGetSearchResultSummary(id); case AyaType.PMItemExpense: return await ExpenseGetSearchResultSummary(id); case AyaType.PMItemLabor: return await LaborGetSearchResultSummary(id); case AyaType.PMItemLoan: return await LoanGetSearchResultSummary(id); case AyaType.PMItemPart: return await PartGetSearchResultSummary(id); case AyaType.PMItemTask: return await TaskGetSearchResultSummary(id); case AyaType.PMItemTravel: return await TravelGetSearchResultSummary(id); case AyaType.PMItemOutsideService: return await OutsideServiceGetSearchResultSummary(id); case AyaType.PMItemUnit: return await UnitGetSearchResultSummary(id); default: return null; } } public void DigestSearchText(PM obj, Search.SearchIndexProcessObjectParameters searchParams) { if (obj != null) searchParams.AddText(obj.Notes) .AddText(obj.Serial) .AddText(obj.InternalReferenceNumber) .AddText(obj.CustomerReferenceNumber) .AddText(obj.CustomerContactName) .AddText(obj.PostAddress) .AddText(obj.PostCity) .AddText(obj.PostRegion) .AddText(obj.PostCountry) .AddText(obj.PostCode) .AddText(obj.Address) .AddText(obj.City) .AddText(obj.Region) .AddText(obj.Country) .AddText(obj.AddressPostal) .AddText(obj.Wiki) .AddText(obj.Tags) .AddCustomFields(obj.CustomFields); } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // //Can save or update? private async Task PMValidateAsync(PM proposedObj, PM currentObj) { //This may become necessary for v8migrate, leaving out for now //skip validation if seeding //if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; //Check restricted role preventing create if (isNew && UserIsRestrictedType) { AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return;//this is a completely disqualifying error } //Did they exclude *all* days of the week (int value 127) if ((int)proposedObj.ExcludeDaysOfWeek == 127) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ExcludeDaysOfWeek"); } //GenerateBefore MUST be less than Repeat Interval or bad things happen //normalizing to dates makes this easier var dtNow = DateTime.UtcNow; var dtGenBefore = CalculateNewDateFromSpanAndUnit(dtNow, proposedObj.GenerateBeforeUnit, proposedObj.GenerateBeforeInterval); var dtRepeat = CalculateNewDateFromSpanAndUnit(dtNow, proposedObj.RepeatUnit, proposedObj.RepeatInterval); if (!(dtGenBefore < dtRepeat) && proposedObj.Active == true)//INACTIVE OK DUE TO v8 MIGRATE { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "GenerateBeforeInterval", await Translate("ErrorGenBeforeTooSmall")); } var tsRepeatInterval = dtRepeat - dtNow; if (tsRepeatInterval.TotalSeconds < 3600)//One hour minimum repeat interval { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "RepeatInterval", await Translate("ErrorRepeatIntervalTooSmall")); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj); //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private async Task PMValidateCanDelete(PM dbObject) { //Check restricted role preventing create if (UserIsRestrictedType) { AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return;//this is a completely disqualifying error } //FOREIGN KEY CHECKS if (await ct.WorkOrder.AnyAsync(m => m.FromPMId == dbObject.Id)) AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("PM")); } //////////////////////////////////////////////////////////////////////////////////////////////// // GET PARTIAL WORKORDER FOR REPORTING // (returns quote consisting only of the path from child or grandchild up to header populated // with display data for reporting) // internal async Task PMGetPartialAsync(AyaType ayaType, long id, bool includeWoItemDescendants, bool populateForReporting) { //if it's the entire quote just get, populate and return as normal if (ayaType == AyaType.PM) return await PMGetAsync(id, true, false, populateForReporting); var wid = await GetPMIdFromRelativeAsync(ayaType, id, ct); //get header only var ret = await ct.PM.AsNoTracking().SingleOrDefaultAsync(x => x.Id == wid.ParentId); //not found don't bomb, just return null if (ret == null) return ret; //explicit load subitems as required... PMItem quoteitem = null; //it's requesting a fully populated woitem so do that here if (includeWoItemDescendants) { quoteitem = await ct.PMItem.AsSplitQuery() .AsNoTracking() .Include(wi => wi.Expenses) .Include(wi => wi.Labors) .Include(wi => wi.Loans) .Include(wi => wi.Parts) .Include(wi => wi.ScheduledUsers) .Include(wi => wi.Tasks) .Include(wi => wi.Travels) .Include(wi => wi.Units) .Include(wi => wi.OutsideServices) .SingleOrDefaultAsync(z => z.Id == wid.ChildItemId); } else { //get the single quote item required quoteitem = await ct.PMItem.AsNoTracking().SingleOrDefaultAsync(x => x.Id == wid.ChildItemId); switch (ayaType) { case AyaType.PMItemExpense: quoteitem.Expenses.Add(await ct.PMItemExpense.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.PMItemLabor: quoteitem.Labors.Add(await ct.PMItemLabor.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.PMItemLoan: quoteitem.Loans.Add(await ct.PMItemLoan.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.PMItemPart: quoteitem.Parts.Add(await ct.PMItemPart.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.PMItemScheduledUser: quoteitem.ScheduledUsers.Add(await ct.PMItemScheduledUser.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.PMItemTask: quoteitem.Tasks.Add(await ct.PMItemTask.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.PMItemTravel: quoteitem.Travels.Add(await ct.PMItemTravel.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.PMItemOutsideService: quoteitem.OutsideServices.Add(await ct.PMItemOutsideService.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.PMItemUnit: quoteitem.Units.Add(await ct.PMItemUnit.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; } } if (quoteitem != null) ret.Items.Add(quoteitem); await PMPopulateVizFields(ret, false, populateForReporting); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //REPORTING // public async Task GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId) { //quote reports for entire quote or just sub parts all go through here //if the ayatype is a descendant of the quote then only the portion of the quote from that descendant directly up to the header will be populated and returned //however if the report template has includeWoItemDescendants=true then the woitems is fully populated var idList = dataListSelectedRequest.SelectedRowIds; JArray ReportData = new JArray(); List batchResults = new List(); while (idList.Any()) { var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE); idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray(); batchResults.Clear(); foreach (long batchId in batch) { if (!ReportRenderManager.KeepGoing(jobId)) return null; batchResults.Add(await PMGetPartialAsync(dataListSelectedRequest.AType, batchId, dataListSelectedRequest.IncludeWoItemDescendants, true)); } foreach (PM w in batchResults) { if (!ReportRenderManager.KeepGoing(jobId)) return null; var jo = JObject.FromObject(w); //PM header custom fields if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"])) jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]); //PMItem custom fields foreach (JObject jItem in jo["Items"]) { if (!JsonUtil.JTokenIsNullOrEmpty(jItem["CustomFields"])) jItem["CustomFields"] = JObject.Parse((string)jItem["CustomFields"]); //PMItemUnit custom fields foreach (JObject jUnit in jItem["Units"]) { if (!JsonUtil.JTokenIsNullOrEmpty(jUnit["CustomFields"])) jUnit["CustomFields"] = JObject.Parse((string)jUnit["CustomFields"]); } } ReportData.Add(jo); } } vc.Clear(); oc.Clear(); return ReportData; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task PMPopulateVizFields(PM o, bool headerOnly, bool populateForReporting) { o.UserIsRestrictedType = UserIsRestrictedType; o.UserIsTechRestricted = UserIsTechRestricted; o.UserIsSubContractorFull = UserIsSubContractorFull; o.UserIsSubContractorRestricted = UserIsSubContractorRestricted; o.UserCanViewPartCosts = UserCanViewPartCosts; o.UserCanViewLaborOrTravelRateCosts = UserCanViewLaborOrTravelRateCosts; o.UserCanViewLoanerCosts = UserCanViewLoanerCosts; if (!headerOnly) { foreach (var v in o.Items) await ItemPopulateVizFields(v, populateForReporting); } //Alert notes //Customer notes first then others below { if (vc.Get("wocustname", o.CustomerId) == null)//will always be present so no need to check other values { var custInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => new { x.AlertNotes, x.TechNotes, x.Name, x.Phone1, x.Phone2, x.Phone3, x.Phone4, x.Phone5, x.EmailAddress }).FirstOrDefaultAsync(); vc.Add(custInfo.Name, "wocustname", o.CustomerId); if (!string.IsNullOrWhiteSpace(custInfo.AlertNotes)) { vc.Add($"{await Translate("Customer")} - {await Translate("AlertNotes")}\n{custInfo.AlertNotes}\n\n", "woalert", o.CustomerId); } else { vc.Add(string.Empty, "woalert", o.CustomerId); } if (!string.IsNullOrWhiteSpace(custInfo.TechNotes)) { vc.Add(custInfo.TechNotes, "custtechnotes", o.CustomerId); } vc.Add(custInfo.Phone1, "custphone1", o.CustomerId); vc.Add(custInfo.Phone2, "custphone2", o.CustomerId); vc.Add(custInfo.Phone3, "custphone3", o.CustomerId); vc.Add(custInfo.Phone4, "custphone4", o.CustomerId); vc.Add(custInfo.Phone5, "custphone5", o.CustomerId); vc.Add(custInfo.EmailAddress, "custemail", o.CustomerId); } o.CustomerViz = vc.Get("wocustname", o.CustomerId); o.AlertViz = vc.Get("woalert", o.CustomerId); o.CustomerTechNotesViz = vc.Get("custtechnotes", o.CustomerId); o.CustomerPhone1Viz = vc.Get("custphone1", o.CustomerId); o.CustomerPhone2Viz = vc.Get("custphone2", o.CustomerId); o.CustomerPhone3Viz = vc.Get("custphone3", o.CustomerId); o.CustomerPhone4Viz = vc.Get("custphone4", o.CustomerId); o.CustomerPhone5Viz = vc.Get("custphone5", o.CustomerId); o.CustomerEmailAddressViz = vc.Get("custemail", o.CustomerId); } if (o.ProjectId != null) { string value = vc.Get("projname", o.ProjectId); if (value == null) { value = await ct.Project.AsNoTracking().Where(x => x.Id == o.ProjectId).Select(x => x.Name).FirstOrDefaultAsync(); vc.Add(value, "projname", o.ProjectId); } o.ProjectViz = value; } if (o.ContractId != null) { if (vc.Get("ctrctname", o.ContractId) == null) { var contractVizFields = await ct.Contract.AsNoTracking().Where(x => x.Id == o.ContractId).Select(x => new { Name = x.Name, AlertNotes = x.AlertNotes }).FirstOrDefaultAsync(); vc.Add(contractVizFields.Name, "ctrctname", o.ContractId); vc.Add(contractVizFields.AlertNotes, "ctrctalrt", o.ContractId); } o.ContractViz = vc.Get("ctrctname", o.ContractId);//contractVizFields.Name; var alrtNotes = vc.Get("ctrctalrt", o.ContractId); if (!string.IsNullOrWhiteSpace(alrtNotes)) { o.AlertViz += $"{await Translate("Contract")}\n{alrtNotes}\n\n"; } } else o.ContractViz = "-"; } //////////////////////////////////////////////////////////////////////////////////////////////// // IMPORT EXPORT // public async Task GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId) { //for now just re-use the report data code //this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time return await GetReportData(dataListSelectedRequest, jobId); } //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // public async Task HandleJobAsync(OpsJob job) { switch (job.JobType) { case JobType.BatchCoreObjectOperation: await ProcessBatchJobAsync(job); break; default: throw new System.ArgumentOutOfRangeException($"PM.HandleJob-> Invalid job type{job.JobType.ToString()}"); } } private async Task ProcessBatchJobAsync(OpsJob job) { await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running); await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}"); List idList = new List(); long FailedObjectCount = 0; JObject jobData = JObject.Parse(job.JobInfo); if (jobData.ContainsKey("idList")) idList = ((JArray)jobData["idList"]).ToObject>(); else idList = await ct.PM.Select(z => z.Id).ToListAsync(); bool SaveIt = false; //--------------------------------- //case 4192 TimeSpan ProgressAndCancelCheckSpan = new TimeSpan(0, 0, ServerBootConfig.JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS); DateTime LastProgressCheck = DateTime.UtcNow.Subtract(new TimeSpan(1, 1, 1, 1, 1)); var TotalRecords = idList.LongCount(); long CurrentRecord = -1; //--------------------------------- foreach (long id in idList) { try { //-------------------------------- //case 4192 //Update progress / cancel requested? CurrentRecord++; if (DateUtil.IsAfterDuration(LastProgressCheck, ProgressAndCancelCheckSpan)) { await JobsBiz.UpdateJobProgressAsync(job.GId, $"{CurrentRecord}/{TotalRecords}"); if (await JobsBiz.GetJobStatusAsync(job.GId) == JobStatus.CancelRequested) break; LastProgressCheck = DateTime.UtcNow; } //--------------------------------- SaveIt = false; ClearErrors(); ICoreBizObjectModel o = null; //save a fetch if it's a delete if (job.SubType != JobSubType.Delete) o = await GetPMGraphItem(job.AType, id); switch (job.SubType) { case JobSubType.TagAddAny: case JobSubType.TagAdd: case JobSubType.TagRemoveAny: case JobSubType.TagRemove: case JobSubType.TagReplaceAny: case JobSubType.TagReplace: SaveIt = TagBiz.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType); break; case JobSubType.Delete: if (!await DeletePMGraphItem(job.AType, id)) { await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}"); FailedObjectCount++; } break; default: throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}"); } if (SaveIt) { o = await PutPMGraphItem(job.AType, o); if (o == null) { await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}"); FailedObjectCount++; } } //delay so we're not tying up all the resources in a tight loop await Task.Delay(AyaNova.Util.ServerBootConfig.JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY); } catch (Exception ex) { await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})"); await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex)); } } //--------------------------------- //case 4192 await JobsBiz.UpdateJobProgressAsync(job.GId, $"{++CurrentRecord}/{TotalRecords}"); //--------------------------------- await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}"); await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed); } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task PMHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PM oProposed = (PM)proposedObj; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);//Note: will properly handle all delete events and event removal if deleted //SPECIFIC EVENTS FOR THIS OBJECT PM oCurrent = null; bool SameTags = true; if (currentObj != null) { oCurrent = (PM)currentObj; SameTags = NotifyEventHelper.TwoObjectsHaveSameTags(proposedObj.Tags, currentObj.Tags); } #region STOP GENERATING DATE REACHED if (ayaEvent == AyaEvent.Created && oProposed.StopGeneratingDate != null) { //PMStopGeneratingDateReached Created here on workorder creation for any subscribers var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.PMStopGeneratingDateReached).ToListAsync(); foreach (var sub in subs) { //not for inactive users if (!await UserBiz.UserIsActive(sub.UserId)) continue; //Tag match? (will be true if no sub tags so always safe to call this) if (NotifyEventHelper.ObjectHasAllSubscriptionTags(proposedObj.Tags, sub.Tags)) { NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.PMStopGeneratingDateReached, UserId = sub.UserId, AyaType = proposedObj.AyaType, ObjectId = proposedObj.Id, NotifySubscriptionId = sub.Id, Name = oProposed.Serial.ToString(), EventDate = (DateTime)oProposed.StopGeneratingDate }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//StopGeneratingDate if (ayaEvent == AyaEvent.Modified) {// PMStopGeneratingDateReached modified in some way, could be tags, could be date either of which is relevant to this notification block //differences requiring re-processing of notification?? if (oProposed.StopGeneratingDate != oCurrent.StopGeneratingDate || !SameTags) { await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.PMStopGeneratingDateReached); //new has date? if (oProposed.StopGeneratingDate != null) { var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.PMStopGeneratingDateReached).ToListAsync(); foreach (var sub in subs) { //not for inactive users if (!await UserBiz.UserIsActive(sub.UserId)) continue; //Tag match? (will be true if no sub tags so always safe to call this) if (NotifyEventHelper.ObjectHasAllSubscriptionTags(proposedObj.Tags, sub.Tags)) { NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.PMStopGeneratingDateReached, UserId = sub.UserId, AyaType = proposedObj.AyaType, ObjectId = proposedObj.Id, NotifySubscriptionId = sub.Id, Name = oProposed.Serial.ToString(), EventDate = (DateTime)oProposed.StopGeneratingDate }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } } } }//StopGeneratingDate #endregion }//end of process notifications #endregion quote level /* ██╗████████╗███████╗███╗ ███╗███████╗ ██║╚══██╔══╝██╔════╝████╗ ████║██╔════╝ ██║ ██║ █████╗ ██╔████╔██║███████╗ ██║ ██║ ██╔══╝ ██║╚██╔╝██║╚════██║ ██║ ██║ ███████╗██║ ╚═╝ ██║███████║ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝ */ #region PMItem level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ItemExistsAsync(long id) { return await ct.PMItem.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task ItemCreateAsync(PMItem newObject) { await ItemValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.PMItem.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.PMItem, AyaEvent.Created), ct); await ItemSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await ItemPopulateVizFields(newObject, false); await ItemHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task ItemGetAsync(long id, bool logTheGetEvent = true) { //Restricted users can not fetch a woitem directly //arbitrary decision so don't have to put in all the cleanup code //because from our own UI they wouldn't fetch this anyway and //so this is only to cover api use by 3rd parties if (UserIsRestrictedType) { return null; } //Note: there could be rules checking here in future, i.e. can only get own quote or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.PMItem.AsSplitQuery().AsNoTracking() .Include(wi => wi.Expenses) .Include(wi => wi.Labors) .Include(wi => wi.Loans) .Include(wi => wi.Parts) .Include(wi => wi.ScheduledUsers) .Include(wi => wi.Tasks) .Include(wi => wi.Travels) .Include(wi => wi.Units) .Include(wi => wi.OutsideServices) .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.PMItem, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task ItemPutAsync(PMItem putObject) { //Note: this is intentionally not using the getasync because //doing so would also fetch the children which would then get deleted on save since putobject has no children var dbObject = await ct.PMItem.AsNoTracking().FirstOrDefaultAsync(z => z.Id == putObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } putObject.Tags = TagBiz.NormalizeTags(putObject.Tags); putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields); await ItemValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ItemExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, AyaType.PMItem, AyaEvent.Modified), ct); await ItemSearchIndexAsync(putObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); await ItemPopulateVizFields(putObject, false); await ItemHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task ItemDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await ct.PMItem.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND); return false; } ItemValidateCanDelete(dbObject); if (HasErrors) return false; //collect the child id's to delete var ExpenseIds = await ct.PMItemExpense.Where(z => z.PMItemId == id).Select(z => z.Id).ToListAsync(); var LaborIds = await ct.PMItemLabor.Where(z => z.PMItemId == id).Select(z => z.Id).ToListAsync(); var LoanIds = await ct.PMItemLoan.Where(z => z.PMItemId == id).Select(z => z.Id).ToListAsync(); var PartIds = await ct.PMItemPart.Where(z => z.PMItemId == id).Select(z => z.Id).ToListAsync(); var ScheduledUserIds = await ct.PMItemScheduledUser.Where(z => z.PMItemId == id).Select(z => z.Id).ToListAsync(); var TaskIds = await ct.PMItemTask.Where(z => z.PMItemId == id).Select(z => z.Id).ToListAsync(); var TravelIds = await ct.PMItemTravel.Where(z => z.PMItemId == id).Select(z => z.Id).ToListAsync(); var UnitIds = await ct.PMItemUnit.Where(z => z.PMItemId == id).Select(z => z.Id).ToListAsync(); var OutsideServiceIds = await ct.PMItemOutsideService.Where(z => z.PMItemId == id).Select(z => z.Id).ToListAsync(); //Delete children foreach (long ItemId in ExpenseIds) if (!await ExpenseDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in LaborIds) if (!await LaborDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in LoanIds) if (!await LoanDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in PartIds) if (!await PartDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in ScheduledUserIds) if (!await ScheduledUserDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in TaskIds) if (!await TaskDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in TravelIds) if (!await TravelDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in UnitIds) if (!await UnitDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in OutsideServiceIds) if (!await OutsideServiceDeleteAsync(ItemId, transaction)) return false; ct.PMItem.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "wo:" + dbObject.PMId.ToString(), ct);//FIX wo?? Not sure what is best here; revisit await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct); //all good do the commit if it's ours if (parentTransaction == null) await transaction.CommitAsync(); await ItemHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } private async Task ItemSearchIndexAsync(PMItem obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.PMItem); SearchParams.AddText(obj.Notes).AddText(obj.TechNotes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task ItemGetSearchResultSummary(long id) { var obj = await ct.PMItem.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);//# Note, intentionally not calling ItemGetAsync here as don't want whole graph var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.TechNotes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task ItemPopulateVizFields(PMItem o, bool populateForReporting) { if (o.WorkOrderItemStatusId != null) { string value = vc.Get("woistatname", o.WorkOrderItemStatusId); if (value == null) { var StatusInfo = await ct.WorkOrderItemStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.WorkOrderItemStatusId); vc.Add(StatusInfo.Name, "woistatname", o.WorkOrderItemStatusId); vc.Add(StatusInfo.Color, "woistatcolor", o.WorkOrderItemStatusId); o.WorkOrderItemStatusNameViz = StatusInfo.Name; o.WorkOrderItemStatusColorViz = StatusInfo.Color; } else { o.WorkOrderItemStatusNameViz = value; o.WorkOrderItemStatusColorViz = vc.Get("woistatcolor", o.WorkOrderItemStatusId); } } if (o.WorkOrderItemPriorityId != null) { string value = vc.Get("woipriorityname", o.WorkOrderItemPriorityId); if (value == null) { var PriorityInfo = await ct.WorkOrderItemPriority.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.WorkOrderItemPriorityId); vc.Add(PriorityInfo.Name, "woipriorityname", o.WorkOrderItemPriorityId); vc.Add(PriorityInfo.Color, "woiprioritycolor", o.WorkOrderItemPriorityId); o.WorkOrderItemPriorityNameViz = PriorityInfo.Name; o.WorkOrderItemPriorityColorViz = PriorityInfo.Color; } else { o.WorkOrderItemPriorityNameViz = value; o.WorkOrderItemPriorityColorViz = vc.Get("woiprioritycolor", o.WorkOrderItemPriorityId); } } foreach (var v in o.Expenses) await ExpensePopulateVizFields(v); foreach (var v in o.Labors) await LaborPopulateVizFields(v); foreach (var v in o.Loans) await LoanPopulateVizFields(v); foreach (var v in o.OutsideServices) await OutsideServicePopulateVizFields(v); foreach (var v in o.Parts) await PartPopulateVizFields(v); foreach (var v in o.ScheduledUsers) await ScheduledUserPopulateVizFields(v); foreach (var v in o.Tasks) await TaskPopulateVizFields(v); foreach (var v in o.Travels) await TravelPopulateVizFields(v); foreach (var v in o.Units) await UnitPopulateVizFields(v, populateForReporting); } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ItemValidateAsync(PMItem proposedObj, PMItem currentObj) { //run validation and biz rules bool isNew = currentObj == null; //does it have a valid id if (proposedObj.PMId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMId"); else if (!await PMExistsAsync(proposedObj.PMId)) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMId"); //Check restricted role preventing create if (isNew && UserIsRestrictedType) { AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return;//this is a completely disqualifying error } if (string.IsNullOrWhiteSpace(proposedObj.Notes)) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Notes"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields, "WorkOrderItemCustom"); } } private void ItemValidateCanDelete(PMItem obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //Check restricted role preventing create if (UserIsRestrictedType) { AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return;//this is a completely disqualifying error } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.PMItem)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task ItemHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PMItem oProposed = (PMItem)proposedObj; var qid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == qid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); //for notification purposes because has no name field itself if (WorkorderInfo != null) oProposed.Name = WorkorderInfo.Serial.ToString(); else oProposed.Name = "??"; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item level /* ███████╗██╗ ██╗██████╗ ███████╗███╗ ██╗███████╗███████╗███████╗ ██╔════╝╚██╗██╔╝██╔══██╗██╔════╝████╗ ██║██╔════╝██╔════╝██╔════╝ █████╗ ╚███╔╝ ██████╔╝█████╗ ██╔██╗ ██║███████╗█████╗ ███████╗ ██╔══╝ ██╔██╗ ██╔═══╝ ██╔══╝ ██║╚██╗██║╚════██║██╔══╝ ╚════██║ ███████╗██╔╝ ██╗██║ ███████╗██║ ╚████║███████║███████╗███████║ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚══════╝╚══════╝ */ #region PMItemExpense level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExpenseExistsAsync(long id) { return await ct.PMItemExpense.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task ExpenseCreateAsync(PMItemExpense newObject) { await ExpenseValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.PMItemExpense.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await ExpenseSearchIndexAsync(newObject, true); await ExpensePopulateVizFields(newObject); await ExpenseHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task ExpenseGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorFull || UserIsSubContractorRestricted) //no access allowed at all return null; var ret = await ct.PMItemExpense.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (UserIsTechRestricted && ret.UserId != UserId)//tech restricted can only see their own expenses { AddError(ApiErrorCode.NOT_AUTHORIZED); return null; } if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task ExpensePutAsync(PMItemExpense putObject) { var dbObject = await ExpenseGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await ExpenseValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ExpenseExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await ExpenseSearchIndexAsync(putObject, false); await ExpensePopulateVizFields(putObject); await ExpenseHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task ExpenseDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await ExpenseGetAsync(id, false); ExpenseValidateCanDelete(dbObject); if (HasErrors) return false; ct.PMItemExpense.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "pmitem:" + dbObject.PMItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await ExpenseHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task ExpenseSearchIndexAsync(PMItemExpense obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Name).AddText(obj.Description); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task ExpenseGetSearchResultSummary(long id) { var obj = await ExpenseGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Description).AddText(obj.Name); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task ExpensePopulateVizFields(PMItemExpense o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (o.UserId != null) { if (!vc.Has("user", o.UserId)) { vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId); } o.UserViz = vc.Get("user", o.UserId); } } TaxCode Tax = null; if (o.ChargeTaxCodeId != null) { if (!oc.Has("tax", o.ChargeTaxCodeId)) { Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.ChargeTaxCodeId); oc.Add(Tax, "tax", o.ChargeTaxCodeId); } else Tax = (TaxCode)oc.Get("tax", o.ChargeTaxCodeId); } if (Tax != null) o.TaxCodeViz = Tax.Name; //Calculate totals and taxes if (o.ChargeToCustomer) { o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.ChargeAmount * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.ChargeAmount + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.ChargeAmount * (Tax.TaxBPct / 100)); } } o.LineTotalViz = o.ChargeAmount + o.TaxAViz + o.TaxBViz; } else { o.LineTotalViz = o.ChargeAmount + o.TaxPaid; } } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ExpenseValidateAsync(PMItemExpense proposedObj, PMItemExpense currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsSubContractorFull || UserIsSubContractorRestricted) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId))) { //no edits allowed on other people's records AddError(ApiErrorCode.NOT_AUTHORIZED); return; } if (proposedObj.PMItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.PMItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMItemId"); return;//this is a completely disqualifying error } if (!isNew && UserIsTechRestricted) { //Existing record so just make sure they haven't changed the not changeable fields from the db version //Expenses: add (no user selection defaults to themselves), view, partial fields available // to edit or delete only where they are the selected user and only edit fields //Summary, Cost, Tax paid, Description //note that UI will prevent this, this rule is only backup for 3rd party api users if (currentObj.ChargeAmount != proposedObj.ChargeAmount) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeAmount"); //if (currentObj.TaxPaid != proposedObj.TaxPaid) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "TaxPaid"); if (currentObj.ChargeTaxCodeId != proposedObj.ChargeTaxCodeId) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeTaxCodeId"); if (currentObj.ReimburseUser != proposedObj.ReimburseUser) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ReimburseUser"); if (currentObj.ChargeToCustomer != proposedObj.ChargeToCustomer) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeToCustomer"); } if (isNew && UserIsTechRestricted) { //NEW record, they are not allowed to set several fields so make sure they are still at their defaults /* from client new expense record: concurrency: 0, description: null, name: null, totalCost: 0, chargeAmount: 0, taxPaid: 0, chargeTaxCodeId: null, taxCodeViz: null, reimburseUser: false, userId: null, userViz: null, chargeToCustomer: false, isDirty: true, workOrderItemId: this.value.items[this.activeWoItemIndex].id, uid: Date.now() //used for */ if (proposedObj.ChargeAmount != 0) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeAmount"); // if (proposedObj.TaxPaid != 0) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "TaxPaid"); if (proposedObj.ChargeTaxCodeId != null) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeTaxCodeId"); if (proposedObj.ReimburseUser != false) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ReimburseUser"); if (proposedObj.ChargeToCustomer != false) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeToCustomer"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields // CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void ExpenseValidateCanDelete(PMItemExpense obj) { if (UserIsSubContractorFull || UserIsSubContractorRestricted) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } if (UserIsTechRestricted && obj.UserId != UserId) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.PMItemExpense)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task ExpenseHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PMItemExpense oProposed = (PMItemExpense)proposedObj; var qid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMItemId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == qid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item EXPENSE level /* ██╗ █████╗ ██████╗ ██████╗ ██████╗ ██║ ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗ ██║ ███████║██████╔╝██║ ██║██████╔╝ ██║ ██╔══██║██╔══██╗██║ ██║██╔══██╗ ███████╗██║ ██║██████╔╝╚██████╔╝██║ ██║ ╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝ */ #region PMItemLabor level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task LaborExistsAsync(long id) { return await ct.PMItemLabor.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task LaborCreateAsync(PMItemLabor newObject) { await LaborValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.PMItemLabor.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await LaborSearchIndexAsync(newObject, true); await LaborPopulateVizFields(newObject); await LaborHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task LaborGetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.PMItemLabor.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (UserIsRestrictedType && ret.UserId != UserId) { AddError(ApiErrorCode.NOT_AUTHORIZED); return null; } if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task LaborPutAsync(PMItemLabor putObject) { var dbObject = await LaborGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await LaborValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await LaborExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await LaborSearchIndexAsync(putObject, false); await LaborPopulateVizFields(putObject); await LaborHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task LaborDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await LaborGetAsync(id, false); LaborValidateCanDelete(dbObject); if (HasErrors) return false; ct.PMItemLabor.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "pmitem:" + dbObject.PMItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await LaborHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task LaborSearchIndexAsync(PMItemLabor obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.ServiceDetails); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task LaborGetSearchResultSummary(long id) { var obj = await LaborGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.ServiceDetails); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task LaborPopulateVizFields(PMItemLabor o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (o.UserId != null) { if (!vc.Has("user", o.UserId)) { vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId); } o.UserViz = vc.Get("user", o.UserId); } } ServiceRate Rate = null; if (o.ServiceRateId != null) { if (!oc.Has("servicerate", o.ServiceRateId)) { Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.ServiceRateId); oc.Add(Rate, "servicerate", o.ServiceRateId); } else Rate = (ServiceRate)oc.Get("servicerate", o.ServiceRateId); o.ServiceRateViz = Rate.Name; } TaxCode Tax = null; if (o.TaxCodeSaleId != null) { if (!oc.Has("tax", o.TaxCodeSaleId)) { Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId); oc.Add(Tax, "tax", o.TaxCodeSaleId); } else Tax = (TaxCode)oc.Get("tax", o.TaxCodeSaleId); } if (Tax != null) o.TaxCodeViz = Tax.Name; o.PriceViz = 0; if (Rate != null) { o.CostViz = Rate.Cost; o.ListPriceViz = Rate.Charge; o.UnitOfMeasureViz = Rate.Unit; o.PriceViz = Rate.Charge;//default price used if not manual or contract override } //manual price overrides anything if (o.PriceOverride != null) o.PriceViz = (decimal)o.PriceOverride; else { //not manual so could potentially have a contract adjustment var c = await GetCurrentPMContractFromRelatedAsync(AyaType.PMItem, o.PMItemId); if (c != null) { decimal pct = 0; ContractOverrideType cot = ContractOverrideType.PriceDiscount; bool TaggedAdjustmentInEffect = false; //POTENTIAL CONTRACT ADJUSTMENTS //First check if there is a matching tagged service rate contract discount, that takes precedence if (c.ContractServiceRateOverrideItems.Count > 0 && Rate != null) { //Iterate all contract tagged items in order of ones with the most tags first foreach (var csr in c.ContractServiceRateOverrideItems.OrderByDescending(z => z.Tags.Count)) if (csr.Tags.All(z => Rate.Tags.Any(x => x == z))) { if (csr.OverridePct != 0) { pct = csr.OverridePct / 100; cot = csr.OverrideType; TaggedAdjustmentInEffect = true; } } } //Generic discount? if (!TaggedAdjustmentInEffect && c.ServiceRatesOverridePct != 0) { pct = c.ServiceRatesOverridePct / 100; cot = c.ServiceRatesOverrideType; } //apply if discount found if (pct != 0) { if (cot == ContractOverrideType.CostMarkup) o.PriceViz = MoneyUtil.Round(o.CostViz + (o.CostViz * pct)); else if (cot == ContractOverrideType.PriceDiscount) o.PriceViz = MoneyUtil.Round(o.ListPriceViz - (o.ListPriceViz * pct)); } } } //Calculate totals and taxes //NET o.NetViz = MoneyUtil.Round(o.PriceViz * o.ServiceRateQuantity); //TAX o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100)); } } } o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz; //RESTRICTIONS ON COST VISIBILITY? if (!UserCanViewLaborOrTravelRateCosts) { o.CostViz = 0; } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task LaborValidateAsync(PMItemLabor proposedObj, PMItemLabor currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.PMItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.PMItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMItemId"); return;//this is a completely disqualifying error } if (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId))) { //no edits allowed on other people's records AddError(ApiErrorCode.NOT_AUTHORIZED); return; } //Start date AND end date must both be null or both contain values if (proposedObj.ServiceStartDate == null && proposedObj.ServiceStopDate != null) AddError(ApiErrorCode.VALIDATION_REQUIRED, "ServiceStartDate"); if (proposedObj.ServiceStartDate != null && proposedObj.ServiceStopDate == null) AddError(ApiErrorCode.VALIDATION_REQUIRED, "ServiceStopDate"); //Start date before end date if (proposedObj.ServiceStartDate != null && proposedObj.ServiceStopDate != null) if (proposedObj.ServiceStartDate > proposedObj.ServiceStopDate) AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "ServiceStartDate"); //case 4171 removed // if (proposedObj.ServiceRateQuantity < 0)//negative quantities are not allowed // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ServiceRateQuantity"); //case 4171 removed // if (proposedObj.NoChargeQuantity < 0)//negative quantities are not allowed // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "NoChargeQuantity"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void LaborValidateCanDelete(PMItemLabor obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } if (UserIsRestrictedType) { //Labors: add (no user selection defaults to themselves), remove, view and edit only when they are the selected User if (obj.UserId != UserId) AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.PMItemLabor)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task LaborHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PMItemLabor oProposed = (PMItemLabor)proposedObj; var wid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMItemId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); //for notification purposes because has no name or tags field itself if (WorkorderInfo != null) oProposed.Name = WorkorderInfo.Serial.ToString(); else oProposed.Name = "??"; oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item LABOR level /* ██╗ ██████╗ █████╗ ███╗ ██╗ ██║ ██╔═══██╗██╔══██╗████╗ ██║ ██║ ██║ ██║███████║██╔██╗ ██║ ██║ ██║ ██║██╔══██║██║╚██╗██║ ███████╗╚██████╔╝██║ ██║██║ ╚████║ */ #region PMItemLoan level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task LoanExistsAsync(long id) { return await ct.PMItemLoan.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task LoanCreateAsync(PMItemLoan newObject) { await LoanValidateAsync(newObject, null); if (HasErrors) return null; else { await LoanBizActionsAsync(AyaEvent.Created, newObject, null, null); await ct.PMItemLoan.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await LoanSearchIndexAsync(newObject, true); await LoanPopulateVizFields(newObject); await LoanHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task LoanGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorRestricted) //no access allowed at all return null; var ret = await ct.PMItemLoan.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task LoanPutAsync(PMItemLoan putObject) { var dbObject = await LoanGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await LoanValidateAsync(putObject, dbObject); if (HasErrors) return null; await LoanBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null); ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await LoanExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await LoanSearchIndexAsync(putObject, false); await LoanPopulateVizFields(putObject); await LoanHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task LoanDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await LoanGetAsync(id, false); LoanValidateCanDelete(dbObject); if (HasErrors) return false; ct.PMItemLoan.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "pmitem:" + dbObject.PMItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await LoanHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task LoanSearchIndexAsync(PMItemLoan obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Notes); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task LoanGetSearchResultSummary(long id) { var obj = await LoanGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task LoanPopulateVizFields(PMItemLoan o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (loanUnitRateEnumList == null) loanUnitRateEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList( StringUtil.TrimTypeName(typeof(LoanUnitRateUnit).ToString()), UserTranslationId, CurrentUserRoles); o.UnitOfMeasureViz = loanUnitRateEnumList.Where(x => x.Id == (long)o.Rate).Select(x => x.Name).First(); } if (!vc.Has("loanunit", o.LoanUnitId)) vc.Add(await ct.LoanUnit.AsNoTracking().Where(x => x.Id == o.LoanUnitId).Select(x => x.Name).FirstOrDefaultAsync(), "loanunit", o.LoanUnitId); o.LoanUnitViz = vc.Get("loanunit", o.LoanUnitId); TaxCode Tax = null; if (o.TaxCodeId != null) { if (!oc.Has("tax", o.TaxCodeId)) { Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeId); oc.Add(Tax, "tax", o.TaxCodeId); } else Tax = (TaxCode)oc.Get("tax", o.TaxCodeId); } if (Tax != null) o.TaxCodeViz = Tax.Name; //manual price overrides anything o.PriceViz = o.ListPrice; if (o.PriceOverride != null) o.PriceViz = (decimal)o.PriceOverride; //Currently not contract discounted so no further calcs need apply to priceViz //Calculate totals and taxes //NET o.NetViz = MoneyUtil.Round(o.PriceViz * o.Quantity); //TAX o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100)); } } } o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz; //RESTRICTED COST FIELD?? if (!UserCanViewLoanerCosts) o.Cost = 0;//cost already used in calcs and will not be updated on any update operation so this ensures the cost isn't sent over the wire } private List loanUnitRateEnumList = null; //////////////////////////////////////////////////////////////////////////////////////////////// //BIZ ACTIONS // // private async Task LoanBizActionsAsync(AyaEvent ayaEvent, PMItemLoan newObj, PMItemLoan oldObj, IDbContextTransaction transaction) { //automatic actions on record change, called AFTER validation //currently no processing required except for created or modified at this time if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified) return; //SNAPSHOT PRICING bool SnapshotPricing = true; //if modifed, see what has changed and should be re-applied if (ayaEvent == AyaEvent.Modified) { //If it wasn't a complete part change there is no need to set pricing if (newObj.LoanUnitId == oldObj.LoanUnitId && newObj.Rate == oldObj.Rate) { SnapshotPricing = false; //maintain old cost as it can come from the client as zero when it shouldn't be or someone using the api and setting it directly //but we will only allow the price *we* set at the server initially newObj.Cost = oldObj.Cost; } } //Pricing if (SnapshotPricing) { //default in case nothing to apply newObj.Cost = 0; newObj.ListPrice = 0; LoanUnit loanUnit = await ct.LoanUnit.AsNoTracking().FirstOrDefaultAsync(x => x.Id == newObj.LoanUnitId); if (loanUnit != null) { switch (newObj.Rate) { case LoanUnitRateUnit.None: break; case LoanUnitRateUnit.Hours: newObj.Cost = loanUnit.RateHourCost; newObj.ListPrice = loanUnit.RateHour; break; case LoanUnitRateUnit.HalfDays: newObj.Cost = loanUnit.RateHalfDayCost; newObj.ListPrice = loanUnit.RateHalfDay; break; case LoanUnitRateUnit.Days: newObj.Cost = loanUnit.RateDayCost; newObj.ListPrice = loanUnit.RateDay; break; case LoanUnitRateUnit.Weeks: newObj.Cost = loanUnit.RateWeekCost; newObj.ListPrice = loanUnit.RateWeek; break; case LoanUnitRateUnit.Months: newObj.Cost = loanUnit.RateMonthCost; newObj.ListPrice = loanUnit.RateMonth; break; case LoanUnitRateUnit.Years: newObj.Cost = loanUnit.RateYearCost; newObj.ListPrice = loanUnit.RateYear; break; } } } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task LoanValidateAsync(PMItemLoan proposedObj, PMItemLoan currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsRestrictedType) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (proposedObj.PMItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.PMItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMItemId"); return;//this is a completely disqualifying error } if (proposedObj.LoanUnitId < 1 || !await ct.LoanUnit.AnyAsync(x => x.Id == proposedObj.LoanUnitId)) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "LoanUnitId"); //case 4171 removed // if (proposedObj.Quantity < 0)//negative quantities are not allowed // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Quantity"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void LoanValidateCanDelete(PMItemLoan obj) { if (UserIsRestrictedType) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.PMItemLoan)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task LoanHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PMItemLoan oProposed = (PMItemLoan)proposedObj; var wid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMItemId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); //for notification purposes because has no name / tags field itself oProposed.Name = WorkorderInfo.Serial.ToString(); oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item LOAN level /* ██████╗ ██╗ ██╗████████╗███████╗██╗██████╗ ███████╗ ███████╗███████╗██████╗ ██╗ ██╗██╗ ██████╗███████╗ ██╔═══██╗██║ ██║╚══██╔══╝██╔════╝██║██╔══██╗██╔════╝ ██╔════╝██╔════╝██╔══██╗██║ ██║██║██╔════╝██╔════╝ ██║ ██║██║ ██║ ██║ ███████╗██║██║ ██║█████╗ ███████╗█████╗ ██████╔╝██║ ██║██║██║ █████╗ ██║ ██║██║ ██║ ██║ ╚════██║██║██║ ██║██╔══╝ ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██║██║ ██╔══╝ ╚██████╔╝╚██████╔╝ ██║ ███████║██║██████╔╝███████╗ ███████║███████╗██║ ██║ ╚████╔╝ ██║╚██████╗███████╗ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝╚═════╝ ╚══════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝╚══════╝ */ #region PMItemOutsideService level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task OutsideServiceExistsAsync(long id) { return await ct.PMItemOutsideService.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task OutsideServiceCreateAsync(PMItemOutsideService newObject) { await OutsideServiceValidateAsync(newObject, null); if (HasErrors) return null; else { // newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); await ct.PMItemOutsideService.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await OutsideServiceSearchIndexAsync(newObject, true); await OutsideServicePopulateVizFields(newObject); await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task OutsideServiceGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorRestricted || UserIsSubContractorFull) //no access allowed at all return null; var ret = await ct.PMItemOutsideService.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.PMItemOutsideService, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task OutsideServicePutAsync(PMItemOutsideService putObject) { PMItemOutsideService dbObject = await OutsideServiceGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await OutsideServiceValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await OutsideServiceExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await OutsideServiceSearchIndexAsync(putObject, false); await OutsideServicePopulateVizFields(putObject); await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task OutsideServiceDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await OutsideServiceGetAsync(id, false); OutsideServiceValidateCanDelete(dbObject); if (HasErrors) return false; ct.PMItemOutsideService.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "pmitem:" + dbObject.PMItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task OutsideServiceSearchIndexAsync(PMItemOutsideService obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Notes).AddText(obj.RMANumber).AddText(obj.TrackingNumber); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task OutsideServiceGetSearchResultSummary(long id) { var obj = await OutsideServiceGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.RMANumber).AddText(obj.TrackingNumber); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task OutsideServicePopulateVizFields(PMItemOutsideService o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (o.UnitId != 0) { if (!vc.Has("unitserial", o.UnitId)) vc.Add(await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync(), "unitserial", o.UnitId); o.UnitViz = vc.Get("unitserial", o.UnitId); } if (o.VendorSentToId != null) { if (!vc.Has("vendorname", o.VendorSentToId)) vc.Add(await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentToId).Select(x => x.Name).FirstOrDefaultAsync(), "vendorname", o.VendorSentToId); o.VendorSentToViz = vc.Get("vendorname", o.VendorSentToId); } if (o.VendorSentViaId != null) { if (!vc.Has("vendorname", o.VendorSentViaId)) vc.Add(await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentViaId).Select(x => x.Name).FirstOrDefaultAsync(), "vendorname", o.VendorSentViaId); o.VendorSentViaViz = vc.Get("vendorname", o.VendorSentViaId); } } TaxCode Tax = null; if (o.TaxCodeId != null) { if (!oc.Has("tax", o.TaxCodeId)) { Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeId); oc.Add(Tax, "tax", o.TaxCodeId); } else Tax = (TaxCode)oc.Get("tax", o.TaxCodeId); } if (Tax != null) o.TaxCodeViz = Tax.Name; o.CostViz = o.ShippingCost + o.RepairCost; o.PriceViz = o.ShippingPrice + o.RepairPrice; //Currently not contract discounted so no further calcs need apply to priceViz //Calculate totals and taxes //NET o.NetViz = o.PriceViz;//just for standardization, no quantity so is redundant but reporting easier if all the same //TAX o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100)); } } } o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task OutsideServiceValidateAsync(PMItemOutsideService proposedObj, PMItemOutsideService currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsRestrictedType) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (proposedObj.PMItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.PMItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMItemId"); return;//this is a completely disqualifying error } if (proposedObj.UnitId < 1 || !await ct.Unit.AnyAsync(x => x.Id == proposedObj.UnitId)) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UnitId"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void OutsideServiceValidateCanDelete(PMItemOutsideService obj) { if (UserIsRestrictedType) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.PMItemOutsideService)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task OutsideServiceHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { /* OutsideServiceOverdue = 16,//* Workorder object , WorkOrderItemOutsideService created / updated, sets advance notice on due date tag filterable OutsideServiceReceived = 17,//* Workorder object , WorkOrderItemOutsideService updated, instant notification when item received, tag filterable */ ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PMItemOutsideService oProposed = (PMItemOutsideService)proposedObj; var wid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMItemId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); //for notification purposes because has no name / tags field itself oProposed.Name = WorkorderInfo.Serial.ToString(); oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT //## DELETED EVENTS //standard process above will remove any hanging around when deleted, nothing else specific here to deal with }//end of process notifications #endregion work order item OUTSIDE SERVICE level /* ██████╗ █████╗ ██████╗ ████████╗███████╗ ██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝ ██████╔╝███████║██████╔╝ ██║ ███████╗ ██╔═══╝ ██╔══██║██╔══██╗ ██║ ╚════██║ ██║ ██║ ██║██║ ██║ ██║ ███████║ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ */ #region PMItemPart level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task PartExistsAsync(long id) { return await ct.PMItemPart.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task CreatePartAsync(PMItemPart newObject) { using (var transaction = await ct.Database.BeginTransactionAsync()) { await PartValidateAsync(newObject, null); if (HasErrors) return null; else { await PartBizActionsAsync(AyaEvent.Created, newObject, null, null); await ct.PMItemPart.AddAsync(newObject); await ct.SaveChangesAsync(); if (HasErrors) { await transaction.RollbackAsync(); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await PartSearchIndexAsync(newObject, true); await transaction.CommitAsync(); await PartPopulateVizFields(newObject); await PartHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task PartGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorRestricted) //no access allowed at all return null; var ret = await ct.PMItemPart.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PartPutAsync(PMItemPart putObject) { using (var transaction = await ct.Database.BeginTransactionAsync()) { PMItemPart dbObject = await PartGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await PartValidateAsync(putObject, dbObject); if (HasErrors) return null; await PartBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null); ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); if (HasErrors) { await transaction.RollbackAsync(); return null; } } catch (DbUpdateConcurrencyException) { if (!await PartExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await PartSearchIndexAsync(putObject, false); await transaction.CommitAsync(); await PartPopulateVizFields(putObject); await PartHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task PartDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await PartGetAsync(id, false); PartValidateCanDelete(dbObject); if (HasErrors) return false; await PartBizActionsAsync(AyaEvent.Deleted, null, dbObject, transaction); ct.PMItemPart.Remove(dbObject); await ct.SaveChangesAsync(); if (HasErrors) { await transaction.RollbackAsync(); return false; } //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "pmitem:" + dbObject.PMItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await PartHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task PartSearchIndexAsync(PMItemPart obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Description).AddText(obj.Serials); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task PartGetSearchResultSummary(long id) { var obj = await PartGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Description).AddText(obj.Serials); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task PartPopulateVizFields(PMItemPart o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (o.PartWarehouseId != 0) { if (!vc.Has("partwarehouse", o.PartWarehouseId)) vc.Add(await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync(), "partwarehouse", o.PartWarehouseId); o.PartWarehouseViz = vc.Get("partwarehouse", o.PartWarehouseId); } } Part part = null; if (o.PartId != 0) { if (!oc.Has("part", o.PartId)) { part = await ct.Part.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.PartId); oc.Add(part, "part", o.PartId); } else part = (Part)oc.Get("part", o.PartId); } else return;//this should never happen but this is insurance in case it does o.PartDescriptionViz = part.Description; o.PartNameViz = part.Name; o.UpcViz = part.UPC; TaxCode Tax = null; if (o.TaxPartSaleId != null) { if (!oc.Has("tax", o.TaxPartSaleId)) { Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxPartSaleId); oc.Add(Tax, "tax", o.TaxPartSaleId); } else Tax = (TaxCode)oc.Get("tax", o.TaxPartSaleId); } if (Tax != null) o.TaxCodeViz = Tax.Name; o.PriceViz = 0; if (part != null) { //COST & PRICE NOT SET HERE, SET IN BIZACTIONS SNAPSHOTTED // o.CostViz = part.Cost; // o.ListPriceViz = part.Retail; o.UnitOfMeasureViz = part.UnitOfMeasure; o.PriceViz = o.ListPrice;//default price used if not manual or contract override } //manual price overrides anything if (o.PriceOverride != null) o.PriceViz = (decimal)o.PriceOverride; else { //not manual so could potentially have a contract adjustment var c = await GetCurrentPMContractFromRelatedAsync(AyaType.PMItem, o.PMItemId); if (c != null) { decimal pct = 0; ContractOverrideType cot = ContractOverrideType.PriceDiscount; bool TaggedAdjustmentInEffect = false; //POTENTIAL CONTRACT ADJUSTMENTS //First check if there is a matching tagged contract discount, that takes precedence if (c.ContractPartOverrideItems.Count > 0) { //Iterate all contract tagged items in order of ones with the most tags first foreach (var cp in c.ContractPartOverrideItems.OrderByDescending(z => z.Tags.Count)) if (cp.Tags.All(z => part.Tags.Any(x => x == z))) { if (cp.OverridePct != 0) { pct = cp.OverridePct / 100; cot = cp.OverrideType; TaggedAdjustmentInEffect = true; } } } //Generic discount? if (!TaggedAdjustmentInEffect && c.ServiceRatesOverridePct != 0) { pct = c.ServiceRatesOverridePct / 100; cot = c.ServiceRatesOverrideType; } //apply if discount found if (pct != 0) { if (cot == ContractOverrideType.CostMarkup) o.PriceViz = MoneyUtil.Round(o.Cost + (o.Cost * pct)); else if (cot == ContractOverrideType.PriceDiscount) o.PriceViz = MoneyUtil.Round(o.ListPrice - (o.ListPrice * pct)); } } } //Calculate totals and taxes //NET o.NetViz = MoneyUtil.Round(o.PriceViz * o.Quantity); //TAX o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100)); } } } o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz; //RESTRICTED COST FIELD?? if (!UserCanViewPartCosts) o.Cost = 0;//cost already used in calcs and will not be updated on any update operation so this ensures the cost isn't sent over the wire } //////////////////////////////////////////////////////////////////////////////////////////////// //BIZ ACTIONS // // private async Task PartBizActionsAsync(AyaEvent ayaEvent, PMItemPart newObj, PMItemPart oldObj, IDbContextTransaction transaction) { //SNAPSHOT PRICING IF NECESSARY if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified) return; //SNAPSHOT PRICING bool SnapshotPricing = true; //if modifed, see what has changed and should be re-applied if (ayaEvent == AyaEvent.Modified) { //If it wasn't a complete part change there is no need to set pricing if (newObj.PartId == oldObj.PartId) { SnapshotPricing = false; //maintain old cost as it can come from the client as zero when it shouldn't be or someone using the api and setting it directly //but we will only allow the price *we* set at the server initially newObj.Cost = oldObj.Cost; } } //Pricing if (SnapshotPricing) { //default in case nothing to apply newObj.Cost = 0; newObj.ListPrice = 0; var s = await ct.Part.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.PartId); if (s != null) { newObj.Cost = s.Cost; newObj.ListPrice = s.Retail; } } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task PartValidateAsync(PMItemPart proposedObj, PMItemPart currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsRestrictedType) { //Parts: no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (proposedObj.PMItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.PMItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMItemId"); return;//this is a completely disqualifying error } if (!await BizObjectExistsInDatabase.ExistsAsync(AyaType.Part, proposedObj.PartId, ct)) { AddError(ApiErrorCode.NOT_FOUND, "PartId"); return; } if (proposedObj.Quantity < 0)//negative quantities are not allowed AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Quantity"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void PartValidateCanDelete(PMItemPart obj) { if (UserIsRestrictedType) { //Parts: no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.PMItemPart)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task PartHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PMItemPart oProposed = (PMItemPart)proposedObj; var wid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMItemId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name / tags field itself oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item PARTS level /* ███████╗ ██████╗██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ ███████╗██████╗ ██╗ ██╗███████╗███████╗██████╗ ███████╗ ██╔════╝██╔════╝██║ ██║██╔════╝██╔══██╗██║ ██║██║ ██╔════╝██╔══██╗ ██║ ██║██╔════╝██╔════╝██╔══██╗██╔════╝ ███████╗██║ ███████║█████╗ ██║ ██║██║ ██║██║ █████╗ ██║ ██║█████╗██║ ██║███████╗█████╗ ██████╔╝███████╗ ╚════██║██║ ██╔══██║██╔══╝ ██║ ██║██║ ██║██║ ██╔══╝ ██║ ██║╚════╝██║ ██║╚════██║██╔══╝ ██╔══██╗╚════██║ ███████║╚██████╗██║ ██║███████╗██████╔╝╚██████╔╝███████╗███████╗██████╔╝ ╚██████╔╝███████║███████╗██║ ██║███████║ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝ */ #region PMItemScheduledUser level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ScheduledUserExistsAsync(long id) { return await ct.PMItemScheduledUser.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task ScheduledUserCreateAsync(PMItemScheduledUser newObject) { await ScheduledUserValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.PMItemScheduledUser.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await ScheduledUserPopulateVizFields(newObject); await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task ScheduledUserGetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.PMItemScheduledUser.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (UserIsRestrictedType && ret.UserId != UserId)//restricted users can only see their own return null; if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task ScheduledUserPutAsync(PMItemScheduledUser putObject) { PMItemScheduledUser dbObject = await ScheduledUserGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await ScheduledUserValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ScheduledUserExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await ScheduledUserPopulateVizFields(putObject); await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task ScheduledUserDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await ScheduledUserGetAsync(id, false); ScheduledUserValidateCanDelete(dbObject); if (HasErrors) return false; ct.PMItemScheduledUser.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "pmitem:" + dbObject.PMItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task ScheduledUserPopulateVizFields(PMItemScheduledUser o) { if (o.UserId != null) { if (!vc.Has("user", o.UserId)) { vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId); } o.UserViz = vc.Get("user", o.UserId); } if (o.ServiceRateId != null) { if (!vc.Has("servicerate", o.ServiceRateId)) { vc.Add(await ct.ServiceRate.AsNoTracking().Where(x => x.Id == o.ServiceRateId).Select(x => x.Name).FirstOrDefaultAsync(), "servicerate", o.ServiceRateId); } o.ServiceRateViz = vc.Get("servicerate", o.ServiceRateId); } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ScheduledUserValidateAsync(PMItemScheduledUser proposedObj, PMItemScheduledUser currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.PMItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.PMItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMItemId"); return;//this is a completely disqualifying error } if (UserIsRestrictedType) { //Scheduled Users: view only where they are the selected User and convert to labor record AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //case 4171 removed // if (proposedObj.EstimatedQuantity < 0)//negative quantities are not allowed // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "EstimatedQuantity"); //Start date AND end date must both be null or both contain values if (proposedObj.StartDate == null && proposedObj.StopDate != null) AddError(ApiErrorCode.VALIDATION_REQUIRED, "StopDate"); if (proposedObj.StartDate != null && proposedObj.StopDate == null) AddError(ApiErrorCode.VALIDATION_REQUIRED, "StopDate"); //Start date before end date if (proposedObj.StartDate != null && proposedObj.StopDate != null) if (proposedObj.StartDate > proposedObj.StopDate) AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "StartDate"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void ScheduledUserValidateCanDelete(PMItemScheduledUser obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } if (UserIsRestrictedType) { //Scheduled Users: view only where they are the selected User and convert to labor record AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.PMItemScheduledUser)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task ScheduledUserHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PMItemScheduledUser oProposed = (PMItemScheduledUser)proposedObj; var wid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMItemId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name field itself oProposed.Tags = WorkorderInfo.Tags; //for notification purposes because has no tag field itself //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item SCHEDULED USER level /* ████████╗ █████╗ ███████╗██╗ ██╗ ╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ ██║ ███████║███████╗█████╔╝ ██║ ██╔══██║╚════██║██╔═██╗ ██║ ██║ ██║███████║██║ ██╗ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ */ #region PMItemTask level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task TaskExistsAsync(long id) { return await ct.PMItemTask.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task TaskCreateAsync(PMItemTask newObject) { await TaskValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.PMItemTask.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await TaskSearchIndexAsync(newObject, true); await TaskPopulateVizFields(newObject); await TaskHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task TaskGetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.PMItemTask.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task TaskPutAsync(PMItemTask putObject) { PMItemTask dbObject = await TaskGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await TaskValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await TaskExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await TaskSearchIndexAsync(dbObject, false); await TaskPopulateVizFields(putObject); await TaskHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task TaskDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await TaskGetAsync(id, false); TaskValidateCanDelete(dbObject); if (HasErrors) return false; ct.PMItemTask.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "pmitem:" + dbObject.PMItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await TaskHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task TaskSearchIndexAsync(PMItemTask obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Task);//some are manually entered so this is worthwhile for that at least, also I guess predefined tasks that are more rare if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task TaskGetSearchResultSummary(long id) { var obj = await TaskGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Task); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task TaskPopulateVizFields(PMItemTask o) { if (o.CompletedByUserId != null) { if (!vc.Has("user", o.CompletedByUserId)) { vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.CompletedByUserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.CompletedByUserId); } o.CompletedByUserViz = vc.Get("user", o.CompletedByUserId); } if (taskCompletionTypeEnumList == null) taskCompletionTypeEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList( StringUtil.TrimTypeName(typeof(WorkOrderItemTaskCompletionType).ToString()), UserTranslationId, CurrentUserRoles); o.StatusViz = taskCompletionTypeEnumList.Where(x => x.Id == (long)o.Status).Select(x => x.Name).First(); } private List taskCompletionTypeEnumList = null; //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task TaskValidateAsync(PMItemTask proposedObj, PMItemTask currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.PMItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.PMItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMItemId"); return;//this is a completely disqualifying error } if (isNew && UserIsRestrictedType) { //restricted users are not allowed to make new task entries only fill them out AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (!isNew && UserIsRestrictedType) { //Existing record so just make sure they haven't changed the not changeable fields from the db version //* Tasks: view and edit existing tasks, set completion type and date only, no add or remove or changing other fields //note that UI will prevent this, this rule is only backup for 3rd party api users if (currentObj.Task != proposedObj.Task) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "Task"); if (currentObj.CompletedByUserId != UserId) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "CompletedByUserId"); if (currentObj.Sequence != proposedObj.Sequence) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "Sequence"); //they *are* permitted to change the date and status but only if they are the selected user: //note that UI will prevent this, this rule is only backup for 3rd party api users if (currentObj.CompletedDate != proposedObj.CompletedDate && currentObj.CompletedByUserId != UserId) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "CompletedDate"); if (currentObj.Status != proposedObj.Status && currentObj.CompletedByUserId != UserId) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "Status"); } if (string.IsNullOrWhiteSpace(proposedObj.Task)) AddError(ApiErrorCode.VALIDATION_REQUIRED, "Task"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors } } private void TaskValidateCanDelete(PMItemTask obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } if (UserIsRestrictedType) { //restricted users are not allowed to delete a task only fill them out AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.PMItemTask)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task TaskHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PMItemTask oProposed = (PMItemTask)proposedObj; var wid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMItemId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name / tags field itself oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item TASK level /* ████████╗██████╗ █████╗ ██╗ ██╗███████╗██╗ ╚══██╔══╝██╔══██╗██╔══██╗██║ ██║██╔════╝██║ ██║ ██████╔╝███████║██║ ██║█████╗ ██║ ██║ ██╔══██╗██╔══██║╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║ ██║██║ ██║ ╚████╔╝ ███████╗███████╗ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝ */ #region PMItemTravel level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task TravelExistsAsync(long id) { return await ct.PMItemTravel.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task TravelCreateAsync(PMItemTravel newObject) { await TravelValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.PMItemTravel.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await TravelSearchIndexAsync(newObject, true); await TravelPopulateVizFields(newObject); await TravelHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task TravelGetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.PMItemTravel.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (UserIsRestrictedType && ret.UserId != UserId) { AddError(ApiErrorCode.NOT_AUTHORIZED); return null; } if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task TravelPutAsync(PMItemTravel putObject) { PMItemTravel dbObject = await TravelGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await TravelValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await TravelExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await TravelSearchIndexAsync(putObject, false); await TravelPopulateVizFields(putObject); await TravelHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task TravelDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await TravelGetAsync(id, false); TravelValidateCanDelete(dbObject); if (HasErrors) return false; ct.PMItemTravel.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "pmitem:" + dbObject.PMItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await TravelHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task TravelSearchIndexAsync(PMItemTravel obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.TravelDetails); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task TravelGetSearchResultSummary(long id) { var obj = await TravelGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.TravelDetails); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task TravelPopulateVizFields(PMItemTravel o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (o.UserId != null) { if (!vc.Has("user", o.UserId)) { vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId); } o.UserViz = vc.Get("user", o.UserId); } } TravelRate Rate = null; if (o.TravelRateId != null) { if (!oc.Has("travelrate", o.TravelRateId)) { Rate = await ct.TravelRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.TravelRateId); oc.Add(Rate, "travelrate", o.TravelRateId); } else Rate = (TravelRate)oc.Get("travelrate", o.TravelRateId); o.TravelRateViz = Rate.Name; } TaxCode Tax = null; if (o.TaxCodeSaleId != null) { if (!oc.Has("tax", o.TaxCodeSaleId)) { Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId); oc.Add(Tax, "tax", o.TaxCodeSaleId); } else Tax = (TaxCode)oc.Get("tax", o.TaxCodeSaleId); } if (Tax != null) o.TaxCodeViz = Tax.Name; o.PriceViz = 0; if (Rate != null) { o.CostViz = Rate.Cost; o.ListPriceViz = Rate.Charge; o.UnitOfMeasureViz = Rate.Unit; o.PriceViz = Rate.Charge;//default price used if not manual or contract override } //manual price overrides anything if (o.PriceOverride != null) o.PriceViz = (decimal)o.PriceOverride; else { //not manual so could potentially have a contract adjustment var c = await GetCurrentPMContractFromRelatedAsync(AyaType.PMItem, o.PMItemId); if (c != null) { decimal pct = 0; ContractOverrideType cot = ContractOverrideType.PriceDiscount; bool TaggedAdjustmentInEffect = false; //POTENTIAL CONTRACT ADJUSTMENTS //First check if there is a matching tagged Travel rate contract discount, that takes precedence if (c.ContractTravelRateOverrideItems.Count > 0 && Rate != null) { //Iterate all contract tagged items in order of ones with the most tags first foreach (var csr in c.ContractTravelRateOverrideItems.OrderByDescending(z => z.Tags.Count)) if (csr.Tags.All(z => Rate.Tags.Any(x => x == z))) { if (csr.OverridePct != 0) { pct = csr.OverridePct / 100; cot = csr.OverrideType; TaggedAdjustmentInEffect = true; } } } //Generic discount? if (!TaggedAdjustmentInEffect && c.TravelRatesOverridePct != 0) { pct = c.TravelRatesOverridePct / 100; cot = c.TravelRatesOverrideType; } //apply if discount found if (pct != 0) { if (cot == ContractOverrideType.CostMarkup) o.PriceViz = MoneyUtil.Round(o.CostViz + (o.CostViz * pct)); else if (cot == ContractOverrideType.PriceDiscount) o.PriceViz = MoneyUtil.Round(o.ListPriceViz - (o.ListPriceViz * pct)); } } } //Calculate totals and taxes //NET o.NetViz = MoneyUtil.Round(o.PriceViz * o.TravelRateQuantity); //TAX o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100)); } } } o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz; //RESTRICTIONS ON COST VISIBILITY? if (!UserCanViewLaborOrTravelRateCosts) { o.CostViz = 0; } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task TravelValidateAsync(PMItemTravel proposedObj, PMItemTravel currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.PMItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.PMItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMItemId"); return;//this is a completely disqualifying error } if (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId))) { //no edits allowed on other people's records AddError(ApiErrorCode.NOT_AUTHORIZED); return; } //case 4171 removed // if (proposedObj.TravelRateQuantity < 0)//negative quantities are not allowed // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "TravelRateQuantity"); // if (proposedObj.NoChargeQuantity < 0)//negative quantities are not allowed // AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "NoChargeQuantity"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors } } private void TravelValidateCanDelete(PMItemTravel obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } if (UserIsRestrictedType) { //Travels: add (no user selection defaults to themselves), remove, view and edit only when they are the selected User if (obj.UserId != UserId) AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.PMItemTravel)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task TravelHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PMItemTravel oProposed = (PMItemTravel)proposedObj; var wid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMItemId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Name = WorkorderInfo.Serial.ToString();//for notification purposes because has no name / tags field itself oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item TRAVEL level /* ██╗ ██╗███╗ ██╗██╗████████╗ ██║ ██║████╗ ██║██║╚══██╔══╝ ██║ ██║██╔██╗ ██║██║ ██║ ██║ ██║██║╚██╗██║██║ ██║ ╚██████╔╝██║ ╚████║██║ ██║ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ */ #region PMItemUnit level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task UnitExistsAsync(long id) { return await ct.PMItemUnit.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task UnitCreateAsync(PMItemUnit newObject) { await UnitValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.PMItemUnit.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await UnitSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await UnitPopulateVizFields(newObject, false); await UnitHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task UnitGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorRestricted) //no access allowed at all return null; var ret = await ct.PMItemUnit.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task UnitPutAsync(PMItemUnit putObject) { PMItemUnit dbObject = await UnitGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); await UnitValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await UnitExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await UnitSearchIndexAsync(putObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); await UnitPopulateVizFields(putObject, false); await UnitHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task UnitDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await UnitGetAsync(id, false); UnitValidateCanDelete(dbObject); if (HasErrors) return false; ct.PMItemUnit.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "pmitem:" + dbObject.PMItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await UnitHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task UnitSearchIndexAsync(PMItemUnit obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task UnitGetSearchResultSummary(long id) { var obj = await UnitGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task UnitPopulateVizFields(PMItemUnit o, bool populateForReporting) { //see if it's in the cache already, populate the cache fully if not bool UnitHasModel = false; if (!vc.Has("unitserial", o.UnitId)) { //cache it var unitInfo = await ct.Unit.AsNoTracking() .Where(x => x.Id == o.UnitId) .Select(x => new { x.Serial, x.Description, x.UnitModelId, x.Address, x.City, x.Region, x.Country, x.Latitude, x.Longitude, x.Metered }) .FirstOrDefaultAsync(); vc.Add(unitInfo.Serial, "unitserial", o.UnitId); vc.Add(unitInfo.Description, "unitdesc", o.UnitId); vc.Add(unitInfo.Address, "unitaddr", o.UnitId); vc.Add(unitInfo.City, "unitcity", o.UnitId); vc.Add(unitInfo.Region, "unitregion", o.UnitId); vc.Add(unitInfo.Country, "unitcountry", o.UnitId); vc.Add(unitInfo.Latitude.ToString(), "unitlat", o.UnitId); vc.Add(unitInfo.Longitude.ToString(), "unitlong", o.UnitId); vc.Add(unitInfo.Metered.ToString(), "unitmetered", o.UnitId); if (unitInfo.UnitModelId != null) { UnitHasModel = true; //units model name cached? (if it is then the rest will be cached as well) if (!vc.Has("unitsmodelname", o.UnitId)) { //nope, model name cached?? if (!vc.Has("unitmodelname", unitInfo.UnitModelId)) { //nope, so cache it all var unitModelInfo = await ct.UnitModel.AsNoTracking().Where(x => x.Id == unitInfo.UnitModelId).Select(x => new { x.Name, x.VendorId }).FirstOrDefaultAsync(); vc.Add(unitModelInfo.Name, "unitmodelname", unitInfo.UnitModelId); vc.Add(unitModelInfo.Name, "unitsmodelname", o.UnitId); if (unitModelInfo.VendorId != null) { var ModelVendorName = vc.Get("unitsmodelvendorname", o.UnitId); if (ModelVendorName == null) { ModelVendorName = vc.Get("vendorname", unitModelInfo.VendorId); if (ModelVendorName == null) { ModelVendorName = await ct.Vendor.AsNoTracking().Where(x => x.Id == unitModelInfo.VendorId).Select(x => x.Name).FirstOrDefaultAsync(); vc.Add(ModelVendorName, "vendorname", unitModelInfo.VendorId); vc.Add(ModelVendorName, "unitsmodelvendorname", o.UnitId); } else { //cached under vendor so reuse here vc.Add(ModelVendorName, "unitsmodelvendorname", o.UnitId); } } } } } } } //populate all fields from cache if (UnitHasModel) { o.UnitModelNameViz = vc.Get("unitsmodelname", o.UnitId); o.UnitModelVendorViz = vc.Get("unitsmodelvendorname", o.UnitId); } o.UnitViz = vc.Get("unitserial", o.UnitId); o.UnitDescriptionViz = vc.Get("unitdesc", o.UnitId); //o.UnitMeteredViz = vc.GetAsBool("unitmetered", o.UnitId); if (populateForReporting) { o.AddressViz = vc.Get("unitaddr", o.UnitId); o.CityViz = vc.Get("unitcity", o.UnitId); o.RegionViz = vc.Get("unitregion", o.UnitId); o.CountryViz = vc.Get("unitcountry", o.UnitId); o.LatitudeViz = vc.GetAsDecimal("unitlat", o.UnitId); o.LongitudeViz = vc.GetAsDecimal("unitlong", o.UnitId); } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task UnitValidateAsync(PMItemUnit proposedObj, PMItemUnit currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsRestrictedType) { //Units: no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (proposedObj.PMItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.PMItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMItemId"); return;//this is a completely disqualifying error } if (proposedObj.UnitId < 1 || !await ct.Unit.AnyAsync(x => x.Id == proposedObj.UnitId)) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UnitId"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields,"WorkOrderItemUnitCustom"); } } private void UnitValidateCanDelete(PMItemUnit obj) { if (UserIsRestrictedType) { //Units: no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.PMItemUnit)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task UnitHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; PMItemUnit oProposed = (PMItemUnit)proposedObj; var wid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMItemId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Name = WorkorderInfo.Serial.ToString();//for notification purposes because has no name field itself //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item LABOR level #region Utility public async Task GetPMGraphItem(AyaType ayaType, long id) { switch (ayaType) { case AyaType.PM: return await PMGetAsync(id, false) as ICoreBizObjectModel; case AyaType.PMItem: return await ItemGetAsync(id, false); case AyaType.PMItemExpense: return await ExpenseGetAsync(id, false); case AyaType.PMItemLabor: return await LaborGetAsync(id, false); case AyaType.PMItemLoan: return await LoanGetAsync(id, false); case AyaType.PMItemPart: return await PartGetAsync(id, false); case AyaType.PMItemScheduledUser: return await ScheduledUserGetAsync(id, false); case AyaType.PMItemTask: return await TaskGetAsync(id, false); case AyaType.PMItemTravel: return await TravelGetAsync(id, false); case AyaType.PMItemUnit: return await UnitGetAsync(id, false); case AyaType.PMItemOutsideService: return await OutsideServiceGetAsync(id, false); default: throw new System.ArgumentOutOfRangeException($"PM::GetPMGraphItem -> Invalid ayaType{ayaType}"); } } public async Task PutPMGraphItem(AyaType ayaType, ICoreBizObjectModel o) { ClearErrors(); switch (ayaType) { case AyaType.PM: if (o is PM) { PM dto = new PM(); CopyObject.Copy(o, dto, "Name"); return await PMPutAsync((PM)dto); } return await PMPutAsync((PM)o) as ICoreBizObjectModel; case AyaType.PMItem: if (o is PMItem) { PMItem dto = new PMItem(); CopyObject.Copy(o, dto); return await ItemPutAsync((PMItem)dto); } return await ItemPutAsync((PMItem)o); case AyaType.PMItemExpense: return await ExpensePutAsync((PMItemExpense)o); case AyaType.PMItemLabor: return await LaborPutAsync((PMItemLabor)o); case AyaType.PMItemLoan: return await LoanPutAsync((PMItemLoan)o); case AyaType.PMItemPart: return await PartPutAsync((PMItemPart)o); case AyaType.PMItemScheduledUser: return await ScheduledUserPutAsync((PMItemScheduledUser)o); case AyaType.PMItemTask: return await TaskPutAsync((PMItemTask)o); case AyaType.PMItemTravel: return await TravelPutAsync((PMItemTravel)o); case AyaType.PMItemUnit: return await UnitPutAsync((PMItemUnit)o); case AyaType.PMItemOutsideService: return await OutsideServicePutAsync((PMItemOutsideService)o); default: throw new System.ArgumentOutOfRangeException($"PM::PutPMGraphItem -> Invalid ayaType{ayaType}"); } } public async Task DeletePMGraphItem(AyaType ayaType, long id) { switch (ayaType) { case AyaType.PM: return await PMDeleteAsync(id); case AyaType.PMItem: return await ItemDeleteAsync(id); case AyaType.PMItemExpense: return await ExpenseDeleteAsync(id); case AyaType.PMItemLabor: return await LaborDeleteAsync(id); case AyaType.PMItemLoan: return await LoanDeleteAsync(id); case AyaType.PMItemPart: return await PartDeleteAsync(id); case AyaType.PMItemScheduledUser: return await ScheduledUserDeleteAsync(id); case AyaType.PMItemTask: return await TaskDeleteAsync(id); case AyaType.PMItemTravel: return await TravelDeleteAsync(id); case AyaType.PMItemUnit: return await UnitDeleteAsync(id); case AyaType.PMItemOutsideService: return await OutsideServiceDeleteAsync(id); default: throw new System.ArgumentOutOfRangeException($"PM::GetPMGraphItem -> Invalid ayaType{ayaType}"); } } //////////////////////////////////////////////////////////////////////////////////////////////// //GET CONTRACT FOR WORKORDER FROM RELATIVE // //cache the contract to save repeatedly fetching it for this operation internal Contract mContractInEffect = null; internal bool mFetchedContractAlready = false;//null contract isn't enough to know it was fetched as it could just not have a contract so this is required internal async Task GetCurrentPMContractFromRelatedAsync(AyaType ayaType, long id) { if (mFetchedContractAlready == false) { var wid = await GetPMIdFromRelativeAsync(ayaType, id, ct); var WoContractId = await ct.PM.AsNoTracking().Where(z => z.Id == wid.ParentId).Select(z => z.ContractId).FirstOrDefaultAsync(); await GetCurrentContractFromContractIdAsync(WoContractId); } return mContractInEffect; } internal async Task GetCurrentContractFromContractIdAsync(long? id) { if (id == null) return null; if (mFetchedContractAlready == false) { mContractInEffect = await GetFullyPopulatedContractGraphFromIdAsync(id); } return mContractInEffect; } internal async Task GetFullyPopulatedContractGraphFromIdAsync(long? id) { if (id == null) return null; return await ct.Contract.AsSplitQuery().AsNoTracking() .Include(c => c.ServiceRateItems) .Include(c => c.TravelRateItems) .Include(c => c.ContractPartOverrideItems) .Include(c => c.ContractTravelRateOverrideItems) .Include(c => c.ContractServiceRateOverrideItems) .FirstOrDefaultAsync(z => z.Id == id); } #endregion utility #region GENERATION //////////////////////////////////////////////////////////////////////////////////////////////// // Process generation of pms to workorders // internal static async Task ProcessInsufficientInventoryNotificationAsync(AyContext ct, ILogger log) { //TODO: how to know when to stop, won't this just keep repeating?? //check log? Has it's own frequency unlike 12 hour thing?? //ideally sb once only, perhaps once only every 90 days or however long the log is kept for //quick check if *anyone* is subscribed to this early exit if not var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.PMInsufficientInventory).ToListAsync(); if (subs.Count == 0) return; foreach (var sub in subs) { //not for inactive users if (!await UserBiz.UserIsActive(sub.UserId)) continue; //ok, active user with active subscription //let's check for pm's within their range which is any pm's from NOW to NOW + AdvanceNotice var checkUpToDate = DateTime.UtcNow + sub.AdvanceNotice; //get the id's of pm's within the advance notification window for this user that are active var l = await ct.PM.AsNoTracking() .Where(z => z.GenerateDate < checkUpToDate && z.Active == true) .Select(z => z.Id) .ToListAsync(); if (l.Count > 0) log.LogDebug($"Found {l.Count} inventory checkable PM orders for subscription id {sub.Id}"); //Get the translations for this user List transl = new List(); transl.Add("PartName"); transl.Add("PartWarehouse"); transl.Add("QuantityRequired"); var Trans = await TranslationBiz.GetSubsetForUserStaticAsync(transl, sub.UserId); //process those pms foreach (long pmid in l) { log.LogDebug($"processing pm id {pmid}"); //look for same delivery already made and skip if already notified (sb one time only but will repeat for > 90 days as delivery log gets pruned) if (await ct.NotifyDeliveryLog.AnyAsync(z => z.NotifySubscriptionId == sub.Id && z.ObjectId == pmid)) { log.LogDebug($"PM {pmid} insufficient inventory already notified to subscriber within last 90 days, no need to send again, skipping"); continue; } //Ok, it's worth checking out and could be a potential notification //get the relevant bits of the PM var p = await ct.PM.AsSplitQuery() .Include(w => w.Items.OrderBy(item => item.Sequence)) .ThenInclude(wi => wi.Parts) .SingleOrDefaultAsync(z => z.Id == pmid); if (p == null) { //extremely unlikely to happen but just in case... log.LogError($"PM was not fetchable when attempting to process PM id: {pmid}, deleted during processing?"); continue; } //Tag match? (will be true if no sub tags so always safe to call this) if (!NotifyEventHelper.ObjectHasAllSubscriptionTags(p.Tags, sub.Tags)) { log.LogDebug($"PM tags don't match subscription required tags PM id: {pmid}, skipping"); continue; } //collect the parts on the pm List PartsOnPM = new List(); foreach (PMItem pmi in p.Items) { foreach (PMItemPart pmp in pmi.Parts) { if (pmp.Quantity > 0) { var i = new PMRestockListItem() { PartId = pmp.PartId, WarehouseId = pmp.PartWarehouseId, QuantityRequired = pmp.Quantity }; PartsOnPM.Add(i); } } } if (PartsOnPM.Count == 0) continue; //group parts and whs, summarize totals var SummarizedPartsOnPM = PartsOnPM.GroupBy(x => new { x.WarehouseId, x.PartId }) .OrderBy(g => g.Key.PartId) .ThenBy(g => g.Key.WarehouseId) .Select(cl => new PMRestockListItem() { PartId = cl.First().PartId, WarehouseId = cl.First().WarehouseId, QuantityRequired = cl.Sum(c => c.QuantityRequired) }); //ok, should have all summarized partid/warehouseid required combos, can now build output System.Text.StringBuilder sb = new System.Text.StringBuilder(); foreach (var i in SummarizedPartsOnPM) { //check inventory and add to sb if necessary var CurrentInventory = await ct.PartInventory.AsNoTracking().OrderByDescending(m => m.EntryDate).FirstOrDefaultAsync(m => m.PartId == i.PartId && m.PartWarehouseId == i.WarehouseId); decimal dBalance = 0; if (CurrentInventory != null) dBalance = CurrentInventory.Balance; if (dBalance < i.QuantityRequired) { var part = await ct.Part.AsNoTracking().Where(x => x.Id == i.PartId).Select(x => x.Name).FirstOrDefaultAsync(); var whs = await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == i.WarehouseId).Select(x => x.Name).FirstOrDefaultAsync(); var qty = (i.QuantityRequired - dBalance).ToString("G29", System.Globalization.CultureInfo.InvariantCulture); sb.Append($"{Trans["PartName"]}: {part}, {Trans["PartWarehouse"]}: {whs}, {Trans["QuantityRequired"]}: {qty}\n"); } } if (sb.Length > 0) { NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.PMInsufficientInventory, UserId = sub.UserId, AyaType = AyaType.PM, ObjectId = p.Id, NotifySubscriptionId = sub.Id, Name = p.Serial.ToString(), Message = sb.ToString() }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } }//each pmid }//each subscriber } public class PMRestockListItem { public long PartId { get; set; } public long WarehouseId { get; set; } public decimal QuantityRequired { get; set; } } internal static bool KeepOnWorking(ILogger log) { ApiServerState serverState = ServiceProviderProvider.ServerState; //system lock (no license) is a complete deal breaker for continuation beyond here if (serverState.IsSystemLocked) return false; if (serverState.IsMigrateMode) { log.LogInformation("Server is in migrate mode, skipping processing further PM's"); return false; } return true; } //////////////////////////////////////////////////////////////////////////////////////////////// // Process generation of pms to workorders // internal static async Task GenerateAsync(AyContext ct, ILogger log) { if (!KeepOnWorking(log)) return; //Get a list of PM id's ready for conversion now var l = await ct.PM.AsNoTracking() .Where(z => z.GenerateDate < DateTime.UtcNow && (z.StopGeneratingDate == null || z.StopGeneratingDate > DateTime.UtcNow) && z.Active == true) .Select(z => z.Id) .ToListAsync(); // #if (DEBUG) // if (l.Count > 0) // log.LogInformation($"Found {l.Count} ready to generate PM orders"); // #endif //process those pms foreach (long pmid in l) { if (!KeepOnWorking(log)) return; // #if (DEBUG) // log.LogInformation($"processing pm id {pmid}"); // #endif var p = await ct.PM.AsSplitQuery() .Include(w => w.Items.OrderBy(item => item.Sequence)) .ThenInclude(wi => wi.Expenses) .Include(w => w.Items) .ThenInclude(wi => wi.Labors) .Include(w => w.Items) .ThenInclude(wi => wi.Loans) .Include(w => w.Items) .ThenInclude(wi => wi.Parts) .Include(w => w.Items) .ThenInclude(wi => wi.ScheduledUsers) .Include(w => w.Items) .ThenInclude(wi => wi.Tasks.OrderBy(t => t.Sequence)) .Include(w => w.Items) .ThenInclude(wi => wi.Travels) .Include(w => w.Items) .ThenInclude(wi => wi.Units) .Include(w => w.Items) .ThenInclude(wi => wi.OutsideServices) .SingleOrDefaultAsync(z => z.Id == pmid); if (p == null) { //extremely unlikely to happen but just in case... log.LogError($"PM was not fetchable when attempting to process PM id: {pmid}, deleted during processing?"); if (!KeepOnWorking(log)) return; continue; } //confirm customer is active before proceeding if (!await ct.Customer.AsNoTracking().Where(x => x.Id == p.CustomerId).Select(x => x.Active).FirstOrDefaultAsync()) { log.LogDebug($"PM {p.Serial} has an Inactive customer selected so it will be skipped for generation"); continue; } try { //make new workorder if (await NewServiceWorkOrderFromPMAsync(p, ct, log)) { //Success //Calculate next service date DateTime NewNextServiceDate = CalculateNewDateFromSpanAndUnit(p.NextServiceDate, p.RepeatUnit, p.RepeatInterval); //Check Exclusions and adjust if ((int)p.ExcludeDaysOfWeek != 0) { //days of week cant be used as flags hence our own var excluded = new List(); if (p.ExcludeDaysOfWeek.HasFlag(AyaDaysOfWeek.Monday)) excluded.Add(DayOfWeek.Monday); if (p.ExcludeDaysOfWeek.HasFlag(AyaDaysOfWeek.Tuesday)) excluded.Add(DayOfWeek.Tuesday); if (p.ExcludeDaysOfWeek.HasFlag(AyaDaysOfWeek.Wednesday)) excluded.Add(DayOfWeek.Wednesday); if (p.ExcludeDaysOfWeek.HasFlag(AyaDaysOfWeek.Thursday)) excluded.Add(DayOfWeek.Thursday); if (p.ExcludeDaysOfWeek.HasFlag(AyaDaysOfWeek.Friday)) excluded.Add(DayOfWeek.Friday); if (p.ExcludeDaysOfWeek.HasFlag(AyaDaysOfWeek.Saturday)) excluded.Add(DayOfWeek.Saturday); if (p.ExcludeDaysOfWeek.HasFlag(AyaDaysOfWeek.Sunday)) excluded.Add(DayOfWeek.Sunday); while (excluded.Contains(NewNextServiceDate.DayOfWeek)) NewNextServiceDate = NewNextServiceDate.AddDays(1); } TimeSpan tsAdd = NewNextServiceDate - p.NextServiceDate; //Stop generating date reached?? if (p.StopGeneratingDate != null && p.StopGeneratingDate < NewNextServiceDate) { p.Active = false; await ct.SaveChangesAsync(); log.LogDebug($"PM {p.Serial} has reached it's stop generating date and has been automatically deactivated"); continue; } //Re-schedule PM p.NextServiceDate = NewNextServiceDate; SetGenerateDate(p); foreach (PMItem pmi in p.Items) { pmi.RequestDate = addts(pmi.RequestDate, tsAdd); foreach (PMItemScheduledUser pmsu in pmi.ScheduledUsers) { pmsu.StartDate = addts(pmsu.StartDate, tsAdd); pmsu.StopDate = addts(pmsu.StopDate, tsAdd); } foreach (PMItemLoan pml in pmi.Loans) { pml.DueDate = addts(pml.DueDate, tsAdd); pml.OutDate = addts(pml.OutDate, tsAdd); pml.ReturnDate = addts(pml.ReturnDate, tsAdd); } foreach (PMItemLabor pmlab in pmi.Labors) { pmlab.ServiceStartDate = addts(pmlab.ServiceStartDate, tsAdd); pmlab.ServiceStopDate = addts(pmlab.ServiceStopDate, tsAdd); } foreach (PMItemTravel pmtrav in pmi.Travels) { pmtrav.TravelStartDate = addts(pmtrav.TravelStartDate, tsAdd); pmtrav.TravelStopDate = addts(pmtrav.TravelStopDate, tsAdd); } foreach (PMItemTask pmt in pmi.Tasks) pmt.CompletedDate = addts(pmt.CompletedDate, tsAdd); foreach (PMItemOutsideService pmo in pmi.OutsideServices) { pmo.SentDate = addts(pmo.SentDate, tsAdd); pmo.ReturnDate = addts(pmo.ReturnDate, tsAdd); pmo.ETADate = addts(pmo.ETADate, tsAdd); } } try { await ct.SaveChangesAsync(); // #if (DEBUG) // log.LogInformation($"updated PM after successful generation {p.Serial}"); // #endif } catch (Exception ex) { log.LogError(ex, $"error updating PM after generation {p.Serial}"); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error updating PM after generation {p.Serial}", "Preventive Maintenance", ex); if (!KeepOnWorking(log)) return; continue; } } } catch (Exception ex) { log.LogError(ex, $"error generating Work order from PM {p.Serial}"); DbUtil.HandleIfDatabaseUnavailableTypeException(ex); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating Work order from PM {p.Serial}", "Preventive Maintenance", ex); if (!KeepOnWorking(log)) return; continue; } } } private static DateTime? addts(DateTime? dt, TimeSpan ts) { if (dt == null) return null; return ((DateTime)dt).Add(ts); } internal static async Task NewServiceWorkOrderFromPMAsync(PM p, AyContext ct, ILogger log) { WorkOrderBiz biz = WorkOrderBiz.GetBiz(ct); WorkOrder wo = new WorkOrder(); wo.Address = p.Address; wo.City = p.City; //o.CompleteByDate=?? wo.ContractId = p.ContractId; wo.Country = p.Country; wo.AddressPostal = p.AddressPostal; wo.CreatedDate = DateTime.UtcNow; wo.CustomerContactName = p.CustomerContactName; wo.CustomerId = p.CustomerId; wo.CustomerReferenceNumber = p.CustomerReferenceNumber; wo.CustomFields = p.CustomFields; wo.FromPMId = p.Id; wo.InternalReferenceNumber = p.InternalReferenceNumber; wo.Latitude = p.Latitude; wo.Longitude = p.Longitude; wo.Notes = p.Notes; wo.Onsite = p.Onsite; wo.PostAddress = p.PostAddress; wo.PostCity = p.PostCity; wo.PostCode = p.PostCode; wo.PostCountry = p.PostCountry; wo.PostRegion = p.PostRegion; wo.ProjectId = p.ProjectId; wo.Region = p.Region; wo.ServiceDate = p.NextServiceDate;//DATE ADJUST wo.Tags = p.Tags; if (p.CopyWiki) wo.Wiki = p.Wiki; if (p.CopyAttachments) wo.GenCopyAttachmentsFrom = new AyaTypeId(AyaType.PM, p.Id); var NewWoHeader = await biz.WorkOrderCreateAsync(wo, false); if (NewWoHeader == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating Work order from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } foreach (PMItem pmi in p.Items) { var woi = new WorkOrderItem(); woi.WorkOrderId = NewWoHeader.Id; woi.Notes = pmi.Notes; woi.RequestDate = pmi.RequestDate;//DATE ADJUST woi.Sequence = pmi.Sequence; woi.Tags = pmi.Tags; woi.TechNotes = pmi.TechNotes; woi.WarrantyService = pmi.WarrantyService; if (p.CopyWiki) woi.Wiki = pmi.Wiki; woi.WorkOrderItemPriorityId = pmi.WorkOrderItemPriorityId; woi.WorkOrderItemStatusId = pmi.WorkOrderItemStatusId; var NewWoItem = await biz.ItemCreateAsync(woi); if (NewWoItem == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating ITEM from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating Work order from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } foreach (PMItemUnit pmiunit in pmi.Units) { var woiunit = new WorkOrderItemUnit(); woiunit.WorkOrderItemId = NewWoItem.Id; woiunit.CustomFields = pmiunit.CustomFields; woiunit.Notes = pmiunit.Notes; woiunit.Tags = pmiunit.Tags; woiunit.UnitId = pmiunit.UnitId; if (p.CopyWiki) woiunit.Wiki = pmiunit.Wiki; //woi.Units.Add(woiunit); if (await biz.UnitCreateAsync(woiunit) == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating WOITEMUNIT from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating WOITEMUNIT from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } } foreach (PMItemScheduledUser pmsu in pmi.ScheduledUsers) { var wois = new WorkOrderItemScheduledUser(); wois.WorkOrderItemId = NewWoItem.Id; wois.ServiceRateId = pmsu.ServiceRateId; wois.StartDate = pmsu.StartDate;//DATE ADJUST wois.StopDate = pmsu.StopDate;//DATE ADJUST wois.Tags = pmsu.Tags; wois.UserId = pmsu.UserId; wois.IsPMGenerated = true;//signifies to ignore schedule conflicts //woi.ScheduledUsers.Add(wois); if (await biz.ScheduledUserCreateAsync(wois) == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating WOITEMSCHEDUSER from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating WOITEMSCHEDUSER from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } } foreach (PMItemPart pmp in pmi.Parts) { //Check inventory, if insufficient, create part request for balance and make a part record for the available amount even if it's zero as a placeholder //Exception to this: if this part/warehouse combo was already requested anywhere on this work order then we know it's already out of stock so just add it all //as a part request //Already requested part/warehouse combo? (means already out of stock effectively) bool AlreadyRequestedOnThisWorkOrder = false; foreach (var inventCheckItem in wo.Items) { foreach (var inventRequest in inventCheckItem.PartRequests) { if (inventRequest.PartId == pmp.PartId && inventRequest.PartWarehouseId == pmp.PartWarehouseId) { AlreadyRequestedOnThisWorkOrder = true; break; } } if (AlreadyRequestedOnThisWorkOrder) break; } decimal wipQuantity = pmp.Quantity; decimal requestQuantity = 0; if (AlreadyRequestedOnThisWorkOrder) { wipQuantity = 0; requestQuantity = pmp.Quantity; } else { //not already requested, so check inventory, this is new var CurrentInventory = await ct.PartInventory.AsNoTracking().OrderByDescending(m => m.EntryDate).FirstOrDefaultAsync(m => m.PartId == pmp.PartId && m.PartWarehouseId == pmp.PartWarehouseId); decimal dBalance = 0; if (CurrentInventory != null)//can be null if it has no opening balance for that warehouse yet dBalance = CurrentInventory.Balance; if (dBalance < pmp.Quantity) { //we will need a part request here and also need to reserve what there is available wipQuantity = dBalance; requestQuantity = pmp.Quantity - wipQuantity; } } //Add request if necessary if (requestQuantity != 0) { var wipr = new WorkOrderItemPartRequest(); wipr.WorkOrderItemId = NewWoItem.Id; wipr.PartId = pmp.PartId; wipr.PartWarehouseId = pmp.PartWarehouseId; wipr.Quantity = requestQuantity; //woi.PartRequests.Add(wipr); if (await biz.PartRequestCreateAsync(wipr) == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating WorkOrderItemPartRequest from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating WorkOrderItemPartRequest from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } } //Add part var wip = new WorkOrderItemPart(); wip.WorkOrderItemId = NewWoItem.Id; wip.Description = pmp.Description; wip.PartId = pmp.PartId; wip.PartWarehouseId = pmp.PartWarehouseId; wip.PriceOverride = pmp.PriceOverride; wip.Quantity = wipQuantity; wip.Tags = pmp.Tags; wip.TaxPartSaleId = pmp.TaxPartSaleId; //woi.Parts.Add(wip); if (await biz.PartCreateAsync(wip) == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating WorkOrderItemPart from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating WorkOrderItemPart from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } } foreach (PMItemLoan pml in pmi.Loans) { var wil = new WorkOrderItemLoan(); wil.WorkOrderItemId = NewWoItem.Id; wil.LoanUnitId = pml.LoanUnitId; wil.Notes = pml.Notes; wil.PriceOverride = pml.PriceOverride; wil.Quantity = pml.Quantity; wil.Rate = pml.Rate; wil.Tags = pml.Tags; wil.TaxCodeId = pml.TaxCodeId; wil.DueDate = pml.DueDate;//DATE ADJUST wil.OutDate = pml.OutDate;//DATE ADJUST wil.ReturnDate = pml.ReturnDate;//DATE ADJUST //woi.Loans.Add(wil); if (await biz.LoanCreateAsync(wil) == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating WorkOrderItemLoan from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating WorkOrderItemLoan from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } } foreach (PMItemLabor pmlab in pmi.Labors) { var wilab = new WorkOrderItemLabor(); wilab.WorkOrderItemId = NewWoItem.Id; wilab.NoChargeQuantity = pmlab.NoChargeQuantity; wilab.PriceOverride = pmlab.PriceOverride; wilab.ServiceDetails = pmlab.ServiceDetails; wilab.ServiceRateId = pmlab.ServiceRateId; wilab.ServiceRateQuantity = pmlab.ServiceRateQuantity; wilab.Tags = pmlab.Tags; wilab.TaxCodeSaleId = pmlab.TaxCodeSaleId; wilab.UserId = pmlab.UserId; wilab.ServiceStartDate = pmlab.ServiceStartDate;//DATE ADJUST wilab.ServiceStopDate = pmlab.ServiceStopDate;//DATE ADJUST //woi.Labors.Add(wilab); if (await biz.LaborCreateAsync(wilab) == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating WorkOrderItemLabor from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating WorkOrderItemLabor from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } } foreach (PMItemTravel pmtrav in pmi.Travels) { var witrav = new WorkOrderItemTravel(); witrav.WorkOrderItemId = NewWoItem.Id; witrav.Distance = pmtrav.Distance; witrav.NoChargeQuantity = pmtrav.NoChargeQuantity; witrav.PriceOverride = pmtrav.PriceOverride; witrav.TravelDetails = pmtrav.TravelDetails; witrav.TravelRateId = pmtrav.TravelRateId; witrav.TravelRateQuantity = pmtrav.TravelRateQuantity; witrav.TaxCodeSaleId = pmtrav.TaxCodeSaleId; witrav.UserId = pmtrav.UserId; witrav.TravelStartDate = pmtrav.TravelStartDate;//DATE ADJUST witrav.TravelStopDate = pmtrav.TravelStopDate;//DATE ADJUST //woi.Travels.Add(witrav); if (await biz.TravelCreateAsync(witrav) == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating WorkOrderItemTravel from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating WorkOrderItemTravel from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } } foreach (PMItemTask pmt in pmi.Tasks) { var wit = new WorkOrderItemTask(); wit.WorkOrderItemId = NewWoItem.Id; wit.CompletedByUserId = pmt.CompletedByUserId; wit.Sequence = pmt.Sequence; wit.Status = pmt.Status; wit.Task = pmt.Task; wit.CompletedDate = pmt.CompletedDate;//DATE ADJUST //woi.Tasks.Add(wit); if (await biz.TaskCreateAsync(wit) == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating WorkOrderItemTask from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating WorkOrderItemTask from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } } foreach (PMItemExpense pme in pmi.Expenses) { var wie = new WorkOrderItemExpense(); wie.WorkOrderItemId = NewWoItem.Id; wie.ChargeAmount = pme.ChargeAmount; wie.ChargeTaxCodeId = pme.ChargeTaxCodeId; wie.ChargeToCustomer = pme.ChargeToCustomer; wie.Description = pme.Description; wie.ReimburseUser = pme.ReimburseUser; wie.TaxPaid = pme.TaxPaid; wie.TotalCost = pme.TotalCost; wie.UserId = pme.UserId; //woi.Expenses.Add(wie); if (await biz.ExpenseCreateAsync(wie) == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating WorkOrderItemExpense from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating WorkOrderItemExpense from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } } foreach (PMItemOutsideService pmo in pmi.OutsideServices) { var wio = new WorkOrderItemOutsideService(); wio.WorkOrderItemId = NewWoItem.Id; wio.Notes = pmo.Notes; wio.RepairCost = pmo.RepairCost; wio.RepairPrice = pmo.RepairPrice; wio.RMANumber = pmo.RMANumber; wio.ShippingCost = pmo.ShippingCost; wio.ShippingPrice = pmo.ShippingPrice; wio.TaxCodeId = pmo.TaxCodeId; wio.TrackingNumber = pmo.TrackingNumber; wio.UnitId = pmo.UnitId; wio.VendorSentToId = pmo.VendorSentToId; wio.VendorSentViaId = pmo.VendorSentViaId; wio.SentDate = pmo.SentDate;//DATE ADJUST wio.ReturnDate = pmo.ReturnDate;//DATE ADJUST wio.ETADate = pmo.ETADate;//DATE ADJUST //woi.OutsideServices.Add(wio); if (await biz.OutsideServiceCreateAsync(wio) == null) { var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating WorkOrderItemOutsideService from PM {p.Serial}\r\n{biz.GetErrorsAsString()}"; log.LogError(err); await NotifyEventHelper.AddGeneralNotifyEvent(AyaType.PM, p.Id, NotifyEventType.PMGenerationFailed, $"Error generating WorkOrderItemOutsideService from PM {p.Serial}\r\n{biz.GetErrorsAsString()}", "Preventive Maintenance"); return false; } } } // #if (DEBUG) // log.LogInformation($"PMBiz::NewServiceWorkOrderFromPMAsync created new workorder {NewWoHeader.Serial}"); // #endif return true; } #endregion #region v7 code for Generate service workorder from PM /* /// /// Loop through PM workorders ready for generation and /// Process them / advance dates /// public static void GeneratePMWorkorders() { WorkorderPMReadyForServiceList l=WorkorderPMReadyForServiceList.GetList(); foreach(WorkorderPMReadyForServiceList.WorkorderPMReadyForServiceListInfo i in l) { Workorder.NewServiceWorkorderFromPM(i.PMWorkorderID); } } protected override void DataPortal_Fetch(object Criteria) { SafeDataReader dr = null; try { //Changed: 26-April-2006 this query was missing the last bit //that filtered out wo's with an expired stop generating date //which in turn was throwing an exception in the Workorder.NewServiceWorkorderFromPM //which was written to assume that this query was filtering them out and threw //an exception as a "backstop" in case someone was calling the method from an api //Changed: 30-Aug-2006 added further check for stop generating date being null //as it is when there is nothing selected DBCommandWrapper cm = DBUtil.DB.GetSqlStringCommandWrapper( //************************************************************ //"SELECT aWorkorderID FROM aWorkorderPreventiveMaintenance " + //"WHERE (AACTIVE = @aTrue) AND (aGenerateDate < @aNow) " + //"AND (aStopGeneratingDate IS NULL OR aStopGeneratingDate > @aNow) " //CASE 789 ignore pm templates "SELECT AWORKORDERID, ACLIENTID from AWORKORDERPREVENTIVEMAINTENANCE " + "LEFT OUTER JOIN AWORKORDER " + "ON (AWORKORDERPREVENTIVEMAINTENANCE.AWORKORDERID=AWORKORDER.AID) " + "WHERE AWORKORDER.AWORKORDERTYPE='2' " + "AND (AACTIVE = @aTrue) AND (aGenerateDate < @aNow) " + "AND (aStopGeneratingDate IS NULL OR aStopGeneratingDate > @aNow) " //************************************************************ ); cm.AddInParameter("@aTrue",DbType.Boolean,true); cm.AddInParameter("@aNow", DbType.DateTime, DBUtil.ToUTC(DBUtil.CurrentWorkingDateTime));//case 957 dr=new SafeDataReader(DBUtil.DB.ExecuteReader(cm)); while(dr.Read()) { //******************************************* //case 3701 - check if client for this pm is active or not and only process for an active client if (ClientActiveChecker.ClientActive(dr.GetGuid("ACLIENTID"))) { WorkorderPMReadyForServiceListInfo info = new WorkorderPMReadyForServiceListInfo(); info._PMWorkorderID = dr.GetGuid("aWorkorderID"); InnerList.Add(info); } //******************************************* } } catch { thrxxow; } finally { if(dr!=null) dr.Close(); } } #endregion /// /// Generates a service workorder from a PM type Workorder /// /// ID of PM /// A new service workorder public static Workorder NewServiceWorkorderFromPM(Guid SourceWorkorderID) { //Fetch the source workorder and verify it's a PM Workorder source = Workorder.GetItemNoMRU(SourceWorkorderID); if (source.WorkorderType != WorkorderTypes.PreventiveMaintenance) throw new NotSupportedException(LocalizedTextTable.GetLocalizedTextDirect("Workorder.Label.Error.SourceInvalidType")); //if it's inactive then there is nothing to Process //this is a backstop, the list the pm is being generated off of //should have already selected only active items that have not reached their //expiry date if (source.WorkorderPreventiveMaintenance.Active == false) { throw new System.ApplicationException("NewServiceWorkorderFromPM: source PM workorder is not active"); } if (source.WorkorderPreventiveMaintenance.StopGeneratingDate != System.DBNull.Value && source.WorkorderPreventiveMaintenance.dtStopGeneratingDate < DBUtil.CurrentWorkingDateTime) { throw new System.ApplicationException("NewServiceWorkorderFromPM: source PM workorder is past StopGeneratingDate"); } //Ok, so far so good, create the new one bool bUseInventory = AyaBizUtils.GlobalSettings.UseInventory; //case 1387 Workorder dest = Workorder.NewItem(WorkorderTypes.Service); ////NOTE: THIS DOESN'T CALL THE SHARED NEW ITEM METHOD //Workorder dest = new Workorder(); //dest.WorkorderType=WorkorderTypes.Service; //dest.mService=WorkorderService.NewItem(dest); #region copy workorder data //WORKORDER HEADER dest.ClientID = source.ClientID; dest.CustomerContactName = source.CustomerContactName; dest.CustomerReferenceNumber = source.CustomerReferenceNumber; dest.mFromPMID = source.WorkorderPreventiveMaintenance.ID; dest.InternalReferenceNumber = source.InternalReferenceNumber; dest.Onsite = source.Onsite; dest.ProjectID = source.ProjectID; //dest.RegionID=source.RegionID; dest.Summary = source.Summary; dest.WorkorderCategoryID = source.WorkorderCategoryID; //PM SPECIFIC dest.WorkorderService.WorkorderStatusID = source.WorkorderPreventiveMaintenance.WorkorderStatusID; //Date stuff (note that date is assumed to have been advanced the last time a workorder was //generated off the pm (see bottom of this method for that)) dest.WorkorderService.ServiceDate = source.WorkorderPreventiveMaintenance.NextServiceDate; //WORKORDERITEMS foreach (WorkOrderItem wisource in source.WorkOrderItems) { WorkOrderItem widest = dest.WorkOrderItems.Add(dest); widest.Custom0 = wisource.Custom0; widest.Custom1 = wisource.Custom1; widest.Custom2 = wisource.Custom2; widest.Custom3 = wisource.Custom3; widest.Custom4 = wisource.Custom4; widest.Custom5 = wisource.Custom5; widest.Custom6 = wisource.Custom6; widest.Custom7 = wisource.Custom7; widest.Custom8 = wisource.Custom8; widest.Custom9 = wisource.Custom9; widest.PriorityID = wisource.PriorityID; widest.RequestDate = wisource.RequestDate; widest.Summary = wisource.Summary; widest.TechNotes = wisource.TechNotes; widest.TypeID = wisource.TypeID; widest.UnitID = wisource.UnitID; widest.WarrantyService = wisource.WarrantyService; widest.WorkOrderItemUnitServiceTypeID = wisource.WorkOrderItemUnitServiceTypeID; widest.WorkorderStatusID = wisource.WorkorderStatusID; //PARTS foreach (WorkOrderItemPart partsource in wisource.Parts) { WorkOrderItemPart partdest = widest.Parts.Add(widest); partdest.Cost = partsource.Cost; partdest.Description = partsource.Description; partdest.Discount = partsource.Discount; partdest.DiscountType = partsource.DiscountType; partdest.PartID = partsource.PartID; partdest.PartWarehouseID = partsource.PartWarehouseID; partdest.Price = partsource.Price; if (bUseInventory) { partdest.QuantityReserved = partsource.Quantity; partdest.Quantity = 0; } else partdest.Quantity = partsource.Quantity; partdest.TaxPartSaleID = partsource.TaxPartSaleID; } //********************************************************** //Part requests would be here if copying a service workorder //********************************************************** //SCHEDULED USERS foreach (WorkOrderItemScheduledUser usersource in wisource.ScheduledUsers) { WorkOrderItemScheduledUser userdest = widest.ScheduledUsers.Add(widest); userdest.EstimatedQuantity = usersource.EstimatedQuantity; userdest.ServiceRateID = usersource.ServiceRateID; userdest.StartDate = usersource.StartDate; userdest.StopDate = usersource.StopDate; userdest.UserID = usersource.UserID; } //LABOR foreach (WorkOrderItemLabor laborsource in wisource.Labors) { WorkOrderItemLabor labordest = widest.Labors.Add(widest); labordest.NoChargeQuantity = laborsource.NoChargeQuantity; labordest.ServiceDetails = laborsource.ServiceDetails; labordest.ServiceRateID = laborsource.ServiceRateID; labordest.ServiceRateQuantity = laborsource.ServiceRateQuantity; labordest.ServiceStartDate = laborsource.ServiceStartDate; labordest.ServiceStopDate = laborsource.ServiceStopDate; labordest.TaxRateSaleID = laborsource.TaxRateSaleID; labordest.UserID = laborsource.UserID; } //********************************************************** //Expenses would be here if copying a service workorder //********************************************************** //********************************************************** //Loans would be here if copying a service workorder //********************************************************** //TRAVEL foreach (WorkOrderItemTravel travelsource in wisource.Travels) { WorkOrderItemTravel traveldest = widest.Travels.Add(widest); traveldest.TravelDetails = travelsource.TravelDetails; traveldest.TravelRateID = travelsource.TravelRateID; traveldest.TravelRateQuantity = travelsource.TravelRateQuantity; traveldest.TravelStartDate = travelsource.TravelStartDate; traveldest.TravelStopDate = travelsource.TravelStopDate; traveldest.TaxRateSaleID = travelsource.TaxRateSaleID; traveldest.UserID = travelsource.UserID; traveldest.Distance = travelsource.Distance; traveldest.Notes = travelsource.Notes; traveldest.NoChargeQuantity = travelsource.NoChargeQuantity; } //TASKS foreach (WorkOrderItemTask tasksource in wisource.Tasks) { WorkOrderItemTask taskdest = widest.Tasks.Add(widest); taskdest.TaskGroupID = tasksource.TaskGroupID; taskdest.TaskID = tasksource.TaskID; } //********************************************************** //Outside service would be here if copying a service workorder //********************************************************** }//foreach workorderitem loop //case 1387 //Delete the auto-created dummy workorder item //if there are more than it present if (dest.WorkOrderItems.Count > 1) dest.WorkOrderItems.RemoveAt(0); #endregion copy workorder data //Now save it to ensure it was created properly so //that we know it's now safe to advance the next service date and all others //case 868 previously didn't set dest to result of save causing it to be a copy dest = (Workorder)dest.Save(); #region Calculate reschedule dates //Get the current next service date for calcs DateTime dtNext = GetDateFromSpanAndUnit(source.WorkorderPreventiveMaintenance.dtNextServiceDate, source.WorkorderPreventiveMaintenance.GenerateSpanUnit, source.WorkorderPreventiveMaintenance.GenerateSpan); //Get to the desired day of the week if necessary... if (source.mWorkorderPreventiveMaintenance.DayOfTheWeek != AyaDayOfWeek.AnyDayOfWeek) { DayOfWeek desired = AyaToSystemDayOfWeek(source.mWorkorderPreventiveMaintenance.DayOfTheWeek); while (dtNext.DayOfWeek != desired) { dtNext = dtNext.AddDays(1); } } //Get the time span to add to all the other relevant dates on teh workorder to match //the amount the next service date has been advanced System.TimeSpan tsToNext = dtNext - source.WorkorderPreventiveMaintenance.dtNextServiceDate; #endregion //Will the next workorder service date fall after the //stop generating date? if (source.WorkorderPreventiveMaintenance.StopGeneratingDate != System.DBNull.Value && source.WorkorderPreventiveMaintenance.dtStopGeneratingDate < dtNext) { //Yes it will, so set it to inactive and bail out source.WorkorderPreventiveMaintenance.Active = false; source.Save(); return dest; } #region Reschedule PM source.WorkorderPreventiveMaintenance.dtNextServiceDate = dtNext; //Calcs the generate date (threshold date) source.WorkorderPreventiveMaintenance.SetGenerateDate(); //WORKORDERITEMS foreach (WorkOrderItem wisource in source.WorkOrderItems) { wisource.RequestDate = wisource.RequestDate; //PARTS //no date changes required //SCHEDULED USERS foreach (WorkOrderItemScheduledUser usersource in wisource.ScheduledUsers) { //Changed: 2-Oct-2006 //check to not add a date if the original date was empty if (usersource.StartDate != System.DBNull.Value) usersource.dtStartDate = usersource.dtStartDate.Add(tsToNext); if (usersource.StopDate != System.DBNull.Value) usersource.dtStopDate = usersource.dtStopDate.Add(tsToNext); } //LABOR foreach (WorkOrderItemLabor laborsource in wisource.Labors) { //Changed: 2-Oct-2006 //check to not add a date if the original date was empty if (laborsource.ServiceStartDate != System.DBNull.Value) laborsource.dtServiceStartDate = laborsource.dtServiceStartDate.Add(tsToNext); if (laborsource.ServiceStopDate != System.DBNull.Value) laborsource.dtServiceStopDate = laborsource.dtServiceStopDate.Add(tsToNext); } //********************************************************** //Expenses would be here if copying a service workorder //********************************************************** //********************************************************** //Loans would be here if copying a service workorder //********************************************************** //TRAVEL foreach (WorkOrderItemTravel travelsource in wisource.Travels) { //Changed: 2-Oct-2006 //check to not add a date if the original date was empty if (travelsource.TravelStartDate != DBNull.Value) travelsource.dtTravelStartDate = travelsource.dtTravelStartDate.Add(tsToNext); if (travelsource.TravelStopDate != DBNull.Value) travelsource.dtTravelStopDate = travelsource.dtTravelStopDate.Add(tsToNext); } //TASKS //********************************************************** //Outside service would be here if copying a service workorder //********************************************************** }//foreach workorderitem loop #endregion reschedule pm //Ok, Source PM is now rescheduled, save it //case 1959 try catch block added to prevent infinite generation issue try { source = (Workorder)source.Save(); } catch (Exception exx) { dest.Delete(); dest.Save(); //crack the exception while (exx.InnerException != null) exx = exx.InnerException; Memo mwarn = Memo.NewItem(); mwarn.ToID = User.AdministratorID; //case 3826 if (User.CurrentUserType == UserTypes.Utility) { //Utility accounts should not be sending memos, it fucks up downstream //trying to view the memo, also it's confusing mwarn.FromID = User.AdministratorID; } else { mwarn.FromID = User.CurrentThreadUserID; } mwarn.Subject = "SYSTEM WARNING: Preventive Maintenance WO PROBLEM"; StringBuilder sb = new StringBuilder(); sb.AppendLine("This is an automated message sent on behalf of the current user from the \"NewServiceWorkorderFromPM\" module."); sb.AppendLine("This message concerns Preventive Maintenance workorder number " + source.WorkorderPreventiveMaintenance.PreventiveMaintenanceNumber.ToString()); sb.AppendLine("The Preventive Maintenance workorder had an error when trying to save it during generation of a service workorder."); sb.AppendLine("This kind of problem could result in loop which generates a very large number of identical service workorders."); sb.AppendLine("In order to prevent this the operation has been stopped and this message generated so you can fix the problem with the source PM workorder."); sb.AppendLine("See below for details and examine the PM workorder for problems or contact support@ayanova.com for help with the information in this message."); sb.AppendLine("Here are the details of the error preventing save:"); sb.AppendLine("================================="); sb.AppendLine("Exception saving source PM:"); sb.AppendLine(exx.Message); sb.AppendLine("================================="); string sSourceErr = source.GetBrokenRulesString(); if (!string.IsNullOrWhiteSpace(sSourceErr)) { sb.AppendLine("Broken business rules on PM object:"); sb.AppendLine(sSourceErr); sb.AppendLine("=============================="); } mwarn.Message = sb.ToString(); mwarn.Save(); throw new System.ApplicationException("Workorder->NewServiceWorkorderFromPM: Error during service workorder generation. Memo with details sent to Administrator account."); } //case 1630 //copy wikipage from pm to service workorder if (dest.CanWiki && source.HasWiki) { try { WikiPage wpSource = WikiPage.GetItem(new TypeAndID(RootObjectTypes.WorkorderPreventiveMaintenance, source.ID)); WikiPage wpDest = WikiPage.GetItem(new TypeAndID(RootObjectTypes.WorkorderService, dest.ID)); wpDest.SetContent(wpSource.GetContent()); wpDest.Save(); } catch { }; } return dest; } /// /// Calculate generate date based on service date and /// threshold span and unit /// internal void SetGenerateDate() { if (this.mNextServiceDate.IsEmpty) return; if (this.mThresholdSpan == 0) { this.mGenerateDate = this.mNextServiceDate; MarkDirty(); return; } mGenerateDate = new SmartDate(Workorder.GetDateFromSpanAndUnit(mNextServiceDate.Date, this.mThresholdSpanUnit, -mThresholdSpan)); MarkDirty(); } #region Date time calcs helpers //Takes an AyaNova day of week and returns //a System.DayOfWeek //Assumes that AyaDayOfWeek is NOT "AnyDay" internal static System.DayOfWeek AyaToSystemDayOfWeek(AyaDayOfWeek day) { switch (day) { case AyaDayOfWeek.Monday: return DayOfWeek.Monday; case AyaDayOfWeek.Tuesday: return DayOfWeek.Tuesday; case AyaDayOfWeek.Wednesday: return DayOfWeek.Wednesday; case AyaDayOfWeek.Thursday: return DayOfWeek.Thursday; case AyaDayOfWeek.Friday: return DayOfWeek.Friday; case AyaDayOfWeek.Saturday: return DayOfWeek.Saturday; case AyaDayOfWeek.Sunday: return DayOfWeek.Sunday; } throw new System.ArgumentOutOfRangeException("DayOfWeekConverter: AyaDayOfWeek.AnyDayOfWeek is not supported"); } internal static DateTime GetDateFromSpanAndUnit(DateTime StartDate, AyaUnitsOfTime unit, int multiple) { switch (unit) { case AyaUnitsOfTime.Seconds: return StartDate.AddSeconds(multiple); case AyaUnitsOfTime.Minutes: return StartDate.AddMinutes(multiple); case AyaUnitsOfTime.Hours: return StartDate.AddHours(multiple); case AyaUnitsOfTime.Days: return StartDate.AddDays(multiple); case AyaUnitsOfTime.Weeks: throw new System.NotSupportedException("GetDateFromSpanAndUnit: Weeks not supported"); case AyaUnitsOfTime.Months: return StartDate.AddMonths(multiple); case AyaUnitsOfTime.Years: return StartDate.AddYears(multiple); } //fail safe: return StartDate; } */ #endregion v7 code gen service wo from pm ///////////////////////////////////////////////////////////////////// }//eoc }//eons