diff --git a/docs/8.0/ayanova/docs/home-notify-subscriptions.md b/docs/8.0/ayanova/docs/home-notify-subscriptions.md index 342aef8f..47754660 100644 --- a/docs/8.0/ayanova/docs/home-notify-subscriptions.md +++ b/docs/8.0/ayanova/docs/home-notify-subscriptions.md @@ -94,7 +94,7 @@ There are no settings adjustable for in app General notifications, however Users | NotifyHealthCheck | Automatic daily "ping" notification to confirm notification and Generator system is active at server | | BackupStatus | Result of last Backup operation at server | | CustomerServiceImminent | Scheduled service date / time is about to be reached. Intended for Customer type User | -| WorkorderTotalExceedsThreshold | The balance of a Work order has exceeded a threshold (aka the "Andy") | +| WorkorderTotalExceedsThreshold | The balance of a Work order has exceeded a threshold (the "Andy") | | WorkorderStatusAge | A Workorder has been sitting at the selected status for longer than the selected time frame | | UnitWarrantyExpiry | A Unit's warranty expiration date is reached | | UnitMeterReadingMultipleExceeded | A meter readingn *multiple* exceeds selected threshold (e.g. every 10,000 etc) | diff --git a/server/AyaNova/biz/WorkOrderBiz.cs b/server/AyaNova/biz/WorkOrderBiz.cs index 5eec8ff4..cbd3b5c8 100644 --- a/server/AyaNova/biz/WorkOrderBiz.cs +++ b/server/AyaNova/biz/WorkOrderBiz.cs @@ -574,62 +574,71 @@ namespace AyaNova.Biz - //NOTE: REMOVED WHEN REMOVED STATIC PRICING - // //////////////////////////////////////////////////////////////////////////////////////////////// - // //CONTRACT CHANGE HANDLER - // // - // // - // private async Task ProcessChangeOfContractAsync(long woId) - // { - // //contract has changed, update entire graph pricing and potentially response time stuff as well here now - // //iterate graph calling *SetListPrice on each item - // var wo = await ct.WorkOrder.AsSplitQuery() - // .Include(s => s.States) - // .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.PartRequests) - // .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 == woId); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // "The Andy" notification helper + // + // (for now this is only for the notification exceeds total so only need one grand total of + // line totals, if in future need more can return a Record object instead with split out + // taxes, net etc etc) + // + private async Task WorkorderTotalAsync(long workOrderId, AyContext ct) + { - // //If Contract has response time then set CompleteByDate - // if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero) - // { - // wo.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime); - // } + var wo = await ct.WorkOrder.AsNoTracking().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.Travels) + .Include(w => w.Items) + .ThenInclude(wi => wi.OutsideServices) + .SingleOrDefaultAsync(z => z.Id == workOrderId); + if (wo == null) return 0m; - // // //update pricing - // // foreach (WorkOrderItem wi in wo.Items) - // // { - // // // foreach (WorkOrderItemLabor o in wi.Labors) - // // // await LaborSetPrice(o, mContractInEffect); - // // foreach (WorkOrderItemTravel o in wi.Travels) - // // TravelSetListPrice(o, mContractInEffect); - // // foreach (WorkOrderItemPart o in wi.Parts) - // // PartSetListPrice(o, mContractInEffect); - // // } + decimal GrandTotal = 0m; + //update pricing + foreach (WorkOrderItem wi in wo.Items) + { + foreach (WorkOrderItemExpense o in wi.Expenses) + await ExpensePopulateVizFields(o, true); + foreach (WorkOrderItemLabor o in wi.Labors) + await LaborPopulateVizFields(o, true); + foreach (WorkOrderItemLoan o in wi.Loans) + await LoanPopulateVizFields(o, null, true); + foreach (WorkOrderItemPart o in wi.Parts) + await PartPopulateVizFields(o, true); + foreach (WorkOrderItemTravel o in wi.Travels) + await TravelPopulateVizFields(o, true); + foreach (WorkOrderItemOutsideService o in wi.OutsideServices) + await OutsideServicePopulateVizFields(o, true); + } - // await ct.SaveChangesAsync(); - // return wo; + foreach (WorkOrderItem wi in wo.Items) + { + foreach (WorkOrderItemExpense o in wi.Expenses) + GrandTotal += o.LineTotalViz; + foreach (WorkOrderItemLabor o in wi.Labors) + GrandTotal += o.LineTotalViz; + foreach (WorkOrderItemLoan o in wi.Loans) + GrandTotal += o.LineTotalViz; + foreach (WorkOrderItemPart o in wi.Parts) + GrandTotal += o.LineTotalViz; + foreach (WorkOrderItemTravel o in wi.Travels) + GrandTotal += o.LineTotalViz; + foreach (WorkOrderItemOutsideService o in wi.OutsideServices) + GrandTotal += o.LineTotalViz; + } - // } + return GrandTotal; + } @@ -1146,8 +1155,7 @@ namespace AyaNova.Biz }//CustomerServiceImminent - #endregion - + #endregion }//end of process notifications @@ -1417,6 +1425,53 @@ namespace AyaNova.Biz } }//workorder complete by overdue change event + + //# WorkorderTotalExceedsThreshold / "The Andy" + { + if (wos.Completed) + { + + //see if any subscribers to the workorder total exceeds notification + //that are active then proceed to fetch billed woitem children and total workorder and send notification if necessary + + bool haveTotal = false; + decimal total = 0m; + + //look for potential subscribers + var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderTotalExceedsThreshold).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) + //check early to avoid cost of fetching and calculating total if unnecessary + if (!NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags)) continue; + + //get the total because we have at least one subscriber and matching tags + if (haveTotal == false) + { + long WorkOrderId = 0; + if (ayaType == AyaType.WorkOrder) + WorkOrderId = id; + else + WorkOrderId = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct); + + //clear out any existing ones as they may have *just* been set from a save and we don't want a workorder save of a bunch of items to trigger 10,000 notifications + //Always clear any old ones for this object as they are all irrelevant the moment changed: + await NotifyEventHelper.ClearPriorEventsForObject(ct, AyaType.WorkOrder, WorkOrderId, NotifyEventType.WorkorderTotalExceedsThreshold); + + //total workorder + + } + + } + + } + }//The Andy notification + + + } }//end of process notifications @@ -1973,11 +2028,13 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // - private async Task ExpensePopulateVizFields(WorkOrderItemExpense o) + private async Task ExpensePopulateVizFields(WorkOrderItemExpense o, bool calculateTotalsOnly = false) { - if (o.UserId != null) - o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); - + if (calculateTotalsOnly == false) + { + if (o.UserId != null) + o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); + } TaxCode Tax = null; if (o.ChargeTaxCodeId != null) Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.ChargeTaxCodeId); @@ -2314,10 +2371,13 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // - private async Task LaborPopulateVizFields(WorkOrderItemLabor o) + private async Task LaborPopulateVizFields(WorkOrderItemLabor o, bool calculateTotalsOnly = false) { - if (o.UserId != null) - o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); + if (calculateTotalsOnly == false) + { + if (o.UserId != null) + o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); + } ServiceRate Rate = null; if (o.ServiceRateId != null) { @@ -2799,14 +2859,17 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // - private async Task LoanPopulateVizFields(WorkOrderItemLoan o, List loanUnitRateEnumList = null) + private async Task LoanPopulateVizFields(WorkOrderItemLoan o, List loanUnitRateEnumList = null, bool 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 (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(); + } LoanUnit loanUnit = await ct.LoanUnit.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.LoanUnitId); o.LoanUnitViz = loanUnit.Name; @@ -3182,14 +3245,17 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // - private async Task OutsideServicePopulateVizFields(WorkOrderItemOutsideService o) + private async Task OutsideServicePopulateVizFields(WorkOrderItemOutsideService o, bool calculateTotalsOnly = false) { - if (o.UnitId != 0) - o.UnitViz = await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync(); - if (o.VendorSentToId != null) - o.VendorSentToViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentToId).Select(x => x.Name).FirstOrDefaultAsync(); - if (o.VendorSentViaId != null) - o.VendorSentViaViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentViaId).Select(x => x.Name).FirstOrDefaultAsync(); + if (calculateTotalsOnly == false) + { + if (o.UnitId != 0) + o.UnitViz = await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync(); + if (o.VendorSentToId != null) + o.VendorSentToViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentToId).Select(x => x.Name).FirstOrDefaultAsync(); + if (o.VendorSentViaId != null) + o.VendorSentViaViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentViaId).Select(x => x.Name).FirstOrDefaultAsync(); + } TaxCode Tax = null; if (o.TaxCodeId != null) @@ -3662,13 +3728,19 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // - private async Task PartPopulateVizFields(WorkOrderItemPart o) + private async Task PartPopulateVizFields(WorkOrderItemPart o, bool calculateTotalsOnly = false) { - if (o.PartWarehouseId != 0) - o.PartWarehouseViz = await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync(); + if (calculateTotalsOnly == false) + { + if (o.PartWarehouseId != 0) + o.PartWarehouseViz = await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync(); + } Part part = null; if (o.PartId != 0) part = await ct.Part.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.PartId); + else + return;//this should never happen but this is insurance in case it does + o.PartViz = part.PartNumber; o.UpcViz = part.UPC; @@ -5095,10 +5167,13 @@ namespace AyaNova.Biz //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // - private async Task TravelPopulateVizFields(WorkOrderItemTravel o) + private async Task TravelPopulateVizFields(WorkOrderItemTravel o, bool calculateTotalsOnly = false) { - if (o.UserId != null) - o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); + if (calculateTotalsOnly == false) + { + if (o.UserId != null) + o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); + } TravelRate Rate = null; if (o.TravelRateId != null) {