This commit is contained in:
@@ -344,7 +344,7 @@ namespace AyaNova.Biz
|
||||
AddError(ApiErrorCode.NOT_FOUND);
|
||||
return false;
|
||||
}
|
||||
PMValidateCanDelete(dbObject);
|
||||
await PMValidateCanDelete(dbObject);
|
||||
if (HasErrors)
|
||||
return false;
|
||||
|
||||
@@ -615,72 +615,6 @@ namespace AyaNova.Biz
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// "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<decimal> WorkorderGrandTotalAsync(long workOrderId, AyContext ct)
|
||||
{
|
||||
var wo = await ct.PM.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;
|
||||
|
||||
decimal GrandTotal = 0m;
|
||||
//update pricing
|
||||
foreach (PMItem wi in wo.Items)
|
||||
{
|
||||
foreach (PMItemExpense o in wi.Expenses)
|
||||
await ExpensePopulateVizFields(o, true);
|
||||
foreach (PMItemLabor o in wi.Labors)
|
||||
await LaborPopulateVizFields(o, true);
|
||||
foreach (PMItemLoan o in wi.Loans)
|
||||
await LoanPopulateVizFields(o, null, true);
|
||||
foreach (PMItemPart o in wi.Parts)
|
||||
await PartPopulateVizFields(o, true);
|
||||
foreach (PMItemTravel o in wi.Travels)
|
||||
await TravelPopulateVizFields(o, true);
|
||||
foreach (PMItemOutsideService o in wi.OutsideServices)
|
||||
await OutsideServicePopulateVizFields(o, true);
|
||||
}
|
||||
|
||||
foreach (PMItem wi in wo.Items)
|
||||
{
|
||||
foreach (PMItemExpense o in wi.Expenses)
|
||||
GrandTotal += o.LineTotalViz;
|
||||
foreach (PMItemLabor o in wi.Labors)
|
||||
GrandTotal += o.LineTotalViz;
|
||||
foreach (PMItemLoan o in wi.Loans)
|
||||
GrandTotal += o.LineTotalViz;
|
||||
foreach (PMItemPart o in wi.Parts)
|
||||
GrandTotal += o.LineTotalViz;
|
||||
foreach (PMItemTravel o in wi.Travels)
|
||||
GrandTotal += o.LineTotalViz;
|
||||
foreach (PMItemOutsideService o in wi.OutsideServices)
|
||||
GrandTotal += o.LineTotalViz;
|
||||
}
|
||||
|
||||
return GrandTotal;
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//VALIDATION
|
||||
//
|
||||
@@ -726,42 +660,6 @@ namespace AyaNova.Biz
|
||||
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "RepeatInterval", await Translate("ErrorRepeatIntervalTooSmall"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
|
||||
todo: quote status list first, it's a table of created items, keep properties from v7 but add the following properties:
|
||||
SelectRoles - who can select the status (still shows if they can't select but that's the current status, like active does)
|
||||
This is best handled at the client. It prefetches all the status out of the normal picklist process, more like how other things are separately handled now without a picklist
|
||||
client then knows if a status is available or not and can process to only present available ones
|
||||
#### Server can use a biz rule to ensure that it can't be circumvented
|
||||
UI defaults to any role
|
||||
DeselectRoles - who can unset this status (important for process control)
|
||||
UI defaults to any role
|
||||
CompletedStatus bool - this is a final status indicating all work on the quote is completed, affects notification etc
|
||||
UI defaults to false but when set to true auto sets lockworkorder to true (but user can just unset lockworkorder)
|
||||
LockWorkorder - this status is considered read only and the quote is locked
|
||||
Just a read only thing, can just change status to "unlock" it
|
||||
to support states where no one should work on a wo for whatever reason but it's not necessarily completed
|
||||
e.g. "Hold for inspection", "On hold" generally etc
|
||||
*/
|
||||
// //Name required
|
||||
// if (string.IsNullOrWhiteSpace(proposedObj.Name))
|
||||
// AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
|
||||
|
||||
|
||||
// //If name is otherwise OK, check that name is unique
|
||||
// if (!PropertyHasErrors("Name"))
|
||||
// {
|
||||
// //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
|
||||
// if (await ct.PM.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id))
|
||||
// {
|
||||
// AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
//Any form customizations to validate?
|
||||
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString());
|
||||
if (FormCustomization != null)
|
||||
@@ -774,12 +672,11 @@ namespace AyaNova.Biz
|
||||
//validate custom fields
|
||||
CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void PMValidateCanDelete(PM dbObject)
|
||||
private async Task PMValidateCanDelete(PM dbObject)
|
||||
{
|
||||
//Check restricted role preventing create
|
||||
if (UserIsRestrictedType)
|
||||
@@ -788,41 +685,12 @@ namespace AyaNova.Biz
|
||||
return;//this is a completely disqualifying error
|
||||
}
|
||||
//FOREIGN KEY CHECKS
|
||||
//these are examples copied from customer for when other objects are actually referencing them
|
||||
// if (await ct.User.AnyAsync(m => m.CustomerId == inObj.Id))
|
||||
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("User"));
|
||||
// if (await ct.Unit.AnyAsync(m => m.CustomerId == inObj.Id))
|
||||
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Unit"));
|
||||
// if (await ct.CustomerServiceRequest.AnyAsync(m => m.CustomerId == inObj.Id))
|
||||
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("CustomerServiceRequest"));
|
||||
// if (await ct.PurchaseOrder.AnyAsync(m => m.DropShipToCustomerId == inObj.Id))
|
||||
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("PurchaseOrder"));
|
||||
if (await ct.WorkOrder.AnyAsync(m => m.FromPMId == dbObject.Id))
|
||||
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("PM"));
|
||||
}
|
||||
|
||||
|
||||
//############### NOTIFICATION TODO
|
||||
/*
|
||||
|
||||
todo: quote notifications remove #30 and #32 as redundant
|
||||
WorkorderStatusChange = 4,//* Workorder object, any *change* of status including from no status (new) to a specific conditional status ID value
|
||||
|
||||
WorkorderStatusAge = 24,//* Workorder object Created / Updated, conditional on exact status selected IdValue, Tags conditional, advance notice can be set
|
||||
|
||||
//THESE TWO ARE REDUNDANT:
|
||||
|
||||
this is actually workorderstatuschange because can just pick any status under workorderstatuschange to be notified about
|
||||
WorkorderCompleted = 30, //*travel work order is set to any status that is flagged as a "Completed" type of status. Customer & User
|
||||
|
||||
//This one could be accomplished with WorkorderStatusAge, just pick a Completed status and set a time frame and wala!
|
||||
WorkorderCompletedFollowUp = 32, //* travel quote closed status follow up again after this many TIMESPAN
|
||||
|
||||
todo: CHANGE WorkorderCompletedStatusOverdue = 15,//* Workorder object not set to a "Completed" flagged quote status type in selected time span from creation of quote
|
||||
Change this to a new type that is based on so many days *without* being set to a particular status
|
||||
but first check if tied to contract response time stuff, how that's handled
|
||||
that's closeby date in v7 but isn't that deprecated now without a "close"?
|
||||
maybe I do need the Completed status bool thing above
|
||||
|
||||
*/
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -932,64 +800,6 @@ namespace AyaNova.Biz
|
||||
foreach (long batchId in batch)
|
||||
batchResults.Add(await PMGetPartialAsync(dataListSelectedRequest.AType, batchId, dataListSelectedRequest.IncludeWoItemDescendants, true));
|
||||
|
||||
#region unnecessary shit removed
|
||||
//This is unnecessary because the re-ordering bit is only needed when the records are fetched in batches directly from the sql server as they
|
||||
//return in db natural order and need to be put back into the same order as the ID List
|
||||
//Here in the quote however, this code is fetching individually one at a time so they are always going to be in the correct order so this re-ordering is unnecessary
|
||||
//I'm keeping this here for future reference when I ineveitably wonder what the hell is happening here :)
|
||||
|
||||
|
||||
//order the results back into original
|
||||
//IEnumerable<PM> orderedList = null;
|
||||
|
||||
//TODO: WHAT IS THIS BATCH RESULT ORDERING CODE REALLY DOING AND CAN IT BE REMOVED / CHANGED????
|
||||
//isn't it alredy working in order? If not maybe simply reversed so reverse it again before querying above or...??
|
||||
|
||||
//todo: can't assume the grandchild item is index 0 anymore as we might have multiple of them if includedescendants is true
|
||||
//so need to find index first then do this
|
||||
// switch (dataListSelectedRequest.AType)
|
||||
// {
|
||||
// case AyaType.PM:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItem:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItemExpense:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Expenses[0].Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItemLabor:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Labors[0].Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItemLoan:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Loans[0].Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItemPart:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Parts[0].Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItemPartRequest:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].PartRequests[0].Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItemScheduledUser:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].ScheduledUsers[0].Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItemTask:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Tasks[0].Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItemTravel:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Travels[0].Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItemOutsideService:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].OutsideServices[0].Id select z;
|
||||
// break;
|
||||
// case AyaType.PMItemUnit:
|
||||
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Units[0].Id select z;
|
||||
// break;
|
||||
// }
|
||||
|
||||
//foreach (PM w in orderedList)
|
||||
#endregion unnecessary shit
|
||||
|
||||
foreach (PM w in batchResults)
|
||||
{
|
||||
var jo = JObject.FromObject(w);
|
||||
@@ -1070,8 +880,6 @@ namespace AyaNova.Biz
|
||||
else
|
||||
o.ContractViz = "-";
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1086,31 +894,6 @@ namespace AyaNova.Biz
|
||||
return await GetReportData(dataListSelectedRequest);
|
||||
}
|
||||
|
||||
// public async Task<List<string>> ImportData(JArray ja)
|
||||
// {
|
||||
// List<string> ImportResult = new List<string>();
|
||||
// string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}";
|
||||
|
||||
// var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
|
||||
// foreach (JObject j in ja)
|
||||
// {
|
||||
// var w = j.ToObject<PM>(jsset);
|
||||
// if (j["CustomFields"] != null)
|
||||
// w.CustomFields = j["CustomFields"].ToString();
|
||||
// w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary
|
||||
// var res = await PMCreateAsync(w);
|
||||
// if (res == null)
|
||||
// {
|
||||
// ImportResult.Add($"* {w.Serial} - {this.GetErrorsAsString()}");
|
||||
// this.ClearErrors();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// ImportResult.Add($"{w.Serial} - ok");
|
||||
// }
|
||||
// }
|
||||
// return ImportResult;
|
||||
// }
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//JOB / OPERATIONS
|
||||
@@ -1581,7 +1364,7 @@ namespace AyaNova.Biz
|
||||
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)
|
||||
if (WorkorderInfo != null)
|
||||
oProposed.Name = WorkorderInfo.Serial.ToString();
|
||||
else
|
||||
oProposed.Name = "??";
|
||||
|
||||
@@ -350,7 +350,7 @@ namespace AyaNova.Biz
|
||||
AddError(ApiErrorCode.NOT_FOUND);
|
||||
return false;
|
||||
}
|
||||
QuoteValidateCanDelete(dbObject);
|
||||
await QuoteValidateCanDelete(dbObject);
|
||||
if (HasErrors)
|
||||
return false;
|
||||
|
||||
@@ -493,38 +493,6 @@ namespace AyaNova.Biz
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// //CONTRACT UPDATE
|
||||
// //
|
||||
// internal async Task<Quote> ChangeContract(long workOrderId, long? newContractId)
|
||||
// {
|
||||
// //this is called by UI via contract change route for contract change only and expects wo back to update client ui
|
||||
// var w = await ct.Quote.FirstOrDefaultAsync(z => z.Id == workOrderId);
|
||||
|
||||
// if (w == null)
|
||||
// {
|
||||
// AddError(ApiErrorCode.NOT_FOUND, "id");
|
||||
// return null;
|
||||
// }
|
||||
// if (newContractId != null && !await ct.Contract.AnyAsync(z => z.Id == newContractId))
|
||||
// {
|
||||
// AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"Contract with id {newContractId} not found");
|
||||
// return null;
|
||||
// }
|
||||
// w.ContractId = newContractId;
|
||||
// await ct.SaveChangesAsync();
|
||||
// await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, workOrderId, BizType, AyaEvent.Modified), ct);
|
||||
// await GetCurrentContractFromContractIdAsync(newContractId);
|
||||
// var updatedQuote = await ProcessChangeOfContractAsync(workOrderId);
|
||||
// await QuotePopulateVizFields(updatedQuote, false);
|
||||
// return updatedQuote;//return entire quote
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//GET WORKORDER ID FROM DESCENDANT TYPE AND ID
|
||||
//
|
||||
@@ -587,20 +555,6 @@ namespace AyaNova.Biz
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// //GET WORKORDER ID FOR WORK ORDER NUMBER
|
||||
// //
|
||||
// internal static async Task<long> GetQuoteIdForNumberAsync(long woNumber, AyContext ct)
|
||||
// {
|
||||
// return await ct.Quote.AsNoTracking()
|
||||
// .Where(z => z.Serial == woNumber)
|
||||
// .Select(z => z.Id)
|
||||
// .SingleOrDefaultAsync();
|
||||
// }
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//SEARCH
|
||||
//
|
||||
@@ -783,7 +737,7 @@ namespace AyaNova.Biz
|
||||
|
||||
|
||||
|
||||
private void QuoteValidateCanDelete(Quote dbObject)
|
||||
private async Task QuoteValidateCanDelete(Quote dbObject)
|
||||
{
|
||||
//Check restricted role preventing create
|
||||
if (UserIsRestrictedType)
|
||||
@@ -791,16 +745,9 @@ namespace AyaNova.Biz
|
||||
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
||||
return;//this is a completely disqualifying error
|
||||
}
|
||||
//FOREIGN KEY CHECKS
|
||||
//these are examples copied from customer for when other objects are actually referencing them
|
||||
// if (await ct.User.AnyAsync(m => m.CustomerId == inObj.Id))
|
||||
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("User"));
|
||||
// if (await ct.Unit.AnyAsync(m => m.CustomerId == inObj.Id))
|
||||
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Unit"));
|
||||
// if (await ct.CustomerServiceRequest.AnyAsync(m => m.CustomerId == inObj.Id))
|
||||
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("CustomerServiceRequest"));
|
||||
// if (await ct.PurchaseOrder.AnyAsync(m => m.DropShipToCustomerId == inObj.Id))
|
||||
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("PurchaseOrder"));
|
||||
if (await ct.WorkOrder.AnyAsync(m => m.FromQuoteId == dbObject.Id))
|
||||
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Quote"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1380,239 +1327,189 @@ namespace AyaNova.Biz
|
||||
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
||||
|
||||
bool isNew = currentObj == null;
|
||||
QuoteState oProposed = (QuoteState)proposedObj;
|
||||
|
||||
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == oProposed.QuoteId).Select(x => new { x.Serial, x.Tags, x.CustomerId }).FirstOrDefaultAsync();
|
||||
QuoteStatus wos = await ct.QuoteStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == oProposed.QuoteStatusId);
|
||||
//for notification purposes because has no name / tags field itself
|
||||
oProposed.Name = WorkorderInfo.Serial.ToString();
|
||||
oProposed.Tags = WorkorderInfo.Tags;
|
||||
//currently no quote state notifications but may well be in future so this saves changing a bunch of shit if necessary later
|
||||
await Task.CompletedTask;
|
||||
// QuoteState oProposed = (QuoteState)proposedObj;
|
||||
|
||||
//STANDARD EVENTS FOR ALL OBJECTS
|
||||
//NONE: state notifications are specific and not the same as for general objects so don't process standard events
|
||||
// var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == oProposed.QuoteId).Select(x => new { x.Serial, x.Tags, x.CustomerId }).FirstOrDefaultAsync();
|
||||
// QuoteStatus wos = await ct.QuoteStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == oProposed.QuoteStatusId);
|
||||
// //for notification purposes because has no name / tags field itself
|
||||
// oProposed.Name = WorkorderInfo.Serial.ToString();
|
||||
// oProposed.Tags = WorkorderInfo.Tags;
|
||||
|
||||
//SPECIFIC EVENTS FOR THIS OBJECT
|
||||
//WorkorderStatusChange = 4,//*Workorder object, any NEW status set. Conditions: specific status ID value only (no generic any status allowed), Workorder TAGS
|
||||
//WorkorderCompletedStatusOverdue = 15,//* Workorder object not set to a "Completed" flagged quote status type in selected time span from creation of workorderWorkorderSetToCompletedStatus
|
||||
//WorkorderStatusAge = 24,//* Workorder STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set
|
||||
// //STANDARD EVENTS FOR ALL OBJECTS
|
||||
// //NONE: state notifications are specific and not the same as for general objects so don't process standard events
|
||||
|
||||
//NOTE: ID, state notifications are for the Workorder, not the state itself unlike other objects, so use the WO type and ID here for all notifications
|
||||
// //SPECIFIC EVENTS FOR THIS OBJECT
|
||||
// //WorkorderStatusChange = 4,//*Workorder object, any NEW status set. Conditions: specific status ID value only (no generic any status allowed), Workorder TAGS
|
||||
// //WorkorderCompletedStatusOverdue = 15,//* Workorder object not set to a "Completed" flagged quote status type in selected time span from creation of workorderWorkorderSetToCompletedStatus
|
||||
// //WorkorderStatusAge = 24,//* Workorder STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set
|
||||
|
||||
// //NOTE: ID, state notifications are for the Workorder, not the state itself unlike other objects, so use the WO type and ID here for all notifications
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//## DELETED EVENTS
|
||||
//A state cannot be deleted so nothing to handle that is required
|
||||
//a quote CAN be deleted and it will automatically remove all events for it so also no need to remove time delayed status events either if wo is deleted.
|
||||
//so in essence there is nothing to be done regarding deleted events with states in a blanket way, however specific events below may remove them as appropriate
|
||||
// //## DELETED EVENTS
|
||||
// //A state cannot be deleted so nothing to handle that is required
|
||||
// //a quote CAN be deleted and it will automatically remove all events for it so also no need to remove time delayed status events either if wo is deleted.
|
||||
// //so in essence there is nothing to be done regarding deleted events with states in a blanket way, however specific events below may remove them as appropriate
|
||||
|
||||
|
||||
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderStatusChange);
|
||||
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderCompletedStatusOverdue);
|
||||
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderStatusAge);
|
||||
// // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderStatusChange);
|
||||
// // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderCompletedStatusOverdue);
|
||||
// // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderStatusAge);
|
||||
|
||||
|
||||
//## CREATED (this is the only possible notification CREATION ayaEvent type for a quote state as they are create only)
|
||||
if (ayaEvent == AyaEvent.Created)
|
||||
{
|
||||
//# STATUS CHANGE (create new status)
|
||||
{
|
||||
//Conditions: must match specific status id value and also tags below
|
||||
//delivery is immediate so no need to remove old ones of this kind
|
||||
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderStatusChange && z.IdValue == oProposed.QuoteStatusId).ToListAsync();
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
//not for inactive users
|
||||
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
||||
// //## CREATED (this is the only possible notification CREATION ayaEvent type for a quote state as they are create only)
|
||||
// if (ayaEvent == AyaEvent.Created)
|
||||
// {
|
||||
// //# STATUS CHANGE (create new status)
|
||||
// {
|
||||
// //Conditions: must match specific status id value and also tags below
|
||||
// //delivery is immediate so no need to remove old ones of this kind
|
||||
// var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderStatusChange && z.IdValue == oProposed.QuoteStatusId).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(WorkorderInfo.Tags, sub.Tags))
|
||||
{
|
||||
NotifyEvent n = new NotifyEvent()
|
||||
{
|
||||
EventType = NotifyEventType.WorkorderStatusChange,
|
||||
UserId = sub.UserId,
|
||||
AyaType = AyaType.Quote,
|
||||
ObjectId = oProposed.QuoteId,
|
||||
NotifySubscriptionId = sub.Id,
|
||||
Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}"
|
||||
};
|
||||
await ct.NotifyEvent.AddAsync(n);
|
||||
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
||||
await ct.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}//quote status change event
|
||||
// //Tag match? (will be true if no sub tags so always safe to call this)
|
||||
// if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
|
||||
// {
|
||||
// NotifyEvent n = new NotifyEvent()
|
||||
// {
|
||||
// EventType = NotifyEventType.WorkorderStatusChange,
|
||||
// UserId = sub.UserId,
|
||||
// AyaType = AyaType.Quote,
|
||||
// ObjectId = oProposed.QuoteId,
|
||||
// NotifySubscriptionId = sub.Id,
|
||||
// Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}"
|
||||
// };
|
||||
// await ct.NotifyEvent.AddAsync(n);
|
||||
// log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
||||
// await ct.SaveChangesAsync();
|
||||
// }
|
||||
// }
|
||||
// }//quote status change event
|
||||
|
||||
//# STATUS AGE
|
||||
{
|
||||
//WorkorderStatusAge = 24,//* Workorder STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set
|
||||
//Always clear any old ones for this object as they are all irrelevant the moment the state has changed:
|
||||
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.WorkorderStatusAge);
|
||||
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderStatusAge && z.IdValue == oProposed.QuoteStatusId).ToListAsync();
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
//not for inactive users
|
||||
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
||||
// //# STATUS AGE
|
||||
// {
|
||||
// //WorkorderStatusAge = 24,//* Workorder STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set
|
||||
// //Always clear any old ones for this object as they are all irrelevant the moment the state has changed:
|
||||
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.WorkorderStatusAge);
|
||||
// var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderStatusAge && z.IdValue == oProposed.QuoteStatusId).ToListAsync();
|
||||
// foreach (var sub in subs)
|
||||
// {
|
||||
// //not for inactive users
|
||||
// if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
||||
|
||||
//Quote Tag match? (Not State, state has no tags, will be true if no sub tags so always safe to call this)
|
||||
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
|
||||
{
|
||||
NotifyEvent n = new NotifyEvent()
|
||||
{
|
||||
EventType = NotifyEventType.WorkorderStatusAge,
|
||||
UserId = sub.UserId,
|
||||
AyaType = AyaType.Quote,
|
||||
ObjectId = oProposed.QuoteId,
|
||||
NotifySubscriptionId = sub.Id,
|
||||
Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}"
|
||||
};
|
||||
await ct.NotifyEvent.AddAsync(n);
|
||||
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
||||
await ct.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}//quote status change event
|
||||
// //Quote Tag match? (Not State, state has no tags, will be true if no sub tags so always safe to call this)
|
||||
// if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
|
||||
// {
|
||||
// NotifyEvent n = new NotifyEvent()
|
||||
// {
|
||||
// EventType = NotifyEventType.WorkorderStatusAge,
|
||||
// UserId = sub.UserId,
|
||||
// AyaType = AyaType.Quote,
|
||||
// ObjectId = oProposed.QuoteId,
|
||||
// NotifySubscriptionId = sub.Id,
|
||||
// Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}"
|
||||
// };
|
||||
// await ct.NotifyEvent.AddAsync(n);
|
||||
// log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
||||
// await ct.SaveChangesAsync();
|
||||
// }
|
||||
// }
|
||||
// }//quote status change event
|
||||
|
||||
|
||||
//# COMPLETE BY OVERDUE
|
||||
{
|
||||
//NOTE: the initial notification is created by the Workorder Header notification as it's where this time delayed notification is first generated
|
||||
//the only job here in state notification is to remove any prior finish overdue notifications waiting if a new state is selected that is a completed state
|
||||
// //# COMPLETE BY OVERDUE
|
||||
// {
|
||||
// //NOTE: the initial notification is created by the Workorder Header notification as it's where this time delayed notification is first generated
|
||||
// //the only job here in state notification is to remove any prior finish overdue notifications waiting if a new state is selected that is a completed state
|
||||
|
||||
//NOTE ABOUT RE-OPEN DECISION ON HOW THIS WORKS:
|
||||
// //NOTE ABOUT RE-OPEN DECISION ON HOW THIS WORKS:
|
||||
|
||||
//what though if it's not a Completed status, then I guess don't remove it, but what if it *was* a Completed status and it's change to a non Completed?
|
||||
//that, in essence re-opens it so it's not Completed at that point.
|
||||
//My decision on this june 2021 is that a work order Completed status notification is satisifed the moment it's saved with a Completed status
|
||||
//and nothing afterwards restarts that process so if a person sets closed status then sets open status again no new Completed overdue notification will be generated
|
||||
// //what though if it's not a Completed status, then I guess don't remove it, but what if it *was* a Completed status and it's change to a non Completed?
|
||||
// //that, in essence re-opens it so it's not Completed at that point.
|
||||
// //My decision on this june 2021 is that a work order Completed status notification is satisifed the moment it's saved with a Completed status
|
||||
// //and nothing afterwards restarts that process so if a person sets closed status then sets open status again no new Completed overdue notification will be generated
|
||||
|
||||
if (wos.Completed)
|
||||
{
|
||||
//Workorder was just set to a completed status so remove any notify events lurking to deliver for overdue
|
||||
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, oProposed.QuoteId, NotifyEventType.WorkorderCompletedStatusOverdue);
|
||||
}
|
||||
}//quote complete by overdue change event
|
||||
// if (wos.Completed)
|
||||
// {
|
||||
// //Workorder was just set to a completed status so remove any notify events lurking to deliver for overdue
|
||||
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, oProposed.QuoteId, NotifyEventType.WorkorderCompletedStatusOverdue);
|
||||
// }
|
||||
// }//quote complete by overdue change event
|
||||
|
||||
|
||||
//# WorkorderTotalExceedsThreshold / "The Andy"
|
||||
{
|
||||
if (wos.Completed)
|
||||
{
|
||||
|
||||
|
||||
//see if any subscribers to the quote total exceeds notification
|
||||
//that are active then proceed to fetch billed woitem children and total quote and send notification if necessary
|
||||
// //# WorkorderCompleted - Customer AND User but customer only notifies if it's their quote
|
||||
// {
|
||||
// if (wos.Completed)
|
||||
// {
|
||||
// //look for potential subscribers
|
||||
// var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompleted).ToListAsync();
|
||||
// foreach (var sub in subs)
|
||||
// {
|
||||
// //not for inactive users
|
||||
// if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
||||
|
||||
bool haveTotal = false;
|
||||
decimal GrandTotal = 0m;
|
||||
// //Customer User?
|
||||
// var UserInfo = await ct.User.AsNoTracking().Where(x => x.Id == sub.UserId).Select(x => new { x.CustomerId, x.UserType, x.HeadOfficeId }).FirstOrDefaultAsync();
|
||||
// if (UserInfo.UserType == UserType.Customer || UserInfo.UserType == UserType.HeadOffice)
|
||||
// {
|
||||
// //CUSTOMER USER
|
||||
|
||||
//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;
|
||||
// //Quick short circuit: if quote doesn't have a customer id then it's not going to match no matter what
|
||||
// if (WorkorderInfo.CustomerId == 0) 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;
|
||||
// var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId);
|
||||
|
||||
//get the total because we have at least one subscriber and matching tags
|
||||
if (haveTotal == false)
|
||||
{
|
||||
GrandTotal = await WorkorderGrandTotalAsync(oProposed.QuoteId, ct);
|
||||
haveTotal = true;
|
||||
// //Are they allowed right now to use this type of notification?
|
||||
// if (!customerUserRights.NotifyWOCompleted) continue;
|
||||
|
||||
//Note: not a time delayed notification, however user could be flipping states quickly triggering multiple notifications that are in queue temporarily
|
||||
//so this will prevent that:
|
||||
await NotifyEventHelper.ClearPriorEventsForObject(ct, AyaType.Quote, oProposed.QuoteId, NotifyEventType.WorkorderTotalExceedsThreshold);
|
||||
}
|
||||
//Ok, we're here because there is a subscriber who is active and tags match so only check left is total against decvalue
|
||||
if (sub.DecValue < GrandTotal)
|
||||
{
|
||||
//notification is a go
|
||||
NotifyEvent n = new NotifyEvent()
|
||||
{
|
||||
EventType = NotifyEventType.WorkorderTotalExceedsThreshold,
|
||||
UserId = sub.UserId,
|
||||
AyaType = AyaType.Quote,
|
||||
ObjectId = oProposed.QuoteId,
|
||||
NotifySubscriptionId = sub.Id,
|
||||
Name = $"{WorkorderInfo.Serial.ToString()}",
|
||||
DecValue = GrandTotal
|
||||
};
|
||||
await ct.NotifyEvent.AddAsync(n);
|
||||
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
||||
await ct.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}//"The Andy" for Dynamic Dental corp. notification
|
||||
// //is this their related work order?
|
||||
// if (UserInfo.CustomerId != WorkorderInfo.CustomerId)
|
||||
// {
|
||||
// //not the same customer but might be a head office user and this is one of their customers so check for that
|
||||
// if (UserInfo.HeadOfficeId == null) continue;//can't match any head office so no need to go further
|
||||
|
||||
// //see if quote customer's head office is the same id as the user's headofficeid (note that a customer user with the same head office as a *different* customer quote doesn't qualify)
|
||||
// var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == WorkorderInfo.CustomerId).Select(x => new { x.HeadOfficeId, x.BillHeadOffice }).FirstOrDefaultAsync();
|
||||
// if (!CustomerInfo.BillHeadOffice) continue;//can't possibly match so no need to go further
|
||||
// if (UserInfo.HeadOfficeId != CustomerInfo.HeadOfficeId) continue;
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// //INSIDE USER
|
||||
// //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;
|
||||
// }
|
||||
|
||||
//# WorkorderCompleted - Customer AND User but customer only notifies if it's their quote
|
||||
{
|
||||
if (wos.Completed)
|
||||
{
|
||||
//look for potential subscribers
|
||||
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompleted).ToListAsync();
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
//not for inactive users
|
||||
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
||||
|
||||
//Customer User?
|
||||
var UserInfo = await ct.User.AsNoTracking().Where(x => x.Id == sub.UserId).Select(x => new { x.CustomerId, x.UserType, x.HeadOfficeId }).FirstOrDefaultAsync();
|
||||
if (UserInfo.UserType == UserType.Customer || UserInfo.UserType == UserType.HeadOffice)
|
||||
{
|
||||
//CUSTOMER USER
|
||||
|
||||
//Quick short circuit: if quote doesn't have a customer id then it's not going to match no matter what
|
||||
if (WorkorderInfo.CustomerId == 0) continue;
|
||||
|
||||
var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId);
|
||||
|
||||
//Are they allowed right now to use this type of notification?
|
||||
if (!customerUserRights.NotifyWOCompleted) continue;
|
||||
|
||||
//is this their related work order?
|
||||
if (UserInfo.CustomerId != WorkorderInfo.CustomerId)
|
||||
{
|
||||
//not the same customer but might be a head office user and this is one of their customers so check for that
|
||||
if (UserInfo.HeadOfficeId == null) continue;//can't match any head office so no need to go further
|
||||
|
||||
//see if quote customer's head office is the same id as the user's headofficeid (note that a customer user with the same head office as a *different* customer quote doesn't qualify)
|
||||
var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == WorkorderInfo.CustomerId).Select(x => new { x.HeadOfficeId, x.BillHeadOffice }).FirstOrDefaultAsync();
|
||||
if (!CustomerInfo.BillHeadOffice) continue;//can't possibly match so no need to go further
|
||||
if (UserInfo.HeadOfficeId != CustomerInfo.HeadOfficeId) continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//INSIDE USER
|
||||
//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;
|
||||
}
|
||||
|
||||
//Ok, we're here so it must be ok to notify user
|
||||
NotifyEvent n = new NotifyEvent()
|
||||
{
|
||||
EventType = NotifyEventType.WorkorderCompleted,
|
||||
UserId = sub.UserId,
|
||||
AyaType = AyaType.Quote,
|
||||
ObjectId = oProposed.QuoteId,
|
||||
NotifySubscriptionId = sub.Id,
|
||||
Name = $"{WorkorderInfo.Serial.ToString()}"
|
||||
};
|
||||
await ct.NotifyEvent.AddAsync(n);
|
||||
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
||||
await ct.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}//WorkorderCompleted
|
||||
}
|
||||
// //Ok, we're here so it must be ok to notify user
|
||||
// NotifyEvent n = new NotifyEvent()
|
||||
// {
|
||||
// EventType = NotifyEventType.WorkorderCompleted,
|
||||
// UserId = sub.UserId,
|
||||
// AyaType = AyaType.Quote,
|
||||
// ObjectId = oProposed.QuoteId,
|
||||
// NotifySubscriptionId = sub.Id,
|
||||
// Name = $"{WorkorderInfo.Serial.ToString()}"
|
||||
// };
|
||||
// await ct.NotifyEvent.AddAsync(n);
|
||||
// log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
||||
// await ct.SaveChangesAsync();
|
||||
// }
|
||||
// }
|
||||
// }//WorkorderCompleted
|
||||
// }
|
||||
|
||||
}//end of process notifications
|
||||
|
||||
@@ -1981,7 +1878,7 @@ namespace AyaNova.Biz
|
||||
var qid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteId, ct);
|
||||
var WorkorderInfo = await ct.Quote.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)
|
||||
if (WorkorderInfo != null)
|
||||
oProposed.Name = WorkorderInfo.Serial.ToString();
|
||||
else
|
||||
oProposed.Name = "??";
|
||||
|
||||
Reference in New Issue
Block a user