5934 lines
286 KiB
C#
5934 lines
286 KiB
C#
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 WorkOrderBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject
|
|
{
|
|
// //Feature specific roles
|
|
// internal static AuthorizationRoles RolesAllowedToChangeSerial = AuthorizationRoles.BizAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.AccountingFull;
|
|
|
|
internal WorkOrderBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
|
|
{
|
|
ct = dbcontext;
|
|
UserId = currentUserId;
|
|
UserTranslationId = userTranslationId;
|
|
CurrentUserRoles = UserRoles;
|
|
BizType = AyaType.WorkOrder;
|
|
}
|
|
|
|
internal static WorkOrderBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
|
|
{
|
|
if (httpContext != null)
|
|
return new WorkOrderBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
|
|
else
|
|
return new WorkOrderBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗██████╗
|
|
██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝ ██╔═══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗
|
|
██║ █╗ ██║██║ ██║██████╔╝█████╔╝█████╗██║ ██║██████╔╝██║ ██║█████╗ ██████╔╝
|
|
██║███╗██║██║ ██║██╔══██╗██╔═██╗╚════╝██║ ██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗
|
|
╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗ ╚██████╔╝██║ ██║██████╔╝███████╗██║ ██║
|
|
╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝
|
|
*/
|
|
|
|
#region WorkOrder level
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> WorkOrderExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrder.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrder> WorkOrderCreateAsync(WorkOrder newObject, bool populateViz = true)
|
|
{
|
|
using (var transaction = await ct.Database.BeginTransactionAsync())
|
|
{
|
|
await WorkOrderValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
await WorkOrderBizActionsAsync(AyaEvent.Created, newObject, null, null);
|
|
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrder.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
|
|
await WorkOrderSearchIndexAsync(newObject, true);
|
|
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
|
|
//# NOTE: only internal code can post an entire workorder 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);
|
|
|
|
|
|
if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero)
|
|
newObject.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime);
|
|
|
|
//GRANDCHILD BIZ ACTIONS
|
|
foreach (WorkOrderItem wi in newObject.Items)
|
|
{
|
|
foreach (WorkOrderItemPart wip in wi.Parts)
|
|
await PartBizActionsAsync(AyaEvent.Created, wip, null, null);
|
|
foreach (WorkOrderItemLoan wil in wi.Loans)
|
|
await LoanBizActionsAsync(AyaEvent.Created, wil, null, null);
|
|
}
|
|
await ct.SaveChangesAsync();
|
|
|
|
//INVENTORY ADJUSTMENTS
|
|
foreach (WorkOrderItem wi in newObject.Items)
|
|
{
|
|
foreach (WorkOrderItemPart wip in wi.Parts)
|
|
{
|
|
await PartInventoryAdjustmentAsync(AyaEvent.Created, wip, null, transaction);
|
|
if (HasErrors)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//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 WorkOrderPopulateVizFields(newObject, true);
|
|
|
|
await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DUPLICATE
|
|
//
|
|
internal async Task<WorkOrder> WorkOrderDuplicateAsync(long id)
|
|
{
|
|
WorkOrder dbObject = await WorkOrderGetAsync(id, false);
|
|
if (dbObject == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return null;
|
|
}
|
|
WorkOrder newObject = new WorkOrder();
|
|
CopyObject.Copy(dbObject, newObject, "Wiki, Serial, States");
|
|
|
|
//walk the tree and reset all id's and concurrencies
|
|
//TOP
|
|
newObject.Id = 0;
|
|
newObject.Concurrency = 0;
|
|
foreach (var o in newObject.Items)
|
|
{
|
|
o.Id = 0;
|
|
o.Concurrency = 0;
|
|
foreach (var v in o.Expenses)
|
|
{ v.Id = 0; v.Concurrency = 0; }
|
|
foreach (var v in o.Labors)
|
|
{ v.Id = 0; v.Concurrency = 0; }
|
|
foreach (var v in o.Loans)
|
|
{ v.Id = 0; v.Concurrency = 0; }
|
|
foreach (var v in o.OutsideServices)
|
|
{ v.Id = 0; v.Concurrency = 0; }
|
|
foreach (var v in o.PartRequests)
|
|
{ v.Id = 0; v.Concurrency = 0; }
|
|
foreach (var v in o.Parts)
|
|
{ v.Id = 0; v.Concurrency = 0; }
|
|
foreach (var v in o.ScheduledUsers)
|
|
{ v.Id = 0; v.Concurrency = 0; }
|
|
foreach (var v in o.Tasks)
|
|
{ v.Id = 0; v.Concurrency = 0; }
|
|
foreach (var v in o.Travels)
|
|
{ v.Id = 0; v.Concurrency = 0; }
|
|
foreach (var v in o.Units)
|
|
{ v.Id = 0; v.Concurrency = 0; }
|
|
}
|
|
|
|
|
|
|
|
await ct.WorkOrder.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
|
|
await WorkOrderSearchIndexAsync(newObject, true);
|
|
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await WorkOrderPopulateVizFields(newObject, false);//doing this here ahead of notification because notification may require the viz field lookup anyway and afaict no harm in it
|
|
await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
return newObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrder> WorkOrderGetAsync(long id, bool populateDisplayFields, bool logTheGetEvent = true)
|
|
{
|
|
//Note: there could be rules checking here in future, i.e. can only get own workorder 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.WorkOrder.AsSplitQuery().AsNoTracking()
|
|
.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 == id);
|
|
|
|
|
|
//todo: set isLocked from state
|
|
var stat = await GetCurrentWorkOrderStatusFromRelatedAsync(BizType, ret.Id);
|
|
ret.IsLockedAtServer = stat.Locked;
|
|
|
|
|
|
if (populateDisplayFields)
|
|
await WorkOrderPopulateVizFields(ret, false);
|
|
|
|
if (logTheGetEvent && ret != null)
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct);
|
|
return ret;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//UPDATE
|
|
//
|
|
internal async Task<WorkOrder> WorkOrderPutAsync(WorkOrder 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
|
|
WorkOrder dbObject = await ct.WorkOrder.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 WorkOrderValidateAsync(putObject, dbObject);
|
|
if (HasErrors)
|
|
return null;
|
|
await WorkOrderBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
|
|
|
|
long? newContractId = null;
|
|
if (putObject.ContractId != dbObject.ContractId)//manual change of contract
|
|
{
|
|
newContractId = putObject.ContractId;
|
|
await GetCurrentContractFromContractIdAsync(newContractId);
|
|
if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero)
|
|
putObject.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime);
|
|
}
|
|
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await WorkOrderExistsAsync(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 WorkOrderSearchIndexAsync(putObject, false);
|
|
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
|
|
await WorkOrderPopulateVizFields(putObject, true);//doing this here ahead of notification because notification may require the viz field lookup anyway and afaict no harm in it
|
|
await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> WorkOrderDeleteAsync(long id)
|
|
{
|
|
using (var transaction = await ct.Database.BeginTransactionAsync())
|
|
{
|
|
try
|
|
{
|
|
WorkOrder dbObject = await ct.WorkOrder.AsNoTracking().Where(z => z.Id == id).FirstOrDefaultAsync();// WorkOrderGetAsync(id, false);
|
|
if (dbObject == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND);
|
|
return false;
|
|
}
|
|
WorkOrderValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
|
|
//States collection
|
|
if (!await StateDeleteAsync(id, transaction))
|
|
return false;
|
|
|
|
//collect the child id's to delete
|
|
var ItemIds = await ct.WorkOrderItem.AsNoTracking().Where(z => z.WorkOrderId == id).Select(z => z.Id).ToListAsync();
|
|
|
|
//Delete children
|
|
foreach (long ItemId in ItemIds)
|
|
if (!await ItemDeleteAsync(ItemId, transaction))
|
|
return false;
|
|
|
|
ct.WorkOrder.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 WorkOrderHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//NOTE: no need to rollback the transaction, it will auto-rollback if not committed and it is disposed when it goes out of scope either way
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//BIZ ACTIONS
|
|
//
|
|
//
|
|
private async Task WorkOrderBizActionsAsync(AyaEvent ayaEvent, WorkOrder newObj, WorkOrder 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;
|
|
|
|
//CREATION ACTIONS
|
|
if (ayaEvent == AyaEvent.Created)
|
|
{
|
|
await AutoSetContractAsync(newObj);
|
|
if (newObj.CompleteByDate == null)//need to account for a user manually selecting a specific close by date in advance indicating to ignore any auto sets
|
|
await AutoSetCloseByDateAsync(newObj);
|
|
await AutoSetAddressAsync(newObj);
|
|
return;
|
|
}
|
|
|
|
//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);
|
|
}
|
|
|
|
if (newObj.ContractId != oldObj.ContractId)
|
|
await AutoSetCloseByDateAsync(newObj);
|
|
}
|
|
}
|
|
|
|
private async Task AutoSetAddressAsync(WorkOrder newObj)
|
|
{
|
|
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.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(WorkOrder 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task AutoSetCloseByDateAsync(WorkOrder newObj)
|
|
{
|
|
//called when there is a definite possibility of change of close by i.e. new contract, new customer, new workorder
|
|
//RESPONSE TIME / COMPLETE BY AUTO SET
|
|
//precedence: manually pre-set -> contract -> global biz
|
|
if (newObj.ContractId != null)
|
|
{
|
|
await GetCurrentContractFromContractIdAsync(newObj.ContractId);
|
|
if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero)
|
|
{
|
|
newObj.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime);
|
|
return; //our work here is done
|
|
}
|
|
}
|
|
|
|
//not set yet, maybe the global default is the way...
|
|
if (AyaNova.Util.ServerGlobalBizSettings.WorkOrderCompleteByAge != TimeSpan.Zero)
|
|
newObj.CompleteByDate = DateTime.UtcNow.Add(AyaNova.Util.ServerGlobalBizSettings.WorkOrderCompleteByAge);
|
|
|
|
}
|
|
|
|
// ////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //CONTRACT UPDATE
|
|
// //
|
|
// internal async Task<WorkOrder> 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.WorkOrder.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 updatedWorkOrder = await ProcessChangeOfContractAsync(workOrderId);
|
|
// await WorkOrderPopulateVizFields(updatedWorkOrder, false);
|
|
// return updatedWorkOrder;//return entire workorder
|
|
// }
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//GET WORKORDER ID FROM DESCENDANT TYPE AND ID
|
|
//
|
|
internal static async Task<long> GetWorkOrderIdFromRelativeAsync(AyaType ayaType, long id, AyContext ct)
|
|
{
|
|
long woitemid = 0;
|
|
switch (ayaType)
|
|
{
|
|
case AyaType.WorkOrder:
|
|
return id;
|
|
case AyaType.WorkOrderItem:
|
|
woitemid = id;
|
|
break;
|
|
case AyaType.WorkOrderItemExpense:
|
|
woitemid = await ct.WorkOrderItemExpense.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
|
|
break;
|
|
case AyaType.WorkOrderItemLabor:
|
|
woitemid = await ct.WorkOrderItemLabor.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
|
|
break;
|
|
case AyaType.WorkOrderItemLoan:
|
|
woitemid = await ct.WorkOrderItemLoan.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
|
|
break;
|
|
case AyaType.WorkOrderItemPart:
|
|
woitemid = await ct.WorkOrderItemPart.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
|
|
break;
|
|
case AyaType.WorkOrderItemPartRequest:
|
|
woitemid = await ct.WorkOrderItemPartRequest.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
|
|
break;
|
|
case AyaType.WorkOrderItemScheduledUser:
|
|
woitemid = await ct.WorkOrderItemScheduledUser.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
|
|
break;
|
|
case AyaType.WorkOrderItemTask:
|
|
woitemid = await ct.WorkOrderItemTask.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
|
|
break;
|
|
case AyaType.WorkOrderItemTravel:
|
|
woitemid = await ct.WorkOrderItemTravel.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
|
|
break;
|
|
case AyaType.WorkOrderItemOutsideService:
|
|
woitemid = await ct.WorkOrderItemOutsideService.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
|
|
break;
|
|
case AyaType.WorkOrderStatus:
|
|
return await ct.WorkOrderState.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderId).SingleOrDefaultAsync();
|
|
case AyaType.WorkOrderItemUnit:
|
|
woitemid = await ct.WorkOrderItemUnit.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
|
|
break;
|
|
default:
|
|
throw new System.NotSupportedException($"WorkOrderBiz::GetAncestor -> AyaType {ayaType.ToString()} is not supported");
|
|
}
|
|
return await ct.WorkOrderItem.AsNoTracking()
|
|
.Where(z => z.Id == woitemid)
|
|
.Select(z => z.WorkOrderId)
|
|
.SingleOrDefaultAsync();
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//SEARCH
|
|
//
|
|
private async Task WorkOrderSearchIndexAsync(WorkOrder 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<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id)
|
|
{
|
|
var obj = await ct.WorkOrder.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);//# NOTE intentionally not calling workorder get async here, don't need the whole graph
|
|
var SearchParams = new Search.SearchIndexProcessObjectParameters();
|
|
DigestSearchText(obj, SearchParams);
|
|
return SearchParams;
|
|
}
|
|
|
|
public void DigestSearchText(WorkOrder obj, Search.SearchIndexProcessObjectParameters searchParams)
|
|
{
|
|
if (obj != null)
|
|
searchParams.AddText(obj.Notes).AddText(obj.Serial).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
|
|
}
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// "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.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;
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
|
|
//Can save or update?
|
|
private async Task WorkOrderValidateAsync(WorkOrder proposedObj, WorkOrder currentObj)
|
|
{
|
|
|
|
//This may become necessary for v8migrate, leaving out for now
|
|
//skip validation if seeding
|
|
//if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
|
todo: workorder 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 workorder 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 workorder 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.WorkOrder.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.WorkOrder.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 void WorkOrderValidateCanDelete(WorkOrder dbObject)
|
|
{
|
|
//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"));
|
|
}
|
|
|
|
|
|
//############### NOTIFICATION TODO
|
|
/*
|
|
|
|
todo: workorder 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 workorder closed status follow up again after this many TIMESPAN
|
|
|
|
todo: CHANGE WorkorderCompletedStatusOverdue = 15,//* Workorder object not set to a "Completed" flagged workorder status type in selected time span from creation of workorder
|
|
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
|
|
|
|
*/
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//REPORTING
|
|
//
|
|
public async Task<JArray> GetReportData(long[] idList)
|
|
{
|
|
|
|
|
|
JArray ReportData = new JArray();
|
|
while (idList.Any())
|
|
{
|
|
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
|
|
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
|
|
List<WorkOrder> batchResults = new List<WorkOrder>();
|
|
foreach (long batchId in batch)
|
|
batchResults.Add(await WorkOrderGetAsync(batchId, true, false));
|
|
|
|
//order the results back into original
|
|
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
|
|
|
|
|
|
|
|
//TODO: ##HERE## SEE ContractBiz for example implementation; will need to pre-cache some translation keys for enum lists etc, for example:
|
|
// private async Task TaskPopulateVizFields(WorkOrderItemTask o, List<NameIdItem> taskCompletionTypeEnumList = null)
|
|
|
|
|
|
|
|
foreach (WorkOrder w in orderedList)
|
|
{
|
|
//populate entire workorder graph
|
|
//await WorkOrderPopulateVizFields(w);
|
|
//this is done by the initial fetch now
|
|
|
|
|
|
var jo = JObject.FromObject(w);
|
|
|
|
//WorkOrder header custom fields
|
|
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
|
|
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
|
|
|
|
//WorkOrderItem custom fields
|
|
foreach (JObject jItem in jo["Items"])
|
|
{
|
|
if (!JsonUtil.JTokenIsNullOrEmpty(jItem["CustomFields"]))
|
|
jItem["CustomFields"] = JObject.Parse((string)jItem["CustomFields"]);
|
|
|
|
//WorkOrderItemUnit custom fields
|
|
foreach (JObject jUnit in jItem["Units"])
|
|
{
|
|
if (!JsonUtil.JTokenIsNullOrEmpty(jUnit["CustomFields"]))
|
|
jUnit["CustomFields"] = JObject.Parse((string)jUnit["CustomFields"]);
|
|
}
|
|
}
|
|
ReportData.Add(jo);
|
|
}
|
|
}
|
|
return ReportData;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VIZ POPULATE
|
|
//
|
|
private async Task WorkOrderPopulateVizFields(WorkOrder o, bool headerOnly)
|
|
{
|
|
if (!headerOnly)
|
|
{
|
|
foreach (var v in o.States)
|
|
await StatePopulateVizFields(v);
|
|
foreach (var v in o.Items)
|
|
await ItemPopulateVizFields(v);
|
|
}
|
|
|
|
//popup Alert notes
|
|
//Customer notes first then others below
|
|
var custInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => new { AlertViz = x.PopUpNotes, CustomerViz = x.Name }).FirstOrDefaultAsync();
|
|
if (!string.IsNullOrWhiteSpace(custInfo.AlertViz))
|
|
{
|
|
o.AlertViz = $"{await Translate("Customer")}\n{custInfo.AlertViz}\n\n";
|
|
}
|
|
|
|
o.CustomerViz = custInfo.CustomerViz;
|
|
|
|
if (o.ProjectId != null)
|
|
o.ProjectViz = await ct.Project.AsNoTracking().Where(x => x.Id == o.ProjectId).Select(x => x.Name).FirstOrDefaultAsync();
|
|
|
|
if (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();
|
|
o.ContractViz = contractVizFields.Name;
|
|
if (!string.IsNullOrWhiteSpace(contractVizFields.AlertNotes))
|
|
{
|
|
o.AlertViz += $"{await Translate("Contract")}\n{contractVizFields.AlertNotes}\n\n";
|
|
}
|
|
}
|
|
else
|
|
o.ContractViz = "-";
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// IMPORT EXPORT
|
|
//
|
|
|
|
public async Task<JArray> GetExportData(long[] idList)
|
|
{
|
|
//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(idList);
|
|
}
|
|
|
|
// 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<WorkOrder>(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 WorkOrderCreateAsync(w);
|
|
// if (res == null)
|
|
// {
|
|
// ImportResult.Add($"* {w.Serial} - {this.GetErrorsAsString()}");
|
|
// this.ClearErrors();
|
|
// }
|
|
// else
|
|
// {
|
|
// ImportResult.Add($"{w.Serial} - ok");
|
|
// }
|
|
// }
|
|
// return ImportResult;
|
|
// }
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//JOB / OPERATIONS
|
|
//
|
|
public async Task HandleJobAsync(OpsJob job)
|
|
{
|
|
switch (job.JobType)
|
|
{
|
|
case JobType.BatchCoreObjectOperation:
|
|
await ProcessBatchJobAsync(job);
|
|
break;
|
|
default:
|
|
throw new System.ArgumentOutOfRangeException($"WorkOrder.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<long> idList = new List<long>();
|
|
long FailedObjectCount = 0;
|
|
JObject jobData = JObject.Parse(job.JobInfo);
|
|
if (jobData.ContainsKey("idList"))
|
|
idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
|
|
else
|
|
idList = await ct.Widget.Select(z => z.Id).ToListAsync();
|
|
bool SaveIt = false;
|
|
foreach (long id in idList)
|
|
{
|
|
try
|
|
{
|
|
SaveIt = false;
|
|
ClearErrors();
|
|
ICoreBizObjectModel o = null;
|
|
//save a fetch if it's a delete
|
|
if (job.SubType != JobSubType.Delete)
|
|
o = await GetWorkOrderGraphItem(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 DeleteWorkOrderGraphItem(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 PutWorkOrderGraphItem(job.AType, o);
|
|
if (o == null)
|
|
{
|
|
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
|
|
FailedObjectCount++;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})");
|
|
await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
|
|
}
|
|
}
|
|
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 WorkOrderHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
|
|
WorkOrder oProposed = (WorkOrder)proposedObj;
|
|
proposedObj.Name = oProposed.Serial.ToString();
|
|
|
|
//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
|
|
|
|
WorkOrder oCurrent = null;
|
|
bool SameTags = true;
|
|
if (currentObj != null)
|
|
{
|
|
oCurrent = (WorkOrder)currentObj;
|
|
SameTags = NotifyEventHelper.TwoObjectsHaveSameTags(proposedObj.Tags, currentObj.Tags);
|
|
}
|
|
|
|
|
|
#region COMPLETE BY OVERDUE
|
|
|
|
if (ayaEvent == AyaEvent.Created && oProposed.CompleteByDate != null)
|
|
{
|
|
//WorkorderCompletedStatusOverdue Created here on workorder creation for any subscribers
|
|
//State notify event processor below has more notes and details and will remove this event if set to a completed state
|
|
//If new and has completeby then can do notification
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompletedStatusOverdue).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.WorkorderCompletedStatusOverdue,
|
|
UserId = sub.UserId,
|
|
AyaType = proposedObj.AyaType,
|
|
ObjectId = proposedObj.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = oProposed.Serial.ToString(),
|
|
EventDate = (DateTime)oProposed.CompleteByDate
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
}//CREATED overdue completion
|
|
|
|
if (ayaEvent == AyaEvent.Modified)
|
|
{// WorkorderCompletedStatusOverdue 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.CompleteByDate != oCurrent.CompleteByDate || !SameTags)
|
|
{
|
|
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.WorkorderCompletedStatusOverdue);
|
|
|
|
//new has date?
|
|
if (oProposed.CompleteByDate != null)
|
|
{
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompletedStatusOverdue).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.WorkorderCompletedStatusOverdue,
|
|
UserId = sub.UserId,
|
|
AyaType = proposedObj.AyaType,
|
|
ObjectId = proposedObj.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = oProposed.Serial.ToString(),
|
|
EventDate = (DateTime)oProposed.CompleteByDate
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}//CREATED overdue completion
|
|
|
|
#endregion
|
|
|
|
|
|
#region CustomerServiceImminent
|
|
if (ayaEvent == AyaEvent.Created && oProposed.ServiceDate != null && oProposed.ServiceDate > DateTime.UtcNow)
|
|
{
|
|
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.CustomerServiceImminent).ToListAsync();
|
|
foreach (var sub in subs)
|
|
{
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
|
|
|
//No tag match for this one
|
|
|
|
NotifyEvent n = new NotifyEvent()
|
|
{
|
|
EventType = NotifyEventType.CustomerServiceImminent,
|
|
UserId = sub.UserId,
|
|
AyaType = proposedObj.AyaType,
|
|
ObjectId = proposedObj.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = oProposed.Serial.ToString(),
|
|
EventDate = (DateTime)oProposed.ServiceDate
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
|
|
}
|
|
|
|
}//CustomerServiceImminent
|
|
|
|
if (ayaEvent == AyaEvent.Modified)
|
|
{// CustomerServiceImminent
|
|
|
|
//differences requiring re-processing of notification
|
|
if (oProposed.ServiceDate != oCurrent.ServiceDate)
|
|
{
|
|
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.CustomerServiceImminent);
|
|
|
|
//new has date?
|
|
if (oProposed.ServiceDate != null && oProposed.ServiceDate > DateTime.UtcNow)
|
|
{
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.CustomerServiceImminent).ToListAsync();
|
|
|
|
foreach (var sub in subs)
|
|
{
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
|
|
|
//No tag match for this one
|
|
NotifyEvent n = new NotifyEvent()
|
|
{
|
|
EventType = NotifyEventType.CustomerServiceImminent,
|
|
UserId = sub.UserId,
|
|
AyaType = proposedObj.AyaType,
|
|
ObjectId = proposedObj.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = oProposed.Serial.ToString(),
|
|
EventDate = (DateTime)oProposed.ServiceDate
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
}//CustomerServiceImminent
|
|
|
|
#endregion
|
|
|
|
}//end of process notifications
|
|
|
|
|
|
|
|
#endregion workorder level
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
███████╗████████╗ █████╗ ████████╗███████╗███████╗
|
|
██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██╔════╝██╔════╝
|
|
███████╗ ██║ ███████║ ██║ █████╗ ███████╗
|
|
╚════██║ ██║ ██╔══██║ ██║ ██╔══╝ ╚════██║
|
|
███████║ ██║ ██║ ██║ ██║ ███████╗███████║
|
|
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝
|
|
|
|
*/
|
|
|
|
|
|
#region WorkOrderState level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> StateExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderState.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderState> StateCreateAsync(WorkOrderState newObject)
|
|
{
|
|
await StateValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
await ct.WorkOrderState.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderStatus, AyaEvent.Created), ct);
|
|
await StateHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderState> StateGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
//Note: there could be rules checking here in future, i.e. can only get own workorder 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.WorkOrderState.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
|
|
if (logTheGetEvent && ret != null)
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderStatus, AyaEvent.Retrieved), ct);
|
|
return ret;
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VIZ POPULATE
|
|
//
|
|
private async Task StatePopulateVizFields(WorkOrderState o)
|
|
{
|
|
|
|
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
|
|
// if (o.WorkOrderOverseerId != null)
|
|
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
// (note: this would only ever be called when a workorder is deleted, there is no direct delete)
|
|
internal async Task<bool> StateDeleteAsync(long workOrderId, IDbContextTransaction parentTransaction)
|
|
{
|
|
try
|
|
{
|
|
var stateList = await ct.WorkOrderState.AsNoTracking().Where(z => z.WorkOrderId == workOrderId).ToListAsync();
|
|
|
|
foreach (var wostate in stateList)
|
|
{
|
|
ct.WorkOrderState.Remove(wostate);
|
|
await ct.SaveChangesAsync();
|
|
//no need to call this because it's only going to run this method if the workorder is deleted and
|
|
//via process standard notifciation events for workorder deletion will remove any state delayed notifications anyway so
|
|
//nothing to call or do here related to notification
|
|
// await StateHandlePotentialNotificationEvent(AyaEvent.Deleted, wostate);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task StateValidateAsync(WorkOrderState proposedObj, WorkOrderState currentObj)
|
|
{
|
|
// //skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
//does it have a valid workorder id
|
|
if (proposedObj.WorkOrderId == 0)
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderId");
|
|
else if (!await WorkOrderExistsAsync(proposedObj.WorkOrderId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderId");
|
|
}
|
|
}
|
|
|
|
|
|
// private void StateValidateCanDelete(WorkOrderState obj)
|
|
// {
|
|
// 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.WorkOrderStatus))
|
|
// {
|
|
// AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
// return;
|
|
// }
|
|
// }
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task StateHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderState oProposed = (WorkOrderState)proposedObj;
|
|
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == oProposed.WorkOrderId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
WorkOrderStatus wos = await ct.WorkOrderStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == oProposed.WorkOrderStatusId);
|
|
oProposed.Name = WorkorderInfo.Serial.ToString();
|
|
oProposed.Tags = WorkorderInfo.Tags;
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
//NONE: state notifications are specific and not the same as for general objects so don't process standard events
|
|
|
|
//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 workorder 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 workorder 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);
|
|
|
|
|
|
//## CREATED (this is the only possible notification CREATION ayaEvent type for a workorder 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.WorkOrderStatusId).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.WorkOrder,
|
|
ObjectId = oProposed.WorkOrderId,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}//workorder 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.WorkOrderStatusId).ToListAsync();
|
|
foreach (var sub in subs)
|
|
{
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
|
|
|
//WorkOrder 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.WorkOrder,
|
|
ObjectId = oProposed.WorkOrderId,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}//workorder 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
|
|
|
|
//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
|
|
|
|
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.WorkOrderId, NotifyEventType.WorkorderCompletedStatusOverdue);
|
|
}
|
|
}//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 GrandTotal = 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)
|
|
{
|
|
GrandTotal = await WorkorderGrandTotalAsync(oProposed.WorkOrderId, ct);
|
|
haveTotal = true;
|
|
|
|
//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.WorkOrder, oProposed.WorkOrderId, 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.WorkOrder,
|
|
ObjectId = oProposed.WorkOrderId,
|
|
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
|
|
|
|
|
|
|
|
}
|
|
|
|
}//end of process notifications
|
|
|
|
#endregion work order STATE level
|
|
|
|
|
|
/*
|
|
██╗████████╗███████╗███╗ ███╗███████╗
|
|
██║╚══██╔══╝██╔════╝████╗ ████║██╔════╝
|
|
██║ ██║ █████╗ ██╔████╔██║███████╗
|
|
██║ ██║ ██╔══╝ ██║╚██╔╝██║╚════██║
|
|
██║ ██║ ███████╗██║ ╚═╝ ██║███████║
|
|
╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝
|
|
*/
|
|
|
|
#region WorkOrderItem level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> ItemExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItem.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItem> ItemCreateAsync(WorkOrderItem newObject)
|
|
{
|
|
await ItemValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItem.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItem, AyaEvent.Created), ct);
|
|
await ItemSearchIndexAsync(newObject, true);
|
|
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await ItemHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
await ItemPopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItem> ItemGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
//Note: there could be rules checking here in future, i.e. can only get own workorder 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.WorkOrderItem.AsSplitQuery().AsNoTracking()
|
|
.Include(wi => wi.Expenses)
|
|
.Include(wi => wi.Labors)
|
|
.Include(wi => wi.Loans)
|
|
.Include(wi => wi.Parts)
|
|
.Include(wi => wi.PartRequests)
|
|
.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.WorkOrderItem, AyaEvent.Retrieved), ct);
|
|
return ret;
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//UPDATE
|
|
//
|
|
internal async Task<WorkOrderItem> ItemPutAsync(WorkOrderItem 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.WorkOrderItem.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.WorkOrderItem, AyaEvent.Modified), ct);
|
|
await ItemSearchIndexAsync(putObject, false);
|
|
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
|
|
await ItemHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await ItemPopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> ItemDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
var dbObject = await ct.WorkOrderItem.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.WorkOrderItemExpense.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
|
|
var LaborIds = await ct.WorkOrderItemLabor.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
|
|
var LoanIds = await ct.WorkOrderItemLoan.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
|
|
var PartIds = await ct.WorkOrderItemPart.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
|
|
var PartRequestIds = await ct.WorkOrderItemPartRequest.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
|
|
var ScheduledUserIds = await ct.WorkOrderItemScheduledUser.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
|
|
var TaskIds = await ct.WorkOrderItemTask.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
|
|
var TravelIds = await ct.WorkOrderItemTravel.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
|
|
var UnitIds = await ct.WorkOrderItemUnit.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
|
|
var OutsideServiceIds = await ct.WorkOrderItemOutsideService.Where(z => z.WorkOrderItemId == 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 PartRequestIds)
|
|
if (!await PartRequestDeleteAsync(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.WorkOrderItem.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "wo:" + dbObject.WorkOrderId.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);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private async Task ItemSearchIndexAsync(WorkOrderItem obj, bool isNew)
|
|
{
|
|
//SEARCH INDEXING
|
|
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItem);
|
|
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<Search.SearchIndexProcessObjectParameters> ItemGetSearchResultSummary(long id)
|
|
{
|
|
var obj = await ct.WorkOrderItem.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.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
|
|
return SearchParams;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VIZ POPULATE
|
|
//
|
|
private async Task ItemPopulateVizFields(WorkOrderItem o)
|
|
{
|
|
// if (o.WorkOrderOverseerId != null)
|
|
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
|
|
|
|
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.PartRequests)
|
|
await PartRequestPopulateVizFields(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);
|
|
|
|
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task ItemValidateAsync(WorkOrderItem proposedObj, WorkOrderItem currentObj)
|
|
{
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
//does it have a valid workorder id
|
|
if (proposedObj.WorkOrderId == 0)
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderId");
|
|
else if (!await WorkOrderExistsAsync(proposedObj.WorkOrderId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderId");
|
|
}
|
|
|
|
//summary is required now, this is a change from v7
|
|
//I did this because it is required in terms of hiding on the form so it also
|
|
//is required to have a value. This is really because the form field customization I took away the hideable field
|
|
//maybe I should add that feature back?
|
|
if (proposedObj.WorkOrderId == 0)
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderId");
|
|
|
|
|
|
// // //TEST TEST TEST
|
|
// if (string.IsNullOrWhiteSpace(proposedObj.Notes))
|
|
// {
|
|
// AddError(ApiErrorCode.VALIDATION_REQUIRED, "Notes");
|
|
// }
|
|
// if (proposedObj.Notes.Contains("blah"))
|
|
// {
|
|
// ;
|
|
// }
|
|
// if (proposedObj.Notes != null && proposedObj.Notes.Contains("generalerror"))
|
|
// {
|
|
// AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", "Test general error");
|
|
// }
|
|
// if (proposedObj.Notes != null && proposedObj.Notes.Contains("aytesterror"))
|
|
// {
|
|
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Notes", "SAVE TEST ERROR");
|
|
// }
|
|
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
}
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItem.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 ItemValidateCanDelete(WorkOrderItem obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
// //TEST TEST TEST
|
|
// if (obj.Notes != null && obj.Notes.Contains("aytesterror"))
|
|
// {
|
|
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, $"Notes", "DELETE TEST ERROR");
|
|
// }
|
|
|
|
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItem))
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task ItemHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
|
|
WorkOrderItem oProposed = (WorkOrderItem)proposedObj;
|
|
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
oProposed.Name = WorkorderInfo.Serial.ToString();
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
|
|
//## DELETED EVENTS
|
|
//any event added below needs to be removed, so
|
|
//just blanket remove any event for this object of eventtype that would be added below here
|
|
//do it regardless any time there's an update and then
|
|
//let this code below handle the refreshing addition that could have changes
|
|
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.ContractExpiring);
|
|
|
|
|
|
//## CREATED / MODIFIED EVENTS
|
|
if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
|
|
{
|
|
|
|
//todo: fix etc, tons of shit here incoming
|
|
|
|
}
|
|
|
|
}//end of process notifications
|
|
|
|
|
|
|
|
#endregion work order item level
|
|
|
|
|
|
/*
|
|
███████╗██╗ ██╗██████╗ ███████╗███╗ ██╗███████╗███████╗███████╗
|
|
██╔════╝╚██╗██╔╝██╔══██╗██╔════╝████╗ ██║██╔════╝██╔════╝██╔════╝
|
|
█████╗ ╚███╔╝ ██████╔╝█████╗ ██╔██╗ ██║███████╗█████╗ ███████╗
|
|
██╔══╝ ██╔██╗ ██╔═══╝ ██╔══╝ ██║╚██╗██║╚════██║██╔══╝ ╚════██║
|
|
███████╗██╔╝ ██╗██║ ███████╗██║ ╚████║███████║███████╗███████║
|
|
╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚══════╝╚══════╝
|
|
*/
|
|
|
|
#region WorkOrderItemExpense level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> ExpenseExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItemExpense.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItemExpense> ExpenseCreateAsync(WorkOrderItemExpense newObject)
|
|
{
|
|
await ExpenseValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
//await ExpenseBizActionsAsync(AyaEvent.Created, newObject, null, null);
|
|
// newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
// newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItemExpense.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
|
|
await ExpenseSearchIndexAsync(newObject, true);
|
|
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
await ExpensePopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemExpense> ExpenseGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemExpense.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<WorkOrderItemExpense> ExpensePutAsync(WorkOrderItemExpense 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;
|
|
}
|
|
// dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
|
|
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
|
|
await ExpenseValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
//await ExpenseBizActionsAsync(AyaEvent.Modified, putObject, dbObject, 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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
|
|
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await ExpensePopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> ExpenseDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
var dbObject = await ExpenseGetAsync(id, false);
|
|
ExpenseValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.WorkOrderItemExpense.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
|
|
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
|
|
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
|
|
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//////////////////////////////////////////////
|
|
//INDEXING
|
|
//
|
|
private async Task ExpenseSearchIndexAsync(WorkOrderItemExpense 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<Search.SearchIndexProcessObjectParameters> 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(WorkOrderItemExpense o, bool calculateTotalsOnly = false)
|
|
{
|
|
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);
|
|
if (Tax != null)
|
|
o.ChargeTaxCodeViz = Tax.Name;
|
|
|
|
//Calculate totals and taxes
|
|
o.TaxAViz = 0;
|
|
o.TaxBViz = 0;
|
|
|
|
if (Tax != null)
|
|
{
|
|
if (Tax.TaxAPct != 0)
|
|
{
|
|
o.TaxAViz = o.ChargeAmount * (Tax.TaxAPct / 100);
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = (o.ChargeAmount + o.TaxAViz) * (Tax.TaxBPct / 100);
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = o.ChargeAmount * (Tax.TaxBPct / 100);
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.ChargeAmount + o.TaxAViz + o.TaxBViz;
|
|
}
|
|
|
|
|
|
// ////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //BIZ ACTIONS
|
|
// //
|
|
// //
|
|
// private async Task ExpenseBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemExpense newObj, WorkOrderItemExpense 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;
|
|
|
|
// //SET TAXES AND PRICING
|
|
|
|
// //by default apply all automatic actions with further restrictions possible below
|
|
// bool ApplyTax = true;
|
|
|
|
|
|
// //if modifed, see what has changed and should be re-applied
|
|
// if (ayaEvent == AyaEvent.Modified)
|
|
// {
|
|
|
|
// //If taxes haven't change then no need to update taxes
|
|
// if (newObj.ChargeTaxCodeId == oldObj.ChargeTaxCodeId)
|
|
// ApplyTax = false;
|
|
// }
|
|
|
|
// //Tax code
|
|
// if (ApplyTax)
|
|
// {
|
|
// //Default in case nothing to apply
|
|
// newObj.TaxAPct = 0;
|
|
// newObj.TaxBPct = 0;
|
|
// newObj.TaxOnTax = false;
|
|
// newObj.TaxName = "";
|
|
|
|
// if (newObj.ChargeTaxCodeId != null)
|
|
// {
|
|
// var t = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.ChargeTaxCodeId);
|
|
// if (t != null)
|
|
// {
|
|
// newObj.TaxAPct = t.TaxAPct;
|
|
// newObj.TaxBPct = t.TaxBPct;
|
|
// newObj.TaxOnTax = t.TaxOnTax;
|
|
// newObj.TaxName = t.Name;
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task ExpenseValidateAsync(WorkOrderItemExpense proposedObj, WorkOrderItemExpense currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (proposedObj.WorkOrderItemId == 0)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
|
|
}
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemExpense.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(WorkOrderItemExpense obj)
|
|
{
|
|
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.WorkOrderItemExpense))
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task ExpenseHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemExpense oProposed = (WorkOrderItemExpense)proposedObj;
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
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 EXPENSE level
|
|
|
|
|
|
/*
|
|
██╗ █████╗ ██████╗ ██████╗ ██████╗
|
|
██║ ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗
|
|
██║ ███████║██████╔╝██║ ██║██████╔╝
|
|
██║ ██╔══██║██╔══██╗██║ ██║██╔══██╗
|
|
███████╗██║ ██║██████╔╝╚██████╔╝██║ ██║
|
|
╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝
|
|
*/
|
|
|
|
#region WorkOrderItemLabor level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> LaborExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItemLabor.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItemLabor> LaborCreateAsync(WorkOrderItemLabor newObject)
|
|
{
|
|
await LaborValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
// await LaborBizActionsAsync(AyaEvent.Created, newObject, null, null);
|
|
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItemLabor.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
|
|
await LaborSearchIndexAsync(newObject, true);
|
|
// await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await LaborHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
await LaborPopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemLabor> LaborGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemLabor.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<WorkOrderItemLabor> LaborPutAsync(WorkOrderItemLabor 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;
|
|
}
|
|
|
|
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
|
|
//dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
|
|
|
|
await LaborValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
// await LaborBizActionsAsync(AyaEvent.Modified, putObject, dbObject, 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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
|
|
await LaborHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await LaborPopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> LaborDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
var dbObject = await LaborGetAsync(id, false);
|
|
LaborValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.WorkOrderItemLabor.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
|
|
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
|
|
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
|
|
// await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await LaborHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////
|
|
//INDEXING
|
|
//
|
|
private async Task LaborSearchIndexAsync(WorkOrderItemLabor 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<Search.SearchIndexProcessObjectParameters> 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(WorkOrderItemLabor o, bool calculateTotalsOnly = false)
|
|
{
|
|
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)
|
|
{
|
|
Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.ServiceRateId);
|
|
o.ServiceRateViz = Rate.Name;
|
|
}
|
|
TaxCode Tax = null;
|
|
if (o.TaxCodeSaleId != null)
|
|
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId);
|
|
if (Tax != null)
|
|
o.TaxCodeSaleViz = 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 GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, o.WorkOrderItemId);
|
|
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)
|
|
{
|
|
//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 = o.CostViz + (o.CostViz * pct);
|
|
else if (cot == ContractOverrideType.PriceDiscount)
|
|
o.PriceViz = o.ListPriceViz - (o.ListPriceViz * pct);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Calculate totals and taxes
|
|
//NET
|
|
o.NetViz = o.PriceViz * o.ServiceRateQuantity;
|
|
|
|
//TAX
|
|
o.TaxAViz = 0;
|
|
o.TaxBViz = 0;
|
|
if (Tax != null)
|
|
{
|
|
if (Tax.TaxAPct != 0)
|
|
{
|
|
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
|
|
|
|
}
|
|
|
|
// ////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //BIZ ACTIONS
|
|
// //
|
|
// //
|
|
// private async Task LaborBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemLabor newObj, WorkOrderItemLabor 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;
|
|
|
|
// //SET TAXES AND PRICING
|
|
|
|
// //by default apply all automatic actions with further restrictions possible below
|
|
// bool ApplyTax = true;
|
|
// bool SetPrice = true;
|
|
|
|
// //if modifed, see what has changed and should be re-applied
|
|
// if (ayaEvent == AyaEvent.Modified)
|
|
// {
|
|
// //If it wasn't a service rate or quantity change there is no need to set pricing
|
|
// if (newObj.ServiceRateId == oldObj.ServiceRateId && newObj.ServiceRateQuantity == oldObj.ServiceRateQuantity)
|
|
// {
|
|
// SetPrice = false;
|
|
// }
|
|
// //If taxes haven't change then no need to update taxes
|
|
// if (newObj.TaxCodeSaleId == oldObj.TaxCodeSaleId)
|
|
// ApplyTax = false;
|
|
// }
|
|
|
|
// //Tax code
|
|
// if (ApplyTax)
|
|
// {
|
|
// //Default in case nothing to apply
|
|
// newObj.TaxAPct = 0;
|
|
// newObj.TaxBPct = 0;
|
|
// newObj.TaxOnTax = false;
|
|
// newObj.TaxName = "";
|
|
|
|
// if (newObj.TaxCodeSaleId != null)
|
|
// {
|
|
// var t = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.TaxCodeSaleId);
|
|
// if (t != null)
|
|
// {
|
|
// newObj.TaxAPct = t.TaxAPct;
|
|
// newObj.TaxBPct = t.TaxBPct;
|
|
// newObj.TaxOnTax = t.TaxOnTax;
|
|
// newObj.TaxName = t.Name;
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// //Pricing
|
|
// if (SetPrice)
|
|
// {
|
|
// var Contract = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, newObj.WorkOrderItemId);
|
|
// await LaborSetPrice(newObj, Contract);
|
|
|
|
|
|
// }
|
|
// }
|
|
|
|
// ////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// // SET PER UNIT LIST PRICE
|
|
// //
|
|
// //(called by woitemlabor save and also by header save on change of contract)
|
|
// private async Task LaborSetPrice(WorkOrderItemLabor o, Contract c)
|
|
// {
|
|
// //default in case nothing to apply
|
|
// o.Cost = 0;
|
|
// o.ListPrice = 0;
|
|
// o.Price = 0;
|
|
|
|
// //in v7 it was ok to have no service rate selected
|
|
// //not sure why but carried forward to v8 so..
|
|
// if (o.ServiceRateId == null)
|
|
// return;
|
|
|
|
// var Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.ServiceRateId);
|
|
// if (Rate == null)
|
|
// {
|
|
// AddError(ApiErrorCode.NOT_FOUND, "generalerror", "Service rate not found");//this should never happen, no point in localizing
|
|
// return;
|
|
// }
|
|
|
|
// o.Cost = Rate.Cost;
|
|
// o.ListPrice = Rate.Charge;
|
|
// o.Price = o.ListPrice;//default is list price unless a contract overrides it
|
|
|
|
// if (c == null)
|
|
// return;//No contract so bail out now, it's done
|
|
|
|
|
|
// //POTENTIAL CONTRACT ADJUSTMENTS
|
|
// //First check if there is a matching tagged service rate contract discount, that takes precedence
|
|
// if (c.ContractServiceRateOverrideItems.Count > 0)
|
|
// {
|
|
// //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)
|
|
// {
|
|
// var pct = csr.OverridePct / 100;
|
|
// //found a match, apply the discount and return
|
|
// if (csr.OverrideType == ContractOverrideType.CostMarkup)
|
|
// o.Price = o.Cost + (o.Cost * pct);
|
|
// else if (csr.OverrideType == ContractOverrideType.PriceDiscount)
|
|
// o.Price = o.ListPrice - (o.ListPrice * pct);
|
|
// return;
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// //No tag discounts, so check for a generic one
|
|
// if (c.ServiceRatesOverridePct == 0)
|
|
// return;// no generic discount for all items so bail now
|
|
|
|
// {
|
|
// var pct = c.ServiceRatesOverridePct / 100;
|
|
|
|
// //Contract has a generic override so apply it
|
|
// if (c.ServiceRatesOverrideType == ContractOverrideType.CostMarkup)
|
|
// o.Price = o.Cost + (o.Cost * pct);
|
|
// else if (c.ServiceRatesOverrideType == ContractOverrideType.PriceDiscount)
|
|
// o.Price = o.ListPrice - (o.ListPrice * pct);
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task LaborValidateAsync(WorkOrderItemLabor proposedObj, WorkOrderItemLabor currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (proposedObj.WorkOrderItemId == 0)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
|
|
}
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemLabor.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 LaborValidateCanDelete(WorkOrderItemLabor obj)
|
|
{
|
|
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.WorkOrderItemLabor))
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task LaborHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemLabor oProposed = (WorkOrderItemLabor)proposedObj;
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
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 LABOR level
|
|
|
|
|
|
/*
|
|
██╗ ██████╗ █████╗ ███╗ ██╗
|
|
██║ ██╔═══██╗██╔══██╗████╗ ██║
|
|
██║ ██║ ██║███████║██╔██╗ ██║
|
|
██║ ██║ ██║██╔══██║██║╚██╗██║
|
|
███████╗╚██████╔╝██║ ██║██║ ╚████║
|
|
*/
|
|
|
|
#region WorkOrderItemLoan level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> LoanExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItemLoan.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItemLoan> LoanCreateAsync(WorkOrderItemLoan newObject)
|
|
{
|
|
await LoanValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
await LoanBizActionsAsync(AyaEvent.Created, newObject, null, null);
|
|
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItemLoan.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
|
|
await LoanSearchIndexAsync(newObject, true);
|
|
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await LoanHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
await LoanPopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemLoan> LoanGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemLoan.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<WorkOrderItemLoan> LoanPutAsync(WorkOrderItemLoan 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;
|
|
}
|
|
|
|
// dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
|
|
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
|
|
|
|
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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
|
|
await LoanHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await LoanPopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> LoanDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
var dbObject = await LoanGetAsync(id, false);
|
|
LoanValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.WorkOrderItemLoan.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
|
|
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
|
|
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
|
|
//await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await LoanHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//////////////////////////////////////////////
|
|
//INDEXING
|
|
//
|
|
private async Task LoanSearchIndexAsync(WorkOrderItemLoan 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<Search.SearchIndexProcessObjectParameters> 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(WorkOrderItemLoan o, List<NameIdItem> loanUnitRateEnumList = null, 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();
|
|
}
|
|
|
|
LoanUnit loanUnit = await ct.LoanUnit.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.LoanUnitId);
|
|
o.LoanUnitViz = loanUnit.Name;
|
|
|
|
TaxCode Tax = null;
|
|
if (o.TaxCodeId != null)
|
|
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == 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 = o.PriceViz * o.Quantity;
|
|
|
|
//TAX
|
|
o.TaxAViz = 0;
|
|
o.TaxBViz = 0;
|
|
if (Tax != null)
|
|
{
|
|
if (Tax.TaxAPct != 0)
|
|
{
|
|
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//BIZ ACTIONS
|
|
//
|
|
//
|
|
private async Task LoanBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemLoan newObj, WorkOrderItemLoan 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;
|
|
}
|
|
|
|
//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(WorkOrderItemLoan proposedObj, WorkOrderItemLoan currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (proposedObj.WorkOrderItemId == 0)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
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");
|
|
|
|
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemLoan.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 LoanValidateCanDelete(WorkOrderItemLoan obj)
|
|
{
|
|
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.WorkOrderItemLoan))
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task LoanHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemLoan oProposed = (WorkOrderItemLoan)proposedObj;
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
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 WorkOrderItemOutsideService level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> OutsideServiceExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItemOutsideService.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItemOutsideService> OutsideServiceCreateAsync(WorkOrderItemOutsideService newObject)
|
|
{
|
|
await OutsideServiceValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
// newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItemOutsideService.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
|
|
await OutsideServiceSearchIndexAsync(newObject, true);
|
|
// await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
await OutsideServicePopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemOutsideService> OutsideServiceGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemOutsideService.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
|
|
if (logTheGetEvent && ret != null)
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemOutsideService, AyaEvent.Retrieved), ct);
|
|
return ret;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//UPDATE
|
|
//
|
|
internal async Task<WorkOrderItemOutsideService> OutsideServicePutAsync(WorkOrderItemOutsideService putObject)
|
|
{
|
|
WorkOrderItemOutsideService 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;
|
|
}
|
|
|
|
|
|
// dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
|
|
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
|
|
|
|
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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
|
|
|
|
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await OutsideServicePopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> OutsideServiceDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
var dbObject = await OutsideServiceGetAsync(id, false);
|
|
OutsideServiceValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.WorkOrderItemOutsideService.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
|
|
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
|
|
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
|
|
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//////////////////////////////////////////////
|
|
//INDEXING
|
|
//
|
|
private async Task OutsideServiceSearchIndexAsync(WorkOrderItemOutsideService 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<Search.SearchIndexProcessObjectParameters> 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(WorkOrderItemOutsideService o, bool calculateTotalsOnly = false)
|
|
{
|
|
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)
|
|
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == 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 = o.NetViz * (Tax.TaxAPct / 100);
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task OutsideServiceValidateAsync(WorkOrderItemOutsideService proposedObj, WorkOrderItemOutsideService currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (proposedObj.WorkOrderItemId == 0)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
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.WorkOrderItemOutsideService.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 OutsideServiceValidateCanDelete(WorkOrderItemOutsideService obj)
|
|
{
|
|
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.WorkOrderItemOutsideService))
|
|
{
|
|
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<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
|
|
WorkOrderItemOutsideService oProposed = (WorkOrderItemOutsideService)proposedObj;
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
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
|
|
|
|
|
|
|
|
//## CREATED
|
|
if (ayaEvent == AyaEvent.Created)
|
|
{
|
|
//OutsideServiceOverdue
|
|
if (oProposed.ETADate != null)
|
|
{
|
|
//Conditions: tags + time delayed eta value
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceOverdue).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.OutsideServiceOverdue,
|
|
UserId = sub.UserId,
|
|
AyaType = AyaType.WorkOrderItemOutsideService,
|
|
ObjectId = oProposed.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
EventDate = (DateTime)oProposed.ETADate,
|
|
Name = $"{WorkorderInfo.Serial.ToString()}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}//OutsideServiceOverdue
|
|
|
|
//OutsideServiceReceived (here because it's possible a outside service is entered new with both an eta and received date if entered after the fact)
|
|
if (oProposed.ReturnDate != null)
|
|
{
|
|
//Clear overdue ones as it's now received
|
|
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue);
|
|
|
|
//Conditions: tags
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceReceived).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.OutsideServiceReceived,
|
|
UserId = sub.UserId,
|
|
AyaType = AyaType.WorkOrderItemOutsideService,
|
|
ObjectId = oProposed.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = $"{WorkorderInfo.Serial.ToString()}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}//OutsideServiceReceived
|
|
}
|
|
|
|
//## MODIFIED
|
|
if (ayaEvent == AyaEvent.Modified)
|
|
{
|
|
WorkOrderItemOutsideService oCurrent = (WorkOrderItemOutsideService)currentObj;
|
|
|
|
//OutsideServiceOverdue
|
|
//if modified then remove any potential prior ones in case irrelevant
|
|
if (oProposed.ETADate != oCurrent.ETADate)
|
|
{
|
|
//eta changed, so first of all remove any prior ones
|
|
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue);
|
|
//now can go ahead and add back again as appropriate
|
|
if (oProposed.ETADate != null)
|
|
{
|
|
//Conditions: tags + time delayed eta value
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceOverdue).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.OutsideServiceOverdue,
|
|
UserId = sub.UserId,
|
|
AyaType = AyaType.WorkOrderItemOutsideService,
|
|
ObjectId = oProposed.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
EventDate = (DateTime)oProposed.ETADate,
|
|
Name = $"{WorkorderInfo.Serial.ToString()}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}//OutsideServiceOverdue
|
|
}
|
|
|
|
//OutsideServiceReceived
|
|
if (oProposed.ReturnDate != oCurrent.ReturnDate && oProposed.ReturnDate != null)//note that this is an instant notification type so no need to clear older ones like above which is time delayed
|
|
{
|
|
//Clear overdue ones as it's now received
|
|
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue);
|
|
|
|
//Conditions: tags
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceReceived).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.OutsideServiceReceived,
|
|
UserId = sub.UserId,
|
|
AyaType = AyaType.WorkOrderItemOutsideService,
|
|
ObjectId = oProposed.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = $"{WorkorderInfo.Serial.ToString()}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}//OutsideServiceReceived
|
|
}
|
|
|
|
}//end of process notifications
|
|
|
|
|
|
#endregion work order item OUTSIDE SERVICE level
|
|
|
|
|
|
|
|
/*
|
|
██████╗ █████╗ ██████╗ ████████╗███████╗
|
|
██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝
|
|
██████╔╝███████║██████╔╝ ██║ ███████╗
|
|
██╔═══╝ ██╔══██║██╔══██╗ ██║ ╚════██║
|
|
██║ ██║ ██║██║ ██║ ██║ ███████║
|
|
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝
|
|
*/
|
|
|
|
#region WorkOrderItemPart level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> PartExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItemPart.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItemPart> CreatePartAsync(WorkOrderItemPart newObject)
|
|
{
|
|
using (var transaction = await ct.Database.BeginTransactionAsync())
|
|
{
|
|
await PartValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
await PartBizActionsAsync(AyaEvent.Created, newObject, null, null);
|
|
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItemPart.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await PartInventoryAdjustmentAsync(AyaEvent.Created, newObject, null, transaction);
|
|
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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await PartPopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemPart> PartGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemPart.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<WorkOrderItemPart> PartPutAsync(WorkOrderItemPart putObject)
|
|
{
|
|
using (var transaction = await ct.Database.BeginTransactionAsync())
|
|
{
|
|
WorkOrderItemPart 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;
|
|
}
|
|
|
|
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
|
|
//dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
|
|
|
|
await PartValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
await PartBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
await PartInventoryAdjustmentAsync(AyaEvent.Modified, putObject, dbObject, transaction);
|
|
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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
|
|
await PartHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await transaction.CommitAsync();
|
|
await PartPopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> PartDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
// var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
using (var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync())
|
|
{
|
|
try
|
|
{
|
|
var dbObject = await PartGetAsync(id, false);
|
|
PartValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
await PartBizActionsAsync(AyaEvent.Deleted, null, dbObject, transaction);
|
|
ct.WorkOrderItemPart.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
await PartInventoryAdjustmentAsync(AyaEvent.Deleted, null, dbObject, transaction);
|
|
if (HasErrors)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
return false;
|
|
}
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
|
|
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
|
|
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
|
|
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await PartHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////
|
|
//INDEXING
|
|
//
|
|
private async Task PartSearchIndexAsync(WorkOrderItemPart 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<Search.SearchIndexProcessObjectParameters> 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(WorkOrderItemPart o, bool calculateTotalsOnly = false)
|
|
{
|
|
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;
|
|
|
|
TaxCode Tax = null;
|
|
if (o.TaxPartSaleId != null)
|
|
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxPartSaleId);
|
|
if (Tax != null)
|
|
o.TaxPartSaleViz = Tax.Name;
|
|
|
|
o.PriceViz = 0;
|
|
if (part != null)
|
|
{
|
|
// 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 GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, o.WorkOrderItemId);
|
|
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 = o.Cost + (o.Cost * pct);
|
|
else if (cot == ContractOverrideType.PriceDiscount)
|
|
o.PriceViz = o.ListPrice - (o.ListPrice * pct);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Calculate totals and taxes
|
|
//NET
|
|
o.NetViz = o.PriceViz * o.Quantity;
|
|
|
|
//TAX
|
|
o.TaxAViz = 0;
|
|
o.TaxBViz = 0;
|
|
if (Tax != null)
|
|
{
|
|
if (Tax.TaxAPct != 0)
|
|
{
|
|
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//BIZ ACTIONS
|
|
//
|
|
//
|
|
private async Task PartBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemPart newObj, WorkOrderItemPart 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;
|
|
}
|
|
}
|
|
|
|
|
|
//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;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//BIZ ACTIONS
|
|
//
|
|
//
|
|
private async Task PartInventoryAdjustmentAsync(AyaEvent ayaEvent, WorkOrderItemPart newObj, WorkOrderItemPart oldObj, IDbContextTransaction transaction)
|
|
{
|
|
|
|
|
|
if (AyaNova.Util.ServerGlobalBizSettings.UseInventory)
|
|
{
|
|
PartInventoryBiz pib = new PartInventoryBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
|
|
|
|
//DELETED, HANDLE INVENTORY / RETURN SERIALS
|
|
if (ayaEvent == AyaEvent.Deleted)
|
|
{
|
|
dtInternalPartInventory pi =
|
|
new dtInternalPartInventory
|
|
{
|
|
PartId = oldObj.PartId,
|
|
PartWarehouseId = oldObj.PartWarehouseId,
|
|
Quantity = oldObj.Quantity,
|
|
SourceType = null,//null because the po no longer exists so this is technically a manual adjustment
|
|
SourceId = null,//''
|
|
Description = await Translate("WorkOrderItemPart") + $" {oldObj.Serials} " + await Translate("EventDeleted")
|
|
};
|
|
if (await pib.CreateAsync(pi) == null)
|
|
{
|
|
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}");
|
|
return;
|
|
}
|
|
else
|
|
{ //return serial numbers to part
|
|
if (!string.IsNullOrWhiteSpace(oldObj.Serials))
|
|
await PartBiz.AppendSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId);
|
|
}
|
|
}
|
|
|
|
|
|
//CREATED, HANDLE INVENTORY / CONSUME SERIALS
|
|
if (ayaEvent == AyaEvent.Created && newObj.Quantity != 0)//allow zero quantity parts on workorder as placeholder, serials will not be consumed
|
|
{
|
|
dtInternalPartInventory pi =
|
|
new dtInternalPartInventory
|
|
{
|
|
PartId = newObj.PartId,
|
|
PartWarehouseId = newObj.PartWarehouseId,
|
|
Quantity = newObj.Quantity * -1,
|
|
SourceType = AyaType.WorkOrderItemPart,
|
|
SourceId = newObj.Id,
|
|
Description = await Translate("WorkOrderItemPart") + $" {newObj.Serials} " + await Translate("EventCreated")
|
|
};
|
|
if (await pib.CreateAsync(pi) == null)
|
|
{
|
|
if (pib.HasErrors)
|
|
{
|
|
foreach (var e in pib.Errors)
|
|
{
|
|
if (e.Code == ApiErrorCode.INSUFFICIENT_INVENTORY)
|
|
AddError(e.Code, "Quantity", e.Message);
|
|
else
|
|
AddError(e.Code, e.Target, e.Message);
|
|
}
|
|
}
|
|
//AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}");
|
|
return;
|
|
}
|
|
else
|
|
{ //Consume serial numbers from part
|
|
if (!string.IsNullOrWhiteSpace(newObj.Serials))
|
|
await PartBiz.RemoveSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId);
|
|
}
|
|
}
|
|
|
|
|
|
//UPDATED, HANDLE INVENTORY / UPDATE SERIALS AS REQUIRED
|
|
if (ayaEvent == AyaEvent.Modified)
|
|
{
|
|
//INVENTORY
|
|
if (newObj.PartId != oldObj.PartId || newObj.Quantity != oldObj.Quantity)
|
|
{
|
|
//OUT with the old
|
|
if (oldObj.Quantity != 0)//zero quantity doesn't affect inventory or serials
|
|
{
|
|
dtInternalPartInventory piOld = new dtInternalPartInventory
|
|
{
|
|
PartId = oldObj.PartId,
|
|
PartWarehouseId = oldObj.PartWarehouseId,
|
|
Quantity = oldObj.Quantity,
|
|
SourceType = null,//null because the po no longer exists so this is technically a manual adjustment
|
|
SourceId = null,//''
|
|
Description = await Translate("WorkOrderItemPart") + $" {oldObj.Serials} " + await Translate("EventDeleted")
|
|
};
|
|
if (await pib.CreateAsync(piOld) == null)
|
|
{
|
|
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({piOld.Description}):{pib.GetErrorsAsString()}");
|
|
return;
|
|
}
|
|
else
|
|
{ //return serial numbers to part
|
|
if (!string.IsNullOrWhiteSpace(oldObj.Serials))
|
|
await PartBiz.AppendSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId);
|
|
}
|
|
}
|
|
|
|
|
|
//IN with the new
|
|
if (newObj.Quantity != 0)
|
|
{//NOTE: zero quantity is considered to be a placeholder and no serials will be consumed, nor inventory affected
|
|
dtInternalPartInventory piNew = new dtInternalPartInventory
|
|
{
|
|
PartId = newObj.PartId,
|
|
PartWarehouseId = newObj.PartWarehouseId,
|
|
Quantity = newObj.Quantity * -1,
|
|
SourceType = AyaType.WorkOrderItemPart,
|
|
SourceId = newObj.Id,
|
|
Description = await Translate("WorkOrderItemPart") + $" {newObj.Serials} " + await Translate("EventCreated")
|
|
};
|
|
|
|
if (await pib.CreateAsync(piNew) == null)
|
|
{
|
|
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({piNew.Description}):{pib.GetErrorsAsString()}");
|
|
return;
|
|
}
|
|
else
|
|
{ //Consume serial numbers from part
|
|
if (!string.IsNullOrWhiteSpace(newObj.Serials))
|
|
await PartBiz.RemoveSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId);
|
|
}
|
|
}
|
|
}
|
|
//SERIALS
|
|
if (newObj.Serials != oldObj.Serials)
|
|
{
|
|
//NOTE: zero quantity is considered to be a placeholder and no serials will be consumed (hence not returned either)
|
|
|
|
//return serial numbers to part
|
|
if (oldObj.Quantity != 0 && !string.IsNullOrWhiteSpace(oldObj.Serials))
|
|
await PartBiz.AppendSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId);
|
|
|
|
//Consume serial numbers from part
|
|
if (newObj.Quantity != 0 && !string.IsNullOrWhiteSpace(newObj.Serials))
|
|
await PartBiz.RemoveSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task PartValidateAsync(WorkOrderItemPart proposedObj, WorkOrderItemPart currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (proposedObj.WorkOrderItemId == 0)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
|
|
}
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemPart.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 PartValidateCanDelete(WorkOrderItemPart obj)
|
|
{
|
|
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.WorkOrderItemPart))
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task PartHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemPart oProposed = (WorkOrderItemPart)proposedObj;
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
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 PARTS level
|
|
|
|
|
|
/*
|
|
██████╗ █████╗ ██████╗ ████████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗███████╗███████╗████████╗███████╗
|
|
██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝ ██╔══██╗██╔════╝██╔═══██╗██║ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝
|
|
██████╔╝███████║██████╔╝ ██║█████╗██████╔╝█████╗ ██║ ██║██║ ██║█████╗ ███████╗ ██║ ███████╗
|
|
██╔═══╝ ██╔══██║██╔══██╗ ██║╚════╝██╔══██╗██╔══╝ ██║▄▄ ██║██║ ██║██╔══╝ ╚════██║ ██║ ╚════██║
|
|
██║ ██║ ██║██║ ██║ ██║ ██║ ██║███████╗╚██████╔╝╚██████╔╝███████╗███████║ ██║ ███████║
|
|
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝
|
|
*/
|
|
|
|
#region WorkOrderItemPartRequest level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> PartRequestExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItemPartRequest.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItemPartRequest> PartRequestCreateAsync(WorkOrderItemPartRequest newObject)
|
|
{
|
|
await PartRequestValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItemPartRequest.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
|
|
//await PartRequestSearchIndexAsync(newObject, true);
|
|
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await PartRequestHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
await PartRequestPopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemPartRequest> PartRequestGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemPartRequest.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<WorkOrderItemPartRequest> PartRequestPutAsync(WorkOrderItemPartRequest putObject)
|
|
{
|
|
WorkOrderItemPartRequest dbObject = await PartRequestGetAsync(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 PartRequestValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await PartRequestExistsAsync(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 PartRequestSearchIndexAsync(putObject, false);
|
|
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
|
|
await PartRequestHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await PartRequestPopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> PartRequestDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
var dbObject = await PartRequestGetAsync(id, false);
|
|
PartRequestValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.WorkOrderItemPartRequest.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
|
|
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
|
|
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
|
|
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await PartRequestHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VIZ POPULATE
|
|
//
|
|
private async Task PartRequestPopulateVizFields(WorkOrderItemPartRequest o)
|
|
{
|
|
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);
|
|
o.PartViz = part.PartNumber;
|
|
o.UpcViz = part.UPC;
|
|
|
|
PurchaseOrder po = null;
|
|
if (o.PurchaseOrderItemId != null)
|
|
{
|
|
var poid = await ct.PurchaseOrderItem.AsNoTracking().Where(x => x.Id == o.PurchaseOrderItemId).Select(x => x.PurchaseOrderId).FirstOrDefaultAsync();
|
|
if (poid != 0)
|
|
po = await ct.PurchaseOrder.AsNoTracking().Where(x => x.Id == poid).FirstOrDefaultAsync();
|
|
}
|
|
if (po != null)
|
|
{
|
|
o.PurchaseOrderViz = po.Serial.ToString();
|
|
o.PurchaseOrderIdViz = po.Id;
|
|
o.PurchaseOrderDateViz = po.OrderedDate;
|
|
o.PurchaseOrderExpectedDateViz = po.ExpectedReceiveDate;
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task PartRequestValidateAsync(WorkOrderItemPartRequest proposedObj, WorkOrderItemPartRequest currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (proposedObj.WorkOrderItemId == 0)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
|
|
}
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemPartRequest.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 PartRequestValidateCanDelete(WorkOrderItemPartRequest obj)
|
|
{
|
|
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.WorkOrderItemPartRequest))
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task PartRequestHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemPartRequest oProposed = (WorkOrderItemPartRequest)proposedObj;
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
proposedObj.Tags = WorkorderInfo.Tags;
|
|
proposedObj.Name = WorkorderInfo.Serial.ToString();
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
}//end of process notifications
|
|
|
|
|
|
|
|
#endregion work order item PART REQUEST level
|
|
|
|
|
|
/*
|
|
███████╗ ██████╗██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ ███████╗██████╗ ██╗ ██╗███████╗███████╗██████╗ ███████╗
|
|
██╔════╝██╔════╝██║ ██║██╔════╝██╔══██╗██║ ██║██║ ██╔════╝██╔══██╗ ██║ ██║██╔════╝██╔════╝██╔══██╗██╔════╝
|
|
███████╗██║ ███████║█████╗ ██║ ██║██║ ██║██║ █████╗ ██║ ██║█████╗██║ ██║███████╗█████╗ ██████╔╝███████╗
|
|
╚════██║██║ ██╔══██║██╔══╝ ██║ ██║██║ ██║██║ ██╔══╝ ██║ ██║╚════╝██║ ██║╚════██║██╔══╝ ██╔══██╗╚════██║
|
|
███████║╚██████╗██║ ██║███████╗██████╔╝╚██████╔╝███████╗███████╗██████╔╝ ╚██████╔╝███████║███████╗██║ ██║███████║
|
|
╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝
|
|
*/
|
|
|
|
#region WorkOrderItemScheduledUser level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> ScheduledUserExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItemScheduledUser.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItemScheduledUser> ScheduledUserCreateAsync(WorkOrderItemScheduledUser newObject)
|
|
{
|
|
await ScheduledUserValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItemScheduledUser.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
|
|
//await ScheduledUserSearchIndexAsync(newObject, true);
|
|
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
await ScheduledUserPopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemScheduledUser> ScheduledUserGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemScheduledUser.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<WorkOrderItemScheduledUser> ScheduledUserPutAsync(WorkOrderItemScheduledUser putObject)
|
|
{
|
|
WorkOrderItemScheduledUser 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;
|
|
}
|
|
|
|
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
|
|
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
|
|
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 ScheduledUserSearchIndexAsync(dbObject, false);
|
|
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
|
|
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await ScheduledUserPopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> ScheduledUserDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
var dbObject = await ScheduledUserGetAsync(id, false);
|
|
ScheduledUserValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.WorkOrderItemScheduledUser.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
|
|
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
|
|
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
|
|
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VIZ POPULATE
|
|
//
|
|
private async Task ScheduledUserPopulateVizFields(WorkOrderItemScheduledUser o)
|
|
{
|
|
if (o.UserId != null)
|
|
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
|
|
if (o.ServiceRateId != null)
|
|
o.ServiceRateViz = await ct.ServiceRate.AsNoTracking().Where(x => x.Id == o.ServiceRateId).Select(x => x.Name).FirstOrDefaultAsync();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task ScheduledUserValidateAsync(WorkOrderItemScheduledUser proposedObj, WorkOrderItemScheduledUser currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (proposedObj.WorkOrderItemId == 0)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
|
|
// //TEST TEST TEST
|
|
// if (proposedObj.EstimatedQuantity == 69)
|
|
// {
|
|
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, $"EstimatedQuantity", "◈◈ TEST SAVE ERROR ◈◈");
|
|
// }
|
|
|
|
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
|
|
}
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemScheduledUser.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 ScheduledUserValidateCanDelete(WorkOrderItemScheduledUser obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
|
|
// //TEST TEST TEST
|
|
// if (obj.EstimatedQuantity == 69)
|
|
// {
|
|
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, $"EstimatedQuantity", "◈◈ TEST DELETE ERROR ◈◈");
|
|
// }
|
|
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemScheduledUser))
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task ScheduledUserHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemScheduledUser oProposed = (WorkOrderItemScheduledUser)proposedObj;
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
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
|
|
|
|
|
|
//## CREATED / UPDATED - ScheduledOnWorkorder event
|
|
//Note: scheduled on workorder is immediate so same process regardless if modified or updated
|
|
//because modified changes nearly all affect user so decision is just send it no matter what as any difference is enough to send
|
|
if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
|
|
{
|
|
//this block is entirely about //ScheduledOnWorkorder event
|
|
|
|
if (oProposed.UserId != null)
|
|
{
|
|
//Conditions: userid match and tags
|
|
//delivery is immediate so no need to remove old ones of this kind
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorder && z.UserId == oProposed.UserId).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.ScheduledOnWorkorder,
|
|
UserId = sub.UserId,
|
|
AyaType = AyaType.WorkOrderItemScheduledUser,
|
|
ObjectId = oProposed.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = $"{WorkorderInfo.Serial.ToString()}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}//ScheduledOnWorkorder
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
//## CREATED
|
|
if (ayaEvent == AyaEvent.Created)
|
|
{
|
|
//ScheduledOnWorkorderImminent
|
|
if (oProposed.UserId != null && oProposed.StartDate != null)
|
|
{
|
|
//Conditions: userid match and tags + time delayed age value
|
|
//delivery is delayed so need to remove old ones of this kind on update
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorderImminent && z.UserId == oProposed.UserId).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.ScheduledOnWorkorderImminent,
|
|
UserId = sub.UserId,
|
|
AyaType = AyaType.WorkOrderItemScheduledUser,
|
|
ObjectId = oProposed.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
EventDate = (DateTime)oProposed.StartDate,
|
|
Name = $"{WorkorderInfo.Serial.ToString()}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}//ScheduledOnWorkorderImminent
|
|
}
|
|
|
|
|
|
|
|
//## MODIFIED
|
|
if (ayaEvent == AyaEvent.Modified)
|
|
{
|
|
|
|
//ScheduledOnWorkorderImminent
|
|
//Always clear any old ones for this object as they are all irrelevant the moment changed:
|
|
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.ScheduledOnWorkorderImminent);
|
|
|
|
if (oProposed.UserId != null && oProposed.StartDate != null)
|
|
{
|
|
//Conditions: userid match and tags + time delayed age value
|
|
//delivery is delayed so need to remove old ones of this kind on update
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorderImminent && z.UserId == oProposed.UserId).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.ScheduledOnWorkorderImminent,
|
|
UserId = sub.UserId,
|
|
AyaType = AyaType.WorkOrderItemScheduledUser,
|
|
ObjectId = oProposed.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
EventDate = (DateTime)oProposed.StartDate,
|
|
Name = $"{WorkorderInfo.Serial.ToString()}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}//ScheduledOnWorkorderImminent
|
|
}
|
|
|
|
}//end of process notifications
|
|
|
|
|
|
|
|
#endregion work order item SCHEDULED USER level
|
|
|
|
|
|
/*
|
|
████████╗ █████╗ ███████╗██╗ ██╗
|
|
╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
|
|
██║ ███████║███████╗█████╔╝
|
|
██║ ██╔══██║╚════██║██╔═██╗
|
|
██║ ██║ ██║███████║██║ ██╗
|
|
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
|
|
*/
|
|
|
|
#region WorkOrderItemTask level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> TaskExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItemTask.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItemTask> TaskCreateAsync(WorkOrderItemTask newObject)
|
|
{
|
|
await TaskValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItemTask.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
|
|
await TaskSearchIndexAsync(newObject, true);
|
|
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await TaskHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
await TaskPopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemTask> TaskGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemTask.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<WorkOrderItemTask> TaskPutAsync(WorkOrderItemTask putObject)
|
|
{
|
|
WorkOrderItemTask 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;
|
|
}
|
|
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
|
|
//dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
|
|
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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
|
|
await TaskHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await TaskPopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> TaskDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
var dbObject = await TaskGetAsync(id, false);
|
|
TaskValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.WorkOrderItemTask.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
|
|
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
|
|
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
|
|
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await TaskHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//////////////////////////////////////////////
|
|
//INDEXING
|
|
//
|
|
private async Task TaskSearchIndexAsync(WorkOrderItemTask 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<Search.SearchIndexProcessObjectParameters> 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(WorkOrderItemTask o, List<NameIdItem> taskCompletionTypeEnumList = null)
|
|
{
|
|
if (o.CompletedByUserId != null)
|
|
o.CompletedByUserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.CompletedByUserId).Select(x => x.Name).FirstOrDefaultAsync();
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task TaskValidateAsync(WorkOrderItemTask proposedObj, WorkOrderItemTask currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (proposedObj.WorkOrderItemId == 0)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(proposedObj.Task))
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Task");
|
|
|
|
//TEST TEST TEST ERROR
|
|
if (proposedObj.Sequence == 999)
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Sequence");
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemTask.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 TaskValidateCanDelete(WorkOrderItemTask obj)
|
|
{
|
|
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.WorkOrderItemTask))
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task TaskHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemTask oProposed = (WorkOrderItemTask)proposedObj;
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
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 TASK level
|
|
|
|
|
|
/*
|
|
████████╗██████╗ █████╗ ██╗ ██╗███████╗██╗
|
|
╚══██╔══╝██╔══██╗██╔══██╗██║ ██║██╔════╝██║
|
|
██║ ██████╔╝███████║██║ ██║█████╗ ██║
|
|
██║ ██╔══██╗██╔══██║╚██╗ ██╔╝██╔══╝ ██║
|
|
██║ ██║ ██║██║ ██║ ╚████╔╝ ███████╗███████╗
|
|
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝
|
|
*/
|
|
|
|
#region WorkOrderItemTravel level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> TravelExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItemTravel.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItemTravel> TravelCreateAsync(WorkOrderItemTravel newObject)
|
|
{
|
|
await TravelValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
//await TravelBizActionsAsync(AyaEvent.Created, newObject, null, null);
|
|
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItemTravel.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
|
|
await TravelSearchIndexAsync(newObject, true);
|
|
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
|
|
await TravelHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
await TravelPopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemTravel> TravelGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemTravel.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<WorkOrderItemTravel> TravelPutAsync(WorkOrderItemTravel putObject)
|
|
{
|
|
WorkOrderItemTravel 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;
|
|
}
|
|
|
|
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
|
|
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
|
|
await TravelValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
// await TravelBizActionsAsync(AyaEvent.Modified, putObject, dbObject, 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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
|
|
await TravelHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await TravelPopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> TravelDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
var dbObject = await TravelGetAsync(id, false);
|
|
TravelValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.WorkOrderItemTravel.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);
|
|
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
|
|
// await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
|
|
//await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await TravelHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////
|
|
//INDEXING
|
|
//
|
|
private async Task TravelSearchIndexAsync(WorkOrderItemTravel 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<Search.SearchIndexProcessObjectParameters> 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(WorkOrderItemTravel o, bool calculateTotalsOnly = false)
|
|
{
|
|
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)
|
|
{
|
|
Rate = await ct.TravelRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.TravelRateId);
|
|
o.TravelRateViz = Rate.Name;
|
|
}
|
|
TaxCode Tax = null;
|
|
if (o.TaxCodeSaleId != null)
|
|
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId);
|
|
if (Tax != null)
|
|
o.TaxCodeSaleViz = 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 GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, o.WorkOrderItemId);
|
|
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)
|
|
{
|
|
//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 = o.CostViz + (o.CostViz * pct);
|
|
else if (cot == ContractOverrideType.PriceDiscount)
|
|
o.PriceViz = o.ListPriceViz - (o.ListPriceViz * pct);
|
|
}
|
|
}
|
|
}
|
|
|
|
//Calculate totals and taxes
|
|
//NET
|
|
o.NetViz = o.PriceViz * o.TravelRateQuantity;
|
|
|
|
//TAX
|
|
o.TaxAViz = 0;
|
|
o.TaxBViz = 0;
|
|
if (Tax != null)
|
|
{
|
|
if (Tax.TaxAPct != 0)
|
|
{
|
|
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
|
|
}
|
|
|
|
|
|
// ////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// //BIZ ACTIONS
|
|
// //
|
|
// //
|
|
// private async Task TravelBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemTravel newObj, WorkOrderItemTravel 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;
|
|
|
|
// //SET TAXES AND PRICING
|
|
|
|
// //by default apply all automatic actions with further restrictions possible below
|
|
// bool ApplyTax = true;
|
|
// bool ApplyPricingUpdate = true;
|
|
|
|
// //if modifed, see what has changed and should be re-applied
|
|
// if (ayaEvent == AyaEvent.Modified)
|
|
// {
|
|
// //If it wasn't a service rate change there is no need to set pricing
|
|
// if (newObj.TravelRateId == oldObj.TravelRateId)
|
|
// {
|
|
// ApplyPricingUpdate = false;
|
|
// }
|
|
// //If taxes haven't change then no need to update taxes
|
|
// if (newObj.TaxCodeSaleId == oldObj.TaxCodeSaleId)
|
|
// ApplyTax = false;
|
|
// }
|
|
|
|
// //Tax code
|
|
// if (ApplyTax)
|
|
// {
|
|
// //Default in case nothing to apply
|
|
// newObj.TaxAPct = 0;
|
|
// newObj.TaxBPct = 0;
|
|
// newObj.TaxOnTax = false;
|
|
|
|
// if (newObj.TaxCodeSaleId != null)
|
|
// {
|
|
// var t = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.TaxCodeSaleId);
|
|
// if (t != null)
|
|
// {
|
|
// newObj.TaxAPct = t.TaxAPct;
|
|
// newObj.TaxBPct = t.TaxBPct;
|
|
// newObj.TaxOnTax = t.TaxOnTax;
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// //Pricing
|
|
// if (ApplyPricingUpdate)
|
|
// {
|
|
// //default in case nothing to apply
|
|
// newObj.Cost = 0;
|
|
// newObj.ListPrice = 0;
|
|
// newObj.Price = 0;
|
|
|
|
// //in v7 it was ok to have no service rate selected
|
|
// //not sure why but carried forward to v8 so..
|
|
// if (newObj.TravelRateId != null)
|
|
// {
|
|
// var s = await ct.TravelRate.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.TravelRateId);
|
|
// if (s != null)
|
|
// {
|
|
// newObj.Cost = s.Cost;
|
|
// newObj.ListPrice = s.Charge;
|
|
// var Contract = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, newObj.WorkOrderItemId);
|
|
// TravelSetListPrice(newObj, Contract);
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// ////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// // SET PER UNIT LIST PRICE
|
|
// //
|
|
// //(called by woitemtravel save and also by header save on change of contract)
|
|
// private static void TravelSetListPrice(WorkOrderItemTravel o, Contract c)
|
|
// {
|
|
// if (c == null || c.ServiceRatesOverridePct == 0)
|
|
// {
|
|
// o.Price = o.ListPrice;//default with no contract
|
|
// return;
|
|
// }
|
|
// if (c.ServiceRatesOverrideType == ContractOverrideType.CostMarkup)
|
|
// o.Price = o.Cost + (o.Cost * c.ServiceRatesOverridePct);
|
|
// else if (c.ServiceRatesOverrideType == ContractOverrideType.PriceDiscount)
|
|
// o.Price = o.ListPrice - (o.ListPrice * c.ServiceRatesOverridePct);
|
|
// }
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task TravelValidateAsync(WorkOrderItemTravel proposedObj, WorkOrderItemTravel currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (proposedObj.WorkOrderItemId == 0)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
|
|
}
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemTravel.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 TravelValidateCanDelete(WorkOrderItemTravel obj)
|
|
{
|
|
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.WorkOrderItemTravel))
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task TravelHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemTravel oProposed = (WorkOrderItemTravel)proposedObj;
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
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 TRAVEL level
|
|
|
|
|
|
/*
|
|
██╗ ██╗███╗ ██╗██╗████████╗
|
|
██║ ██║████╗ ██║██║╚══██╔══╝
|
|
██║ ██║██╔██╗ ██║██║ ██║
|
|
██║ ██║██║╚██╗██║██║ ██║
|
|
╚██████╔╝██║ ╚████║██║ ██║
|
|
╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
|
|
*/
|
|
|
|
#region WorkOrderItemUnit level
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//EXISTS
|
|
internal async Task<bool> UnitExistsAsync(long id)
|
|
{
|
|
return await ct.WorkOrderItemUnit.AnyAsync(z => z.Id == id);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//CREATE
|
|
//
|
|
internal async Task<WorkOrderItemUnit> UnitCreateAsync(WorkOrderItemUnit newObject)
|
|
{
|
|
await UnitValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
return null;
|
|
else
|
|
{
|
|
//TODO: In biz actions set contract if this unit has a contract, note that we are only here if there is no pre-existing unit with a contract on this workorder via validation above
|
|
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
|
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
|
await ct.WorkOrderItemUnit.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 UnitHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
await UnitPopulateVizFields(newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemUnit> UnitGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemUnit.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<WorkOrderItemUnit> UnitPutAsync(WorkOrderItemUnit putObject)
|
|
{
|
|
WorkOrderItemUnit 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;
|
|
|
|
//TODO: In biz actions set contract if this unit has a contract, note that we are only here if there is no pre-existing unit with a contract on this workorder via validation above
|
|
|
|
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 UnitHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
await UnitPopulateVizFields(putObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> UnitDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
try
|
|
{
|
|
var dbObject = await UnitGetAsync(id, false);
|
|
UnitValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.WorkOrderItemUnit.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
|
|
//Log event
|
|
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
|
|
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
|
|
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
|
|
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await UnitHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
}
|
|
catch
|
|
{
|
|
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
|
|
throw;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////
|
|
//INDEXING
|
|
//
|
|
private async Task UnitSearchIndexAsync(WorkOrderItemUnit obj, bool isNew)
|
|
{
|
|
//SEARCH INDEXING
|
|
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
|
|
SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
|
|
|
|
if (isNew)
|
|
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
|
|
else
|
|
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
|
|
}
|
|
|
|
public async Task<Search.SearchIndexProcessObjectParameters> UnitGetSearchResultSummary(long id)
|
|
{
|
|
var obj = await UnitGetAsync(id, false);
|
|
var SearchParams = new Search.SearchIndexProcessObjectParameters();
|
|
if (obj != null)
|
|
SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
|
|
return SearchParams;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VIZ POPULATE
|
|
//
|
|
private async Task UnitPopulateVizFields(WorkOrderItemUnit o)
|
|
{
|
|
o.UnitViz = await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task UnitValidateAsync(WorkOrderItemUnit proposedObj, WorkOrderItemUnit currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if (ServerBootConfig.SEEDING) return;
|
|
|
|
//TODO: ADD VALIDATIONS:
|
|
// - A work order *MUST* have only one Unit with a Contract, if there is already a unit with a contract on this workorder then a new one cannot be added and it will reject with a validation error
|
|
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (proposedObj.WorkOrderItemId == 0)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
//Check state if updatable right now
|
|
if (!isNew)
|
|
{
|
|
//Front end is coded to save the state first before any other updates if it has changed and it would not be
|
|
//a part of this header update so it's safe to check it here as it will be most up to date
|
|
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
|
|
if (CurrentWoStatus.Locked)
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
|
|
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.WorkOrderItemUnit.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 UnitValidateCanDelete(WorkOrderItemUnit obj)
|
|
{
|
|
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.WorkOrderItemUnit))
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task UnitHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemUnit oProposed = (WorkOrderItemUnit)proposedObj;
|
|
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
oProposed.Name = WorkorderInfo.Serial.ToString();
|
|
|
|
//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<ICoreBizObjectModel> GetWorkOrderGraphItem(AyaType ayaType, long id)
|
|
{
|
|
switch (ayaType)
|
|
{
|
|
case AyaType.WorkOrder:
|
|
return await WorkOrderGetAsync(id, false) as ICoreBizObjectModel;
|
|
case AyaType.WorkOrderItem:
|
|
return await ItemGetAsync(id, false);
|
|
case AyaType.WorkOrderItemExpense:
|
|
return await ExpenseGetAsync(id, false);
|
|
case AyaType.WorkOrderItemLabor:
|
|
return await LaborGetAsync(id, false);
|
|
case AyaType.WorkOrderItemLoan:
|
|
return await LoanGetAsync(id, false);
|
|
case AyaType.WorkOrderItemPart:
|
|
return await PartGetAsync(id, false);
|
|
case AyaType.WorkOrderItemPartRequest:
|
|
return await PartRequestGetAsync(id, false);
|
|
case AyaType.WorkOrderItemScheduledUser:
|
|
return await ScheduledUserGetAsync(id, false);
|
|
case AyaType.WorkOrderItemTask:
|
|
return await TaskGetAsync(id, false);
|
|
case AyaType.WorkOrderItemTravel:
|
|
return await TravelGetAsync(id, false);
|
|
case AyaType.WorkOrderItemUnit:
|
|
return await UnitGetAsync(id, false);
|
|
case AyaType.WorkOrderItemOutsideService:
|
|
return await OutsideServiceGetAsync(id, false);
|
|
default:
|
|
throw new System.ArgumentOutOfRangeException($"WorkOrder::GetWorkOrderGraphItem -> Invalid ayaType{ayaType}");
|
|
}
|
|
}
|
|
|
|
public async Task<ICoreBizObjectModel> PutWorkOrderGraphItem(AyaType ayaType, ICoreBizObjectModel o)
|
|
{
|
|
ClearErrors();
|
|
switch (ayaType)
|
|
{
|
|
case AyaType.WorkOrder:
|
|
if (o is WorkOrder)
|
|
{
|
|
WorkOrder dto = new WorkOrder();
|
|
CopyObject.Copy(o, dto);
|
|
return await WorkOrderPutAsync((WorkOrder)dto);
|
|
}
|
|
return await WorkOrderPutAsync((WorkOrder)o) as ICoreBizObjectModel;
|
|
case AyaType.WorkOrderItem:
|
|
if (o is WorkOrderItem)
|
|
{
|
|
WorkOrderItem dto = new WorkOrderItem();
|
|
CopyObject.Copy(o, dto);
|
|
return await ItemPutAsync((WorkOrderItem)dto);
|
|
}
|
|
return await ItemPutAsync((WorkOrderItem)o);
|
|
case AyaType.WorkOrderItemExpense:
|
|
return await ExpensePutAsync((WorkOrderItemExpense)o);
|
|
case AyaType.WorkOrderItemLabor:
|
|
return await LaborPutAsync((WorkOrderItemLabor)o);
|
|
case AyaType.WorkOrderItemLoan:
|
|
return await LoanPutAsync((WorkOrderItemLoan)o);
|
|
case AyaType.WorkOrderItemPart:
|
|
return await PartPutAsync((WorkOrderItemPart)o);
|
|
case AyaType.WorkOrderItemPartRequest:
|
|
return await PartRequestPutAsync((WorkOrderItemPartRequest)o);
|
|
case AyaType.WorkOrderItemScheduledUser:
|
|
return await ScheduledUserPutAsync((WorkOrderItemScheduledUser)o);
|
|
case AyaType.WorkOrderItemTask:
|
|
return await TaskPutAsync((WorkOrderItemTask)o);
|
|
case AyaType.WorkOrderItemTravel:
|
|
return await TravelPutAsync((WorkOrderItemTravel)o);
|
|
case AyaType.WorkOrderItemUnit:
|
|
return await UnitPutAsync((WorkOrderItemUnit)o);
|
|
case AyaType.WorkOrderItemOutsideService:
|
|
return await OutsideServicePutAsync((WorkOrderItemOutsideService)o);
|
|
default:
|
|
throw new System.ArgumentOutOfRangeException($"WorkOrder::PutWorkOrderGraphItem -> Invalid ayaType{ayaType}");
|
|
}
|
|
}
|
|
|
|
public async Task<bool> DeleteWorkOrderGraphItem(AyaType ayaType, long id)
|
|
{
|
|
switch (ayaType)
|
|
{
|
|
case AyaType.WorkOrder:
|
|
return await WorkOrderDeleteAsync(id);
|
|
case AyaType.WorkOrderItem:
|
|
return await ItemDeleteAsync(id);
|
|
case AyaType.WorkOrderItemExpense:
|
|
return await ExpenseDeleteAsync(id);
|
|
case AyaType.WorkOrderItemLabor:
|
|
return await LaborDeleteAsync(id);
|
|
case AyaType.WorkOrderItemLoan:
|
|
return await LoanDeleteAsync(id);
|
|
case AyaType.WorkOrderItemPart:
|
|
return await PartDeleteAsync(id);
|
|
case AyaType.WorkOrderItemPartRequest:
|
|
return await PartRequestDeleteAsync(id);
|
|
case AyaType.WorkOrderItemScheduledUser:
|
|
return await ScheduledUserDeleteAsync(id);
|
|
case AyaType.WorkOrderItemTask:
|
|
return await TaskDeleteAsync(id);
|
|
case AyaType.WorkOrderItemTravel:
|
|
return await TravelDeleteAsync(id);
|
|
case AyaType.WorkOrderItemUnit:
|
|
return await UnitDeleteAsync(id);
|
|
case AyaType.WorkOrderItemOutsideService:
|
|
return await OutsideServiceDeleteAsync(id);
|
|
default:
|
|
throw new System.ArgumentOutOfRangeException($"WorkOrder::GetWorkOrderGraphItem -> 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<Contract> GetCurrentWorkOrderContractFromRelatedAsync(AyaType ayaType, long id)
|
|
{
|
|
if (mFetchedContractAlready == false)
|
|
{
|
|
long WoId = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct);
|
|
var WoContractId = await ct.WorkOrder.AsNoTracking().Where(z => z.Id == WoId).Select(z => z.ContractId).FirstOrDefaultAsync();
|
|
await GetCurrentContractFromContractIdAsync(WoContractId);
|
|
}
|
|
return mContractInEffect;
|
|
}
|
|
|
|
|
|
internal async Task<Contract> GetCurrentContractFromContractIdAsync(long? id)
|
|
{
|
|
if (id == null) return null;
|
|
if (mFetchedContractAlready == false)
|
|
{
|
|
mContractInEffect = 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);
|
|
}
|
|
return mContractInEffect;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//GET CURRENT STATUS FOR WORKORDER FROM RELATIVE
|
|
//
|
|
internal async Task<WorkOrderStatus> GetCurrentWorkOrderStatusFromRelatedAsync(AyaType ayaType, long id)
|
|
{
|
|
//instantiated method to save adding the context
|
|
return await GetCurrentWorkOrderStatusFromRelatedAsync(ayaType, id, ct);
|
|
}
|
|
internal static async Task<WorkOrderStatus> GetCurrentWorkOrderStatusFromRelatedAsync(AyaType ayaType, long id, AyContext ct)
|
|
{
|
|
//static method
|
|
long WoId = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct);
|
|
var stat = await ct.WorkOrderState.AsNoTracking()
|
|
.Where(z => z.WorkOrderId == WoId)
|
|
.OrderByDescending(z => z.Created)
|
|
.Take(1)
|
|
.FirstOrDefaultAsync();
|
|
|
|
|
|
//no state set yet?
|
|
if (stat == null)
|
|
{ //default
|
|
return new WorkOrderStatus() { Id = -1, Locked = false, Completed = false };
|
|
}
|
|
return await ct.WorkOrderStatus.AsNoTracking().Where(z => z.Id == stat.WorkOrderStatusId).FirstAsync();//this should never not be null
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion utility
|
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////
|
|
|
|
}//eoc
|
|
|
|
|
|
}//eons
|
|
|