6503 lines
322 KiB
C#
6503 lines
322 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
|
|
{
|
|
internal WorkOrderBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles, UserType currentUserType)
|
|
{
|
|
ct = dbcontext;
|
|
UserId = currentUserId;
|
|
UserTranslationId = userTranslationId;
|
|
CurrentUserRoles = UserRoles;
|
|
BizType = AyaType.WorkOrder;
|
|
CurrentUserType = currentUserType;
|
|
|
|
//Sub-role rights flags
|
|
UserHasInventoryFullRole = CurrentUserRoles.HasFlag(AuthorizationRoles.Inventory);
|
|
UserIsTechRestricted = CurrentUserRoles.HasFlag(AuthorizationRoles.TechRestricted);
|
|
UserIsSubContractorFull = CurrentUserType == UserType.ServiceContractor && CurrentUserRoles.HasFlag(AuthorizationRoles.SubContractor);
|
|
UserIsSubContractorRestricted = CurrentUserType == UserType.ServiceContractor && CurrentUserRoles.HasFlag(AuthorizationRoles.SubContractorRestricted);
|
|
UserIsRestrictedType = UserIsTechRestricted || UserIsSubContractorFull || UserIsSubContractorRestricted;
|
|
UserCanViewPartCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.InventoryRestricted)
|
|
|| CurrentUserRoles.HasFlag(AuthorizationRoles.Inventory)
|
|
|| CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin)
|
|
|| CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting);
|
|
UserCanViewLaborOrTravelRateCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.Service)
|
|
|| CurrentUserRoles.HasFlag(AuthorizationRoles.ServiceRestricted)
|
|
|| CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin)
|
|
|| CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting);
|
|
UserCanViewLoanerCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.Service)
|
|
|| CurrentUserRoles.HasFlag(AuthorizationRoles.ServiceRestricted)
|
|
|| CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin)
|
|
|| CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting);
|
|
}
|
|
|
|
internal static 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),
|
|
UserTypeFromContext.Type(httpContext.Items));
|
|
else
|
|
return new WorkOrderBiz(ct,
|
|
1,
|
|
ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID,
|
|
AuthorizationRoles.BizAdmin,
|
|
UserType.Service);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗██████╗
|
|
██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝ ██╔═══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗
|
|
██║ █╗ ██║██║ ██║██████╔╝█████╔╝█████╗██║ ██║██████╔╝██║ ██║█████╗ ██████╔╝
|
|
██║███╗██║██║ ██║██╔══██╗██╔═██╗╚════╝██║ ██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗
|
|
╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗ ╚██████╔╝██║ ██║██████╔╝███████╗██║ ██║
|
|
╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝
|
|
*/
|
|
|
|
#region WorkOrder level
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// SUBRIGHTS / RESTRICTIONS FOR WORK ORDER
|
|
//
|
|
|
|
//Note: these restrictions and rights are in addition to the basic fundamental role access rights (layer 1)
|
|
//and are considered after role rights have already been consulted first (usually at the controller level)
|
|
|
|
internal UserType CurrentUserType { get; set; }
|
|
internal bool UserIsRestrictedType { get; set; }
|
|
internal bool UserIsTechRestricted { get; set; }
|
|
internal bool UserIsSubContractorFull { get; set; }
|
|
internal bool UserIsSubContractorRestricted { get; set; }
|
|
internal bool UserCanViewPartCosts { get; set; }
|
|
internal bool UserCanViewLaborOrTravelRateCosts { get; set; }
|
|
internal bool UserCanViewLoanerCosts { get; set; }
|
|
internal bool UserHasInventoryFullRole { get; set; }
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//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);
|
|
foreach (WorkOrderItemLoan wil in wi.Loans)
|
|
await LoanBizActionsAsync(AyaEvent.Created, wil, null);
|
|
|
|
}
|
|
await ct.SaveChangesAsync();
|
|
|
|
//INVENTORY ADJUSTMENTS
|
|
if (AyaNova.Util.ServerGlobalBizSettings.Cache.UseInventory)
|
|
{
|
|
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, false);
|
|
if (newObject.GenCopyAttachmentsFrom != null && !newObject.GenCopyAttachmentsFrom.IsEmpty)
|
|
{
|
|
//copy attachment from existing object
|
|
await AttachmentBiz.DuplicateAttachments(newObject.GenCopyAttachmentsFrom, new AyaTypeId(AyaType.WorkOrder, newObject.Id), ct);
|
|
newObject.GenCopyAttachmentsFrom = null;//so it doesn't get returned
|
|
}
|
|
await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
//workorder needs to be fetched internally from several places for rule checking etc
|
|
//this just gets it raw and lets others process
|
|
private async Task<WorkOrder> WorkOrderGetFullAsync(long id)
|
|
{
|
|
//https://docs.microsoft.com/en-us/ef/core/querying/related-data
|
|
//docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections
|
|
return await ct.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);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrder> WorkOrderGetAsync(long id, bool populateDisplayFields, bool logTheGetEvent = true, bool populateForReporting = false)
|
|
{
|
|
|
|
var ret = await WorkOrderGetFullAsync(id);
|
|
|
|
if (ret != null)
|
|
{
|
|
var stat = await GetCurrentWorkOrderStatusFromRelatedAsync(BizType, ret.Id);
|
|
ret.IsLockedAtServer = stat.Locked;
|
|
|
|
var userIsTechRestricted = UserIsTechRestricted;
|
|
var userIsSubContractorFull = UserIsSubContractorFull;
|
|
var userIsSubContractorRestricted = UserIsSubContractorRestricted;
|
|
var userIsRestricted = (userIsTechRestricted || userIsSubContractorFull || userIsSubContractorRestricted);
|
|
|
|
|
|
if (userIsRestricted)
|
|
{
|
|
//Restricted users can only work with workorder items they are scheduled on
|
|
|
|
List<WorkOrderItem> removeItems = new List<WorkOrderItem>();
|
|
//gather list of items to remove by checking if they are scheduled on them or not
|
|
foreach (WorkOrderItem wi in ret.Items)
|
|
{
|
|
var userIsSelfScheduledOnThisItem = false;
|
|
foreach (WorkOrderItemScheduledUser su in wi.ScheduledUsers)
|
|
{
|
|
if (su.UserId == UserId)
|
|
{
|
|
userIsSelfScheduledOnThisItem = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!userIsSelfScheduledOnThisItem) removeItems.Add(wi);
|
|
}
|
|
foreach (var removeitem in removeItems)
|
|
{
|
|
ret.Items.Remove(removeitem);
|
|
ret.IsCompleteRecord = false;
|
|
}
|
|
|
|
//Restricted users may have further restrictions
|
|
foreach (WorkOrderItem wi in ret.Items)
|
|
{
|
|
//all restricted types
|
|
wi.ScheduledUsers.RemoveAll(x => x.UserId != UserId);
|
|
wi.Labors.RemoveAll(x => x.UserId != UserId);
|
|
wi.Travels.RemoveAll(x => x.UserId != UserId);
|
|
|
|
if (userIsTechRestricted)
|
|
{
|
|
wi.Expenses.RemoveAll(x => x.UserId != UserId);
|
|
}
|
|
|
|
if (userIsSubContractorFull)
|
|
{
|
|
wi.Expenses.RemoveAll(x => true);
|
|
wi.OutsideServices.RemoveAll(x => true);
|
|
}
|
|
|
|
if (userIsSubContractorRestricted)
|
|
{
|
|
wi.Units.RemoveAll(x => true);
|
|
wi.Parts.RemoveAll(x => true);
|
|
wi.PartRequests.RemoveAll(x => true);
|
|
wi.Expenses.RemoveAll(x => true);
|
|
wi.Loans.RemoveAll(x => true);
|
|
wi.OutsideServices.RemoveAll(x => true);
|
|
}
|
|
|
|
//tasks are allowed to be viewed and update the task completion types
|
|
}
|
|
}
|
|
|
|
if (populateDisplayFields)
|
|
await WorkOrderPopulateVizFields(ret, false, populateForReporting);
|
|
|
|
if (logTheGetEvent && ret != null)
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrder> WorkOrderGetForCustomerAsync(long id, bool populateDisplayFields, bool logTheGetEvent = true, bool populateForReporting = false)
|
|
{
|
|
throw new System.NotImplementedException();
|
|
|
|
// var ret = await WorkOrderGetFullAsync(id);
|
|
|
|
// if (ret != null)
|
|
// {
|
|
// var stat = await GetCurrentWorkOrderStatusFromRelatedAsync(BizType, ret.Id);
|
|
// ret.IsLockedAtServer = stat.Locked;
|
|
|
|
// var userIsTechRestricted = UserIsTechRestricted;
|
|
// var userIsSubContractorFull = UserIsSubContractorFull;
|
|
// var userIsSubContractorRestricted = UserIsSubContractorRestricted;
|
|
// var userIsRestricted = (userIsTechRestricted || userIsSubContractorFull || userIsSubContractorRestricted);
|
|
|
|
|
|
// if (userIsRestricted)
|
|
// {
|
|
// //Restricted users can only work with workorder items they are scheduled on
|
|
|
|
// List<WorkOrderItem> removeItems = new List<WorkOrderItem>();
|
|
// //gather list of items to remove by checking if they are scheduled on them or not
|
|
// foreach (WorkOrderItem wi in ret.Items)
|
|
// {
|
|
// var userIsSelfScheduledOnThisItem = false;
|
|
// foreach (WorkOrderItemScheduledUser su in wi.ScheduledUsers)
|
|
// {
|
|
// if (su.UserId == UserId)
|
|
// {
|
|
// userIsSelfScheduledOnThisItem = true;
|
|
// break;
|
|
// }
|
|
// }
|
|
// if (!userIsSelfScheduledOnThisItem) removeItems.Add(wi);
|
|
// }
|
|
// foreach (var removeitem in removeItems)
|
|
// {
|
|
// ret.Items.Remove(removeitem);
|
|
// ret.IsCompleteRecord = false;
|
|
// }
|
|
|
|
// //Restricted users may have further restrictions
|
|
// foreach (WorkOrderItem wi in ret.Items)
|
|
// {
|
|
// //all restricted types
|
|
// wi.ScheduledUsers.RemoveAll(x => x.UserId != UserId);
|
|
// wi.Labors.RemoveAll(x => x.UserId != UserId);
|
|
// wi.Travels.RemoveAll(x => x.UserId != UserId);
|
|
|
|
// if (userIsTechRestricted)
|
|
// {
|
|
// wi.Expenses.RemoveAll(x => x.UserId != UserId);
|
|
// }
|
|
|
|
// if (userIsSubContractorFull)
|
|
// {
|
|
// wi.Expenses.RemoveAll(x => true);
|
|
// wi.OutsideServices.RemoveAll(x => true);
|
|
// }
|
|
|
|
// if (userIsSubContractorRestricted)
|
|
// {
|
|
// wi.Units.RemoveAll(x => true);
|
|
// wi.Parts.RemoveAll(x => true);
|
|
// wi.PartRequests.RemoveAll(x => true);
|
|
// wi.Expenses.RemoveAll(x => true);
|
|
// wi.Loans.RemoveAll(x => true);
|
|
// wi.OutsideServices.RemoveAll(x => true);
|
|
// }
|
|
|
|
// //tasks are allowed to be viewed and update the task completion types
|
|
// }
|
|
// }
|
|
|
|
// if (populateDisplayFields)
|
|
// await WorkOrderPopulateVizFields(ret, false, populateForReporting);
|
|
|
|
// 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, false);
|
|
await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> WorkOrderDeleteAsync(long id)
|
|
{
|
|
using (var transaction = await ct.Database.BeginTransactionAsync())
|
|
{
|
|
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;
|
|
{
|
|
var IDList = await ct.Review.AsNoTracking().Where(x => x.AType == AyaType.WorkOrder && x.ObjectId == id).Select(x => x.Id).ToListAsync();
|
|
if (IDList.Count() > 0)
|
|
{
|
|
ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
|
|
foreach (long ItemId in IDList)
|
|
if (!await b.DeleteAsync(ItemId, transaction))
|
|
{
|
|
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
//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);
|
|
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;
|
|
|
|
|
|
//CREATED OR MODIFIED
|
|
if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
|
|
{
|
|
|
|
}
|
|
|
|
|
|
//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);
|
|
}
|
|
|
|
//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 (!string.IsNullOrWhiteSpace(newObj.PostAddress) || !string.IsNullOrWhiteSpace(newObj.Address))
|
|
return;
|
|
|
|
|
|
if (newObj.CustomerId == 0)
|
|
return;
|
|
|
|
var cust = await ct.Customer.AsNoTracking().Where(z => z.Id == newObj.CustomerId).FirstOrDefaultAsync();
|
|
if (cust == null)
|
|
return;
|
|
|
|
newObj.PostAddress = cust.PostAddress;
|
|
newObj.PostCity = cust.PostCity;
|
|
newObj.PostRegion = cust.PostRegion;
|
|
newObj.PostCountry = cust.PostCountry;
|
|
newObj.PostCode = cust.PostCode;
|
|
|
|
newObj.Address = cust.Address;
|
|
newObj.City = cust.City;
|
|
newObj.Region = cust.Region;
|
|
newObj.Country = cust.Country;
|
|
newObj.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.Cache.WorkOrderCompleteByAge != TimeSpan.Zero)
|
|
newObj.CompleteByDate = DateTime.UtcNow.Add(AyaNova.Util.ServerGlobalBizSettings.Cache.WorkOrderCompleteByAge);
|
|
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//GET WORKORDER ID FROM DESCENDANT TYPE AND ID
|
|
//
|
|
internal static async Task<ParentAndChildItemId> GetWorkOrderIdFromRelativeAsync(AyaType ayaType, long id, AyContext ct)
|
|
{
|
|
ParentAndChildItemId w = new ParentAndChildItemId();
|
|
long woitemid = 0;
|
|
switch (ayaType)
|
|
{
|
|
case AyaType.WorkOrder:
|
|
w.ParentId = id;
|
|
w.ChildItemId = 0;
|
|
return w;
|
|
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:
|
|
w.ParentId = await ct.WorkOrderState.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderId).SingleOrDefaultAsync();
|
|
w.ChildItemId = 0;
|
|
return w;
|
|
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::GetWorkOrderIdFromRelativeAsync -> AyaType {ayaType.ToString()} is not supported");
|
|
}
|
|
|
|
w.ParentId = await ct.WorkOrderItem.AsNoTracking()
|
|
.Where(z => z.Id == woitemid)
|
|
.Select(z => z.WorkOrderId)
|
|
.SingleOrDefaultAsync();
|
|
w.ChildItemId = woitemid;
|
|
return w;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//GET WORKORDER SERIAL FROM DESCENDANT TYPE AND ID
|
|
//
|
|
internal static async Task<long> GetWorkOrderSerialFromRelativeAsync(AyaType ayaType, long id, AyContext ct)
|
|
{
|
|
var wid = (await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct)).ParentId;
|
|
return await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid).Select(x => x.Serial).FirstOrDefaultAsync();
|
|
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//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, AyaType specificType)
|
|
{
|
|
switch (specificType)
|
|
{
|
|
case AyaType.WorkOrder:
|
|
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;
|
|
case AyaType.WorkOrderItem:
|
|
return await ItemGetSearchResultSummary(id);
|
|
case AyaType.WorkOrderItemExpense:
|
|
return await ExpenseGetSearchResultSummary(id);
|
|
case AyaType.WorkOrderItemLabor:
|
|
return await LaborGetSearchResultSummary(id);
|
|
case AyaType.WorkOrderItemLoan:
|
|
return await LoanGetSearchResultSummary(id);
|
|
case AyaType.WorkOrderItemPart:
|
|
return await PartGetSearchResultSummary(id);
|
|
case AyaType.WorkOrderItemTask:
|
|
return await TaskGetSearchResultSummary(id);
|
|
case AyaType.WorkOrderItemTravel:
|
|
return await TravelGetSearchResultSummary(id);
|
|
case AyaType.WorkOrderItemOutsideService:
|
|
return await OutsideServiceGetSearchResultSummary(id);
|
|
case AyaType.WorkOrderItemUnit:
|
|
return await UnitGetSearchResultSummary(id);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public void DigestSearchText(WorkOrder obj, Search.SearchIndexProcessObjectParameters searchParams)
|
|
{
|
|
if (obj != null)
|
|
searchParams.AddText(obj.Notes)
|
|
.AddText(obj.Serial)
|
|
.AddText(obj.InternalReferenceNumber)
|
|
.AddText(obj.CustomerReferenceNumber)
|
|
.AddText(obj.CustomerContactName)
|
|
.AddText(obj.InvoiceNumber)
|
|
.AddText(obj.CustomerSignatureName)
|
|
.AddText(obj.TechSignatureName)
|
|
.AddText(obj.PostAddress)
|
|
.AddText(obj.PostCity)
|
|
.AddText(obj.PostRegion)
|
|
.AddText(obj.PostCountry)
|
|
.AddText(obj.PostCode)
|
|
.AddText(obj.Address)
|
|
.AddText(obj.City)
|
|
.AddText(obj.Region)
|
|
.AddText(obj.Country)
|
|
.AddText(obj.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 || ServerBootConfig.MIGRATING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
//Check restricted role preventing create
|
|
if (isNew && UserIsRestrictedType)
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
|
|
//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.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)
|
|
{
|
|
//Check restricted role preventing create
|
|
if (UserIsRestrictedType)
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET PARTIAL WORKORDER FOR REPORTING
|
|
// (returns workorder consisting only of the path from child or grandchild up to header populated
|
|
// with display data for reporting)
|
|
//
|
|
internal async Task<WorkOrder> WorkOrderGetPartialAsync(AyaType ayaType, long id, bool includeWoItemDescendants, bool populateForReporting)
|
|
{
|
|
//if it's the entire workorder just get, populate and return as normal
|
|
if (ayaType == AyaType.WorkOrder)
|
|
return await WorkOrderGetAsync(id, true, false, populateForReporting);
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct);
|
|
//get header only
|
|
var ret = await ct.WorkOrder.AsNoTracking().Include(s => s.States).SingleOrDefaultAsync(x => x.Id == wid.ParentId);
|
|
//not found don't bomb, just return null
|
|
if (ret == null) return ret;
|
|
//explicit load subitems as required...
|
|
WorkOrderItem woitem = null;
|
|
//it's requesting a fully populated woitem so do that here
|
|
if (includeWoItemDescendants)
|
|
{
|
|
woitem = 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 == wid.ChildItemId);
|
|
}
|
|
else
|
|
{
|
|
//get the single workorder item required
|
|
woitem = await ct.WorkOrderItem.AsNoTracking().SingleOrDefaultAsync(x => x.Id == wid.ChildItemId);
|
|
switch (ayaType)
|
|
{
|
|
case AyaType.WorkOrderItemExpense:
|
|
woitem.Expenses.Add(await ct.WorkOrderItemExpense.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
|
|
break;
|
|
case AyaType.WorkOrderItemLabor:
|
|
woitem.Labors.Add(await ct.WorkOrderItemLabor.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
|
|
break;
|
|
case AyaType.WorkOrderItemLoan:
|
|
woitem.Loans.Add(await ct.WorkOrderItemLoan.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
|
|
break;
|
|
case AyaType.WorkOrderItemPart:
|
|
woitem.Parts.Add(await ct.WorkOrderItemPart.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
|
|
break;
|
|
case AyaType.WorkOrderItemPartRequest:
|
|
woitem.PartRequests.Add(await ct.WorkOrderItemPartRequest.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
|
|
break;
|
|
case AyaType.WorkOrderItemScheduledUser:
|
|
woitem.ScheduledUsers.Add(await ct.WorkOrderItemScheduledUser.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
|
|
break;
|
|
case AyaType.WorkOrderItemTask:
|
|
woitem.Tasks.Add(await ct.WorkOrderItemTask.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
|
|
break;
|
|
case AyaType.WorkOrderItemTravel:
|
|
woitem.Travels.Add(await ct.WorkOrderItemTravel.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
|
|
break;
|
|
case AyaType.WorkOrderItemOutsideService:
|
|
woitem.OutsideServices.Add(await ct.WorkOrderItemOutsideService.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
|
|
break;
|
|
case AyaType.WorkOrderItemUnit:
|
|
woitem.Units.Add(await ct.WorkOrderItemUnit.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (woitem != null)
|
|
ret.Items.Add(woitem);
|
|
|
|
await WorkOrderPopulateVizFields(ret, false, populateForReporting);
|
|
return ret;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//REPORTING
|
|
//
|
|
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest)
|
|
{
|
|
//workorder reports for entire workorder or just sub parts all go through here
|
|
//if the ayatype is a descendant of the workorder then only the portion of the workorder from that descendant directly up to the header will be populated and returned
|
|
//however if the report template has includeWoItemDescendants=true then the woitems is fully populated
|
|
|
|
var idList = dataListSelectedRequest.SelectedRowIds;
|
|
JArray ReportData = new JArray();
|
|
|
|
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 WorkOrderGetPartialAsync(dataListSelectedRequest.AType, batchId, dataListSelectedRequest.IncludeWoItemDescendants, true));
|
|
|
|
foreach (WorkOrder w in batchResults)
|
|
{
|
|
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, bool populateForReporting)
|
|
{
|
|
o.UserIsRestrictedType = UserIsRestrictedType;
|
|
o.UserIsTechRestricted = UserIsTechRestricted;
|
|
o.UserIsSubContractorFull = UserIsSubContractorFull;
|
|
o.UserIsSubContractorRestricted = UserIsSubContractorRestricted;
|
|
o.UserCanViewPartCosts = UserCanViewPartCosts;
|
|
o.UserCanViewLaborOrTravelRateCosts = UserCanViewLaborOrTravelRateCosts;
|
|
o.UserCanViewLoanerCosts = UserCanViewLoanerCosts;
|
|
|
|
if (!headerOnly)
|
|
{
|
|
foreach (var v in o.States)
|
|
await StatePopulateVizFields(v);
|
|
foreach (var v in o.Items)
|
|
await ItemPopulateVizFields(v, populateForReporting);
|
|
}
|
|
|
|
//Alert notes
|
|
//Customer notes first then others below
|
|
var custInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => new { x.AlertNotes, x.TechNotes, x.Name, x.Phone1, x.Phone2, x.Phone3, x.Phone4, x.Phone5, x.EmailAddress }).FirstOrDefaultAsync();
|
|
if (!string.IsNullOrWhiteSpace(custInfo.AlertNotes))
|
|
{
|
|
o.AlertViz = $"{await Translate("Customer")} - {await Translate("AlertNotes")}\n{custInfo.AlertNotes}\n\n";
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(custInfo.TechNotes))
|
|
{
|
|
o.CustomerTechNotesViz = $"{await Translate("CustomerTechNotes")}\n{custInfo.TechNotes}\n\n";
|
|
}
|
|
|
|
o.CustomerViz = custInfo.Name;
|
|
o.CustomerPhone1Viz = custInfo.Phone1;
|
|
o.CustomerPhone2Viz = custInfo.Phone2;
|
|
o.CustomerPhone3Viz = custInfo.Phone3;
|
|
o.CustomerPhone4Viz = custInfo.Phone4;
|
|
o.CustomerPhone5Viz = custInfo.Phone5;
|
|
o.CustomerEmailAddressViz = custInfo.EmailAddress;
|
|
|
|
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 = "-";
|
|
if (o.FromQuoteId != null)
|
|
o.FromQuoteViz = await ct.Quote.AsNoTracking().Where(x => x.Id == o.FromQuoteId).Select(x => x.Serial.ToString()).FirstOrDefaultAsync();
|
|
if (o.FromPMId != null)
|
|
o.FromPMViz = await ct.PM.AsNoTracking().Where(x => x.Id == o.FromPMId).Select(x => x.Serial.ToString()).FirstOrDefaultAsync();
|
|
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// IMPORT EXPORT
|
|
//
|
|
|
|
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest)
|
|
{
|
|
//for now just re-use the report data code
|
|
//this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
|
|
return await GetReportData(dataListSelectedRequest);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//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.WorkOrder.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]");
|
|
bool isNew = currentObj == null;
|
|
WorkOrder oProposed = (WorkOrder)proposedObj;
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);//Note: will properly handle all delete events and event removal if deleted
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
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 "Service Reminder"
|
|
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;
|
|
|
|
//Customer User?
|
|
var UserInfo = await ct.User.AsNoTracking().Where(x => x.Id == sub.UserId).Select(x => new { x.CustomerId, x.UserType, x.HeadOfficeId }).FirstOrDefaultAsync();
|
|
if (UserInfo.UserType == UserType.Customer || UserInfo.UserType == UserType.HeadOffice)
|
|
{
|
|
//CUSTOMER USER
|
|
|
|
//Quick short circuit: if workorder doesn't have a customer id then it's not going to match no matter what
|
|
if (oProposed.CustomerId == 0) continue;
|
|
|
|
var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId);
|
|
|
|
//Are they allowed right now to use this type of notification?
|
|
if (!customerUserRights.NotifyWOCreated) continue;
|
|
|
|
//is this their related work order?
|
|
if (UserInfo.CustomerId != oProposed.CustomerId)
|
|
{
|
|
//not the same customer but might be a head office user and this is one of their customers so check for that
|
|
if (UserInfo.HeadOfficeId == null) continue;//can't match any head office so no need to go further
|
|
|
|
//see if workorder customer's head office is the same id as the user's headofficeid (note that a customer user with the same head office as a *different* customer workorder doesn't qualify)
|
|
var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == oProposed.CustomerId).Select(x => new { x.HeadOfficeId, x.BillHeadOffice }).FirstOrDefaultAsync();
|
|
if (!CustomerInfo.BillHeadOffice) continue;//can't possibly match so no need to go further
|
|
if (UserInfo.HeadOfficeId != CustomerInfo.HeadOfficeId) continue;
|
|
}
|
|
}
|
|
else
|
|
continue;//only customers can subscribe to this particular notification
|
|
|
|
//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 "Service Reminder"
|
|
|
|
if (ayaEvent == AyaEvent.Modified)
|
|
{// CustomerServiceImminent "Service Reminder"
|
|
|
|
//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;
|
|
|
|
//Customer User?
|
|
var UserInfo = await ct.User.AsNoTracking().Where(x => x.Id == sub.UserId).Select(x => new { x.CustomerId, x.UserType, x.HeadOfficeId }).FirstOrDefaultAsync();
|
|
if (UserInfo.UserType == UserType.Customer || UserInfo.UserType == UserType.HeadOffice)
|
|
{
|
|
//CUSTOMER USER
|
|
|
|
//Quick short circuit: if workorder doesn't have a customer id then it's not going to match no matter what
|
|
if (oProposed.CustomerId == 0) continue;
|
|
|
|
var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId);
|
|
|
|
//Are they allowed right now to use this type of notification?
|
|
if (!customerUserRights.NotifyWOCreated) continue;
|
|
|
|
//is this their related work order?
|
|
if (UserInfo.CustomerId != oProposed.CustomerId)
|
|
{
|
|
//not the same customer but might be a head office user and this is one of their customers so check for that
|
|
if (UserInfo.HeadOfficeId == null) continue;//can't match any head office so no need to go further
|
|
|
|
//see if workorder customer's head office is the same id as the user's headofficeid (note that a customer user with the same head office as a *different* customer workorder doesn't qualify)
|
|
var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == oProposed.CustomerId).Select(x => new { x.HeadOfficeId, x.BillHeadOffice }).FirstOrDefaultAsync();
|
|
if (!CustomerInfo.BillHeadOffice) continue;//can't possibly match so no need to go further
|
|
if (UserInfo.HeadOfficeId != CustomerInfo.HeadOfficeId) continue;
|
|
}
|
|
}
|
|
else
|
|
continue;//only customers can subscribe to this particular notification
|
|
|
|
//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 "Service Reminder"
|
|
|
|
//# WorkorderCreatedForCustomer - Customer / headoffice notification
|
|
if (oProposed.CustomerId != 0 && (ayaEvent == AyaEvent.Created
|
|
|| (ayaEvent == AyaEvent.Modified && oCurrent.CustomerId != oProposed.CustomerId)))
|
|
{
|
|
|
|
//look for potential subscribers
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCreatedForCustomer).ToListAsync();
|
|
foreach (var sub in subs)
|
|
{
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
|
|
|
//Customer User?
|
|
var UserInfo = await ct.User.AsNoTracking().Where(x => x.Id == sub.UserId).Select(x => new { x.CustomerId, x.UserType, x.HeadOfficeId }).FirstOrDefaultAsync();
|
|
if (UserInfo.UserType == UserType.Customer || UserInfo.UserType == UserType.HeadOffice)
|
|
{
|
|
//CUSTOMER USER
|
|
|
|
//Quick short circuit: if workorder doesn't have a customer id then it's not going to match no matter what
|
|
if (oProposed.CustomerId == 0) continue;
|
|
|
|
var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId);
|
|
|
|
//Are they allowed right now to use this type of notification?
|
|
if (!customerUserRights.NotifyWOCreated) continue;
|
|
|
|
//is this their related work order?
|
|
if (UserInfo.CustomerId != oProposed.CustomerId)
|
|
{
|
|
//not the same customer but might be a head office user and this is one of their customers so check for that
|
|
if (UserInfo.HeadOfficeId == null) continue;//can't match any head office so no need to go further
|
|
|
|
//see if workorder customer's head office is the same id as the user's headofficeid (note that a customer user with the same head office as a *different* customer workorder doesn't qualify)
|
|
var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == oProposed.CustomerId).Select(x => new { x.HeadOfficeId, x.BillHeadOffice }).FirstOrDefaultAsync();
|
|
if (!CustomerInfo.BillHeadOffice) continue;//can't possibly match so no need to go further
|
|
if (UserInfo.HeadOfficeId != CustomerInfo.HeadOfficeId) continue;
|
|
}
|
|
}
|
|
else
|
|
continue;//only customers can subscribe to this particular notification
|
|
|
|
|
|
//Ok, we're here so it must be ok to notify user
|
|
NotifyEvent n = new NotifyEvent()
|
|
{
|
|
EventType = NotifyEventType.WorkorderCreatedForCustomer,
|
|
UserId = sub.UserId,
|
|
AyaType = AyaType.WorkOrder,
|
|
ObjectId = oProposed.Id,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = $"{oProposed.Serial.ToString()}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}//WorkorderCreatedForCustomer
|
|
|
|
#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);
|
|
var wo = await ct.WorkOrder.FirstOrDefaultAsync(x => x.Id == newObject.WorkOrderId);
|
|
var newStatusInfo = await ct.WorkOrderStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == newObject.WorkOrderStatusId);
|
|
|
|
//Set duration to completed in workorder header
|
|
//Clear it if set and not completed state
|
|
//or
|
|
//Set it if not set and completed state
|
|
if (newStatusInfo.Completed && wo.DurationToCompleted == TimeSpan.Zero)
|
|
wo.DurationToCompleted = DateTime.UtcNow - wo.CreatedDate;
|
|
else if (wo.DurationToCompleted != TimeSpan.Zero && !newStatusInfo.Completed)
|
|
wo.DurationToCompleted = TimeSpan.Zero;
|
|
wo.LastStatusId = newObject.WorkOrderStatusId;
|
|
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)
|
|
{
|
|
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();
|
|
var StateInfo = await ct.WorkOrderStatus.AsNoTracking().Where(x => x.Id == o.WorkOrderStatusId).FirstOrDefaultAsync();
|
|
o.NameViz = StateInfo.Name;
|
|
o.ColorViz = StateInfo.Color;
|
|
o.CompletedViz = StateInfo.Completed;
|
|
o.LockedViz = StateInfo.Locked;
|
|
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//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)
|
|
{
|
|
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);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task StateValidateAsync(WorkOrderState proposedObj, WorkOrderState currentObj)
|
|
{
|
|
|
|
//of all restricted users, only a restricted tech can change status
|
|
if (UserIsSubContractorFull || UserIsSubContractorRestricted)
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
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");
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// NOTIFICATION PROCESSING
|
|
//
|
|
public async Task StateHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
|
|
{
|
|
|
|
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
|
|
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) 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 { x.Serial, x.Tags, x.CustomerId }).FirstOrDefaultAsync();
|
|
WorkOrderStatus wos = await ct.WorkOrderStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == oProposed.WorkOrderStatusId);
|
|
//for notification purposes because has no name / tags field itself
|
|
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
|
|
|
|
|
|
//## 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
|
|
|
|
|
|
//# WorkorderCompleted - Customer AND User but customer only notifies if it's their workorder
|
|
{
|
|
if (wos.Completed)
|
|
{
|
|
//look for potential subscribers
|
|
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompleted).ToListAsync();
|
|
foreach (var sub in subs)
|
|
{
|
|
//not for inactive users
|
|
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
|
|
|
|
//Customer User?
|
|
var UserInfo = await ct.User.AsNoTracking().Where(x => x.Id == sub.UserId).Select(x => new { x.CustomerId, x.UserType, x.HeadOfficeId }).FirstOrDefaultAsync();
|
|
if (UserInfo.UserType == UserType.Customer || UserInfo.UserType == UserType.HeadOffice)
|
|
{
|
|
//CUSTOMER USER
|
|
|
|
//Quick short circuit: if workorder doesn't have a customer id then it's not going to match no matter what
|
|
if (WorkorderInfo.CustomerId == 0) continue;
|
|
|
|
var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId);
|
|
|
|
//Are they allowed right now to use this type of notification?
|
|
if (!customerUserRights.NotifyWOCompleted) continue;
|
|
|
|
//is this their related work order?
|
|
if (UserInfo.CustomerId != WorkorderInfo.CustomerId)
|
|
{
|
|
//not the same customer but might be a head office user and this is one of their customers so check for that
|
|
if (UserInfo.HeadOfficeId == null) continue;//can't match any head office so no need to go further
|
|
|
|
//see if workorder customer's head office is the same id as the user's headofficeid (note that a customer user with the same head office as a *different* customer workorder doesn't qualify)
|
|
var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == WorkorderInfo.CustomerId).Select(x => new { x.HeadOfficeId, x.BillHeadOffice }).FirstOrDefaultAsync();
|
|
if (!CustomerInfo.BillHeadOffice) continue;//can't possibly match so no need to go further
|
|
if (UserInfo.HeadOfficeId != CustomerInfo.HeadOfficeId) continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//INSIDE USER
|
|
//Tag match? (will be true if no sub tags so always safe to call this)
|
|
//check early to avoid cost of fetching and calculating total if unnecessary
|
|
if (!NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags)) continue;
|
|
}
|
|
|
|
//Ok, we're here so it must be ok to notify user
|
|
NotifyEvent n = new NotifyEvent()
|
|
{
|
|
EventType = NotifyEventType.WorkorderCompleted,
|
|
UserId = sub.UserId,
|
|
AyaType = AyaType.WorkOrder,
|
|
ObjectId = oProposed.WorkOrderId,
|
|
NotifySubscriptionId = sub.Id,
|
|
Name = $"{WorkorderInfo.Serial.ToString()}"
|
|
};
|
|
await ct.NotifyEvent.AddAsync(n);
|
|
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
}//WorkorderCompleted
|
|
}
|
|
|
|
}//end of process notifications
|
|
|
|
#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);
|
|
|
|
//Is this a new CSR fulfillment?
|
|
if (newObject.FromCSRId != null)
|
|
{
|
|
//flag the CSR
|
|
CustomerServiceRequestBiz biz = CustomerServiceRequestBiz.GetBiz(ct);
|
|
var csr = await biz.GetAsync((long)newObject.FromCSRId, false);
|
|
csr.WorkOrderItemId = newObject.Id;
|
|
csr.Status = CustomerServiceRequestStatus.Accepted;
|
|
await biz.PutAsync(csr);
|
|
}
|
|
|
|
await ItemPopulateVizFields(newObject, false);
|
|
await ItemHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
|
|
|
|
|
|
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItem> ItemGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
|
|
//Restricted users can not fetch a woitem directly
|
|
//arbitrary decision so don't have to put in all the cleanup code
|
|
//because from our own UI they wouldn't fetch this anyway and
|
|
//so this is only to cover api use by 3rd parties
|
|
if (UserIsRestrictedType)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
//Note: there could be rules checking here in future, i.e. can only get own 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 ItemPopulateVizFields(putObject, false);
|
|
await ItemHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> ItemDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
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);
|
|
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.TechNotes).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.TechNotes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
|
|
return SearchParams;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VIZ POPULATE
|
|
//
|
|
private async Task ItemPopulateVizFields(WorkOrderItem o, bool populateForReporting)
|
|
{
|
|
if (o.FromCSRId != null)
|
|
o.FromCSRViz = await ct.CustomerServiceRequest.AsNoTracking().Where(x => x.Id == o.FromCSRId).Select(x => x.Name).FirstOrDefaultAsync();
|
|
|
|
if (o.WorkOrderItemStatusId != null)
|
|
{
|
|
var StatusInfo = await ct.WorkOrderItemStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.WorkOrderItemStatusId);
|
|
o.WorkOrderItemStatusNameViz = StatusInfo.Name;
|
|
o.WorkOrderItemStatusColorViz = StatusInfo.Color;
|
|
}
|
|
|
|
if (o.WorkOrderItemPriorityId != null)
|
|
{
|
|
var PriorityInfo = await ct.WorkOrderItemPriority.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.WorkOrderItemPriorityId);
|
|
o.WorkOrderItemPriorityNameViz = PriorityInfo.Name;
|
|
o.WorkOrderItemPriorityColorViz = PriorityInfo.Color;
|
|
}
|
|
|
|
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, populateForReporting);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//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");
|
|
|
|
//Check restricted role preventing create
|
|
if (isNew && UserIsRestrictedType)
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
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.Notes))
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Notes");
|
|
|
|
//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;
|
|
}
|
|
|
|
//Check restricted role preventing create
|
|
if (UserIsRestrictedType)
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;//this is a completely disqualifying error
|
|
}
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
|
|
WorkOrderItem oProposed = (WorkOrderItem)proposedObj;
|
|
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
//for notification purposes because has no name field itself
|
|
if (WorkorderInfo != null)
|
|
oProposed.Name = WorkorderInfo.Serial.ToString();
|
|
else
|
|
oProposed.Name = "??";
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
|
|
}//end of process notifications
|
|
|
|
|
|
|
|
#endregion work order item level
|
|
|
|
|
|
/*
|
|
███████╗██╗ ██╗██████╗ ███████╗███╗ ██╗███████╗███████╗███████╗
|
|
██╔════╝╚██╗██╔╝██╔══██╗██╔════╝████╗ ██║██╔════╝██╔════╝██╔════╝
|
|
█████╗ ╚███╔╝ ██████╔╝█████╗ ██╔██╗ ██║███████╗█████╗ ███████╗
|
|
██╔══╝ ██╔██╗ ██╔═══╝ ██╔══╝ ██║╚██╗██║╚════██║██╔══╝ ╚════██║
|
|
███████╗██╔╝ ██╗██║ ███████╗██║ ╚████║███████║███████╗███████║
|
|
╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚══════╝╚══════╝
|
|
*/
|
|
|
|
#region 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 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 ExpensePopulateVizFields(newObject);
|
|
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemExpense> ExpenseGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
if (UserIsSubContractorFull || UserIsSubContractorRestricted) //no access allowed at all
|
|
return null;
|
|
var ret = await ct.WorkOrderItemExpense.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
|
|
if (UserIsTechRestricted && ret.UserId != UserId)//tech restricted can only see their own expenses
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return null;
|
|
}
|
|
if (logTheGetEvent && ret != null)
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
|
|
return ret;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//UPDATE
|
|
//
|
|
internal async Task<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;
|
|
}
|
|
|
|
await ExpenseValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await ExpenseExistsAsync(putObject.Id))
|
|
AddError(ApiErrorCode.NOT_FOUND);
|
|
else
|
|
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
|
|
return null;
|
|
}
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
|
|
await ExpenseSearchIndexAsync(putObject, false);
|
|
await ExpensePopulateVizFields(putObject);
|
|
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> ExpenseDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
var dbObject = await ExpenseGetAsync(id, false);
|
|
ExpenseValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.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);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
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.TaxCodeViz = Tax.Name;
|
|
|
|
//Calculate totals and taxes
|
|
if (o.ChargeToCustomer)
|
|
{
|
|
o.TaxAViz = 0;
|
|
o.TaxBViz = 0;
|
|
|
|
if (Tax != null)
|
|
{
|
|
if (Tax.TaxAPct != 0)
|
|
{
|
|
o.TaxAViz = MoneyUtil.Round(o.ChargeAmount * (Tax.TaxAPct / 100));
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round((o.ChargeAmount + o.TaxAViz) * (Tax.TaxBPct / 100));
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round(o.ChargeAmount * (Tax.TaxBPct / 100));
|
|
}
|
|
}
|
|
o.LineTotalViz = o.ChargeAmount + o.TaxAViz + o.TaxBViz;
|
|
}
|
|
else
|
|
{
|
|
o.LineTotalViz = o.ChargeAmount + o.TaxPaid;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task ExpenseValidateAsync(WorkOrderItemExpense proposedObj, WorkOrderItemExpense currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
if (UserIsSubContractorFull || UserIsSubContractorRestricted)
|
|
{
|
|
//no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
if (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId)))
|
|
{
|
|
//no edits allowed on other people's records
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
if (proposedObj.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 (!isNew && UserIsTechRestricted)
|
|
{
|
|
//Existing record so just make sure they haven't changed the not changeable fields from the db version
|
|
|
|
//Expenses: add (no user selection defaults to themselves), view, partial fields available
|
|
// to edit or delete only where they are the selected user and only edit fields
|
|
//Summary, Cost, Tax paid, Description
|
|
//note that UI will prevent this, this rule is only backup for 3rd party api users
|
|
|
|
if (currentObj.ChargeAmount != proposedObj.ChargeAmount) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeAmount");
|
|
//if (currentObj.TaxPaid != proposedObj.TaxPaid) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "TaxPaid");
|
|
if (currentObj.ChargeTaxCodeId != proposedObj.ChargeTaxCodeId) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeTaxCodeId");
|
|
if (currentObj.ReimburseUser != proposedObj.ReimburseUser) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ReimburseUser");
|
|
if (currentObj.ChargeToCustomer != proposedObj.ChargeToCustomer) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeToCustomer");
|
|
}
|
|
|
|
if (isNew && UserIsTechRestricted)
|
|
{
|
|
//NEW record, they are not allowed to set several fields so make sure they are still at their defaults
|
|
/* from client new expense record:
|
|
concurrency: 0,
|
|
description: null,
|
|
name: null,
|
|
totalCost: 0,
|
|
chargeAmount: 0,
|
|
taxPaid: 0,
|
|
chargeTaxCodeId: null,
|
|
taxCodeViz: null,
|
|
reimburseUser: false,
|
|
userId: null,
|
|
userViz: null,
|
|
chargeToCustomer: false,
|
|
isDirty: true,
|
|
workOrderItemId: this.value.items[this.activeWoItemIndex].id,
|
|
uid: Date.now() //used for
|
|
*/
|
|
if (proposedObj.ChargeAmount != 0) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeAmount");
|
|
// if (proposedObj.TaxPaid != 0) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "TaxPaid");
|
|
if (proposedObj.ChargeTaxCodeId != null) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeTaxCodeId");
|
|
if (proposedObj.ReimburseUser != false) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ReimburseUser");
|
|
if (proposedObj.ChargeToCustomer != false) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeToCustomer");
|
|
|
|
}
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.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 (UserIsSubContractorFull || UserIsSubContractorRestricted)
|
|
{
|
|
//no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
|
|
if (UserIsTechRestricted && obj.UserId != UserId)
|
|
{
|
|
//no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemExpense oProposed = (WorkOrderItemExpense)proposedObj;
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
oProposed.Tags = WorkorderInfo.Tags;
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
|
|
|
|
}//end of process notifications
|
|
#endregion work order item EXPENSE level
|
|
|
|
|
|
/*
|
|
██╗ █████╗ ██████╗ ██████╗ ██████╗
|
|
██║ ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗
|
|
██║ ███████║██████╔╝██║ ██║██████╔╝
|
|
██║ ██╔══██║██╔══██╗██║ ██║██╔══██╗
|
|
███████╗██║ ██║██████╔╝╚██████╔╝██║ ██║
|
|
╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝
|
|
*/
|
|
|
|
#region 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 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 LaborPopulateVizFields(newObject);
|
|
await LaborHandlePotentialNotificationEvent(AyaEvent.Created, 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 (UserIsRestrictedType && ret.UserId != UserId)
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return null;
|
|
}
|
|
if (logTheGetEvent && ret != null)
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
|
|
return ret;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//UPDATE
|
|
//
|
|
internal async Task<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;
|
|
}
|
|
await LaborValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await LaborExistsAsync(putObject.Id))
|
|
AddError(ApiErrorCode.NOT_FOUND);
|
|
else
|
|
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
|
|
return null;
|
|
}
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
|
|
await LaborSearchIndexAsync(putObject, false);
|
|
await LaborPopulateVizFields(putObject);
|
|
await LaborHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> LaborDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
var dbObject = await LaborGetAsync(id, false);
|
|
LaborValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.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);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await LaborHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
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.TaxCodeViz = Tax.Name;
|
|
|
|
o.PriceViz = 0;
|
|
if (Rate != null)
|
|
{
|
|
o.CostViz = Rate.Cost;
|
|
o.ListPriceViz = Rate.Charge;
|
|
o.UnitOfMeasureViz = Rate.Unit;
|
|
o.PriceViz = Rate.Charge;//default price used if not manual or contract override
|
|
}
|
|
|
|
//manual price overrides anything
|
|
if (o.PriceOverride != null)
|
|
o.PriceViz = (decimal)o.PriceOverride;
|
|
else
|
|
{
|
|
//not manual so could potentially have a contract adjustment
|
|
var c = await 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 = MoneyUtil.Round(o.CostViz + (o.CostViz * pct));
|
|
else if (cot == ContractOverrideType.PriceDiscount)
|
|
o.PriceViz = MoneyUtil.Round(o.ListPriceViz - (o.ListPriceViz * pct));
|
|
}
|
|
}
|
|
}
|
|
|
|
//Calculate totals and taxes
|
|
//NET
|
|
o.NetViz = MoneyUtil.Round(o.PriceViz * o.ServiceRateQuantity);
|
|
|
|
|
|
//TAX
|
|
o.TaxAViz = 0;
|
|
o.TaxBViz = 0;
|
|
if (Tax != null)
|
|
{
|
|
if (Tax.TaxAPct != 0)
|
|
{
|
|
o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100));
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100));
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100));
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
|
|
//RESTRICTIONS ON COST VISIBILITY?
|
|
if (!UserCanViewLaborOrTravelRateCosts)
|
|
{
|
|
o.CostViz = 0;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task LaborValidateAsync(WorkOrderItemLabor proposedObj, WorkOrderItemLabor currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) 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 (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId)))
|
|
{
|
|
//no edits allowed on other people's records
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
|
|
//Start date AND end date must both be null or both contain values
|
|
if (proposedObj.ServiceStartDate == null && proposedObj.ServiceStopDate != null)
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "ServiceStartDate");
|
|
|
|
if (proposedObj.ServiceStartDate != null && proposedObj.ServiceStopDate == null)
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "ServiceStopDate");
|
|
|
|
//Start date before end date
|
|
if (proposedObj.ServiceStartDate != null && proposedObj.ServiceStopDate != null)
|
|
if (proposedObj.ServiceStartDate > proposedObj.ServiceStopDate)
|
|
AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "ServiceStartDate");
|
|
|
|
if (proposedObj.ServiceRateQuantity < 0)//negative quantities are not allowed
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ServiceRateQuantity");
|
|
|
|
if (proposedObj.NoChargeQuantity < 0)//negative quantities are not allowed
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "NoChargeQuantity");
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.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
|
|
}
|
|
}
|
|
|
|
private void LaborValidateCanDelete(WorkOrderItemLabor obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//Labors: add (no user selection defaults to themselves), remove, view and edit only when they are the selected User
|
|
if (obj.UserId != UserId)
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemLabor oProposed = (WorkOrderItemLabor)proposedObj;
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
//for notification purposes because has no name or tags field itself
|
|
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);
|
|
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 LoanPopulateVizFields(newObject);
|
|
await LoanHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
if (newObject.ReturnDate == null)
|
|
{
|
|
var u = await ct.LoanUnit.FirstOrDefaultAsync(x => x.Id == newObject.LoanUnitId);
|
|
if (u != null)
|
|
{
|
|
u.WorkOrderItemLoanId = newObject.Id;
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemLoan> LoanGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
if (UserIsSubContractorRestricted) //no access allowed at all
|
|
return null;
|
|
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;
|
|
}
|
|
await LoanValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
await LoanBizActionsAsync(AyaEvent.Modified, putObject, dbObject);
|
|
bool changeOfUnit = dbObject.LoanUnitId != putObject.LoanUnitId;
|
|
bool changeOfReturned = dbObject.ReturnDate != putObject.ReturnDate;
|
|
ct.Replace(dbObject, putObject);
|
|
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await LoanExistsAsync(putObject.Id))
|
|
AddError(ApiErrorCode.NOT_FOUND);
|
|
else
|
|
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
|
|
return null;
|
|
}
|
|
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
|
|
await LoanSearchIndexAsync(putObject, false);
|
|
await LoanPopulateVizFields(putObject);
|
|
await LoanHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
//update loan unitif (newObject.ReturnDate == null)
|
|
|
|
//Returned or unit changed
|
|
if (changeOfUnit || changeOfReturned)
|
|
{
|
|
var u = await ct.LoanUnit.FirstOrDefaultAsync(x => x.Id == dbObject.LoanUnitId);
|
|
if (u != null && (u.WorkOrderItemLoanId == null || u.WorkOrderItemLoanId == putObject.Id))
|
|
{
|
|
if (putObject.ReturnDate != null)
|
|
{
|
|
u.WorkOrderItemLoanId = null;
|
|
}
|
|
else
|
|
{
|
|
u.WorkOrderItemLoanId = putObject.Id;
|
|
}
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
}
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> LoanDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
var dbObject = await LoanGetAsync(id, false);
|
|
LoanValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
//note:deliberately doing collection here in case of weird issues
|
|
var ul = await ct.LoanUnit.Where(x => x.WorkOrderItemLoanId == dbObject.Id).ToListAsync();
|
|
foreach (var u in ul)
|
|
{
|
|
if (u.WorkOrderItemLoanId == dbObject.Id)//only do this if it's flagged on *this* workorderitemloan
|
|
u.WorkOrderItemLoanId = null;
|
|
}
|
|
await ct.SaveChangesAsync();
|
|
|
|
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);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await LoanHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
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 = MoneyUtil.Round(o.PriceViz * o.Quantity);
|
|
|
|
//TAX
|
|
o.TaxAViz = 0;
|
|
o.TaxBViz = 0;
|
|
if (Tax != null)
|
|
{
|
|
if (Tax.TaxAPct != 0)
|
|
{
|
|
o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100));
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100));
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100));
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
|
|
|
|
//RESTRICTED COST FIELD??
|
|
if (!UserCanViewLoanerCosts)
|
|
o.Cost = 0;//cost already used in calcs and will not be updated on any update operation so this ensures the cost isn't sent over the wire
|
|
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//BIZ ACTIONS
|
|
//
|
|
//
|
|
private async Task LoanBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemLoan newObj, WorkOrderItemLoan oldObj)
|
|
{
|
|
//automatic actions on record change, called AFTER validation
|
|
|
|
//currently no processing required except for created or modified at this time
|
|
if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
|
|
return;
|
|
|
|
//SNAPSHOT PRICING
|
|
bool SnapshotPricing = true;
|
|
|
|
|
|
|
|
//if modifed, see what has changed and should be re-applied
|
|
if (ayaEvent == AyaEvent.Modified)
|
|
{
|
|
//If it wasn't a complete part change there is no need to set pricing
|
|
if (newObj.LoanUnitId == oldObj.LoanUnitId && newObj.Rate == oldObj.Rate)
|
|
{
|
|
SnapshotPricing = false;
|
|
//maintain old cost as it can come from the client as zero when it shouldn't be or someone using the api and setting it directly
|
|
//but we will only allow the price *we* set at the server initially
|
|
newObj.Cost = oldObj.Cost;
|
|
}
|
|
}
|
|
|
|
//Pricing
|
|
if (SnapshotPricing)
|
|
{
|
|
//default in case nothing to apply
|
|
newObj.Cost = 0;
|
|
newObj.ListPrice = 0;
|
|
|
|
LoanUnit loanUnit = await ct.LoanUnit.AsNoTracking().FirstOrDefaultAsync(x => x.Id == newObj.LoanUnitId);
|
|
if (loanUnit != null)
|
|
{
|
|
switch (newObj.Rate)
|
|
{
|
|
case LoanUnitRateUnit.None:
|
|
break;
|
|
case LoanUnitRateUnit.Hours:
|
|
newObj.Cost = loanUnit.RateHourCost;
|
|
newObj.ListPrice = loanUnit.RateHour;
|
|
break;
|
|
|
|
case LoanUnitRateUnit.HalfDays:
|
|
newObj.Cost = loanUnit.RateHalfDayCost;
|
|
newObj.ListPrice = loanUnit.RateHalfDay;
|
|
break;
|
|
case LoanUnitRateUnit.Days:
|
|
newObj.Cost = loanUnit.RateDayCost;
|
|
newObj.ListPrice = loanUnit.RateDay;
|
|
break;
|
|
case LoanUnitRateUnit.Weeks:
|
|
newObj.Cost = loanUnit.RateWeekCost;
|
|
newObj.ListPrice = loanUnit.RateWeek;
|
|
break;
|
|
case LoanUnitRateUnit.Months:
|
|
newObj.Cost = loanUnit.RateMonthCost;
|
|
newObj.ListPrice = loanUnit.RateMonth;
|
|
break;
|
|
case LoanUnitRateUnit.Years:
|
|
newObj.Cost = loanUnit.RateYearCost;
|
|
newObj.ListPrice = loanUnit.RateYear;
|
|
break;
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task LoanValidateAsync(WorkOrderItemLoan proposedObj, WorkOrderItemLoan currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
if (proposedObj.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");
|
|
|
|
if (proposedObj.Quantity < 0)//negative quantities are not allowed
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Quantity");
|
|
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.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
|
|
}
|
|
}
|
|
|
|
private void LoanValidateCanDelete(WorkOrderItemLoan obj)
|
|
{
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemLoan oProposed = (WorkOrderItemLoan)proposedObj;
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
//for notification purposes because has no name / tags field itself
|
|
oProposed.Name = WorkorderInfo.Serial.ToString();
|
|
oProposed.Tags = WorkorderInfo.Tags;
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
}//end of process notifications
|
|
|
|
|
|
#endregion work order item LOAN level
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
██████╗ ██╗ ██╗████████╗███████╗██╗██████╗ ███████╗ ███████╗███████╗██████╗ ██╗ ██╗██╗ ██████╗███████╗
|
|
██╔═══██╗██║ ██║╚══██╔══╝██╔════╝██║██╔══██╗██╔════╝ ██╔════╝██╔════╝██╔══██╗██║ ██║██║██╔════╝██╔════╝
|
|
██║ ██║██║ ██║ ██║ ███████╗██║██║ ██║█████╗ ███████╗█████╗ ██████╔╝██║ ██║██║██║ █████╗
|
|
██║ ██║██║ ██║ ██║ ╚════██║██║██║ ██║██╔══╝ ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██║██║ ██╔══╝
|
|
╚██████╔╝╚██████╔╝ ██║ ███████║██║██████╔╝███████╗ ███████║███████╗██║ ██║ ╚████╔╝ ██║╚██████╗███████╗
|
|
╚═════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝╚═════╝ ╚══════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝╚══════╝
|
|
|
|
|
|
|
|
*/
|
|
|
|
#region 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
|
|
{
|
|
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 OutsideServicePopulateVizFields(newObject);
|
|
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemOutsideService> OutsideServiceGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
if (UserIsSubContractorRestricted || UserIsSubContractorFull) //no access allowed at all
|
|
return null;
|
|
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;
|
|
}
|
|
await OutsideServiceValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await OutsideServiceExistsAsync(putObject.Id))
|
|
AddError(ApiErrorCode.NOT_FOUND);
|
|
else
|
|
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
|
|
return null;
|
|
}
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
|
|
await OutsideServiceSearchIndexAsync(putObject, false);
|
|
await OutsideServicePopulateVizFields(putObject);
|
|
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> OutsideServiceDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
var dbObject = await OutsideServiceGetAsync(id, false);
|
|
OutsideServiceValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.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);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
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 = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100));
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100));
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100));
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task OutsideServiceValidateAsync(WorkOrderItemOutsideService proposedObj, WorkOrderItemOutsideService currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
if (proposedObj.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
|
|
|
|
}
|
|
}
|
|
|
|
|
|
private void OutsideServiceValidateCanDelete(WorkOrderItemOutsideService obj)
|
|
{
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
|
|
WorkOrderItemOutsideService oProposed = (WorkOrderItemOutsideService)proposedObj;
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
//for notification purposes because has no name / tags field itself
|
|
oProposed.Name = WorkorderInfo.Serial.ToString();
|
|
oProposed.Tags = WorkorderInfo.Tags;
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
//## DELETED EVENTS
|
|
//standard process above will remove any hanging around when deleted, nothing else specific here to deal with
|
|
|
|
|
|
|
|
//## 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> PartCreateAsync(WorkOrderItemPart newObject)
|
|
{
|
|
using (var transaction = await ct.Database.BeginTransactionAsync())
|
|
{
|
|
await PartValidateAsync(newObject, null);
|
|
if (HasErrors)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
await PartBizActionsAsync(AyaEvent.Created, newObject, null);
|
|
|
|
|
|
await ct.WorkOrderItemPart.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
if (AyaNova.Util.ServerGlobalBizSettings.Cache.UseInventory)
|
|
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 PartPopulateVizFields(newObject);
|
|
await PartHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemPart> PartGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
if (UserIsSubContractorRestricted) //no access allowed at all
|
|
return null;
|
|
|
|
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;
|
|
}
|
|
|
|
await PartValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
await PartBizActionsAsync(AyaEvent.Modified, putObject, dbObject);
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
if (AyaNova.Util.ServerGlobalBizSettings.Cache.UseInventory)
|
|
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 transaction.CommitAsync();
|
|
await PartPopulateVizFields(putObject);
|
|
await PartHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> PartDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
var dbObject = await PartGetAsync(id, false);
|
|
PartValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
await PartBizActionsAsync(AyaEvent.Deleted, null, dbObject);
|
|
ct.WorkOrderItemPart.Remove(dbObject);
|
|
await ct.SaveChangesAsync();
|
|
if (AyaNova.Util.ServerGlobalBizSettings.Cache.UseInventory)
|
|
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);
|
|
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await PartHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
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.PartNameViz = part.Name;
|
|
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.TaxCodeViz = Tax.Name;
|
|
|
|
o.PriceViz = 0;
|
|
if (part != null)
|
|
{
|
|
//COST & PRICE NOT SET HERE, SET IN BIZACTIONS SNAPSHOTTED
|
|
// o.CostViz = part.Cost;
|
|
// o.ListPriceViz = part.Retail;
|
|
o.UnitOfMeasureViz = part.UnitOfMeasure;
|
|
o.PriceViz = o.ListPrice;//default price used if not manual or contract override
|
|
}
|
|
|
|
//manual price overrides anything
|
|
if (o.PriceOverride != null)
|
|
o.PriceViz = (decimal)o.PriceOverride;
|
|
else
|
|
{
|
|
//not manual so could potentially have a contract adjustment
|
|
var c = await 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 = MoneyUtil.Round(o.Cost + (o.Cost * pct));
|
|
else if (cot == ContractOverrideType.PriceDiscount)
|
|
o.PriceViz = MoneyUtil.Round(o.ListPrice - (o.ListPrice * pct));
|
|
}
|
|
}
|
|
}
|
|
|
|
//Calculate totals and taxes
|
|
//NET
|
|
o.NetViz = MoneyUtil.Round(o.PriceViz * o.Quantity);
|
|
|
|
//TAX
|
|
o.TaxAViz = 0;
|
|
o.TaxBViz = 0;
|
|
if (Tax != null)
|
|
{
|
|
if (Tax.TaxAPct != 0)
|
|
{
|
|
o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100));
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100));
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100));
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
|
|
|
|
|
|
//RESTRICTED COST FIELD??
|
|
if (!UserCanViewPartCosts)
|
|
o.Cost = 0;//cost already used in calcs and will not be updated on any update operation so this ensures the cost isn't sent over the wire
|
|
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//BIZ ACTIONS
|
|
//
|
|
//
|
|
private async Task PartBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemPart newObj, WorkOrderItemPart oldObj)
|
|
{
|
|
|
|
|
|
//SNAPSHOT PRICING IF NECESSARY
|
|
if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
|
|
return;
|
|
|
|
//SNAPSHOT PRICING
|
|
bool SnapshotPricing = true;
|
|
|
|
//if modifed, see what has changed and should be re-applied
|
|
if (ayaEvent == AyaEvent.Modified)
|
|
{
|
|
//If it wasn't a complete part change there is no need to set pricing
|
|
if (newObj.PartId == oldObj.PartId)
|
|
{
|
|
SnapshotPricing = false;
|
|
//maintain old cost as it can come from the client as zero when it shouldn't be or someone using the api and setting it directly
|
|
//but we will only allow the price *we* set at the server initially
|
|
newObj.Cost = oldObj.Cost;
|
|
}
|
|
}
|
|
|
|
//Pricing
|
|
if (SnapshotPricing)
|
|
{
|
|
//default in case nothing to apply
|
|
newObj.Cost = 0;
|
|
newObj.ListPrice = 0;
|
|
|
|
|
|
var s = await ct.Part.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.PartId);
|
|
if (s != null)
|
|
{
|
|
newObj.Cost = s.Cost;
|
|
newObj.ListPrice = s.Retail;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//BIZ ACTIONS
|
|
//
|
|
//
|
|
private async Task PartInventoryAdjustmentAsync(AyaEvent ayaEvent, WorkOrderItemPart newObj, WorkOrderItemPart oldObj, IDbContextTransaction transaction)
|
|
{
|
|
|
|
PartInventoryBiz pib = new PartInventoryBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
|
|
|
|
//DELETED, HANDLE INVENTORY / SERIALS
|
|
if (ayaEvent == AyaEvent.Deleted && oldObj.Quantity != 0)
|
|
{
|
|
//NEGATIVE BLOCK >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
//was originally a return of inventory so needs to consume the inventory back again
|
|
if (oldObj.Quantity < 0)
|
|
{
|
|
dtInternalPartInventory pi =
|
|
new dtInternalPartInventory
|
|
{
|
|
PartId = oldObj.PartId,
|
|
PartWarehouseId = oldObj.PartWarehouseId,
|
|
Quantity = oldObj.Quantity,//was originally returned (negative) and added to inventory so to reverse just use as is to remain negative and consume out of inventory
|
|
SourceType = AyaType.WorkOrderItemPart,
|
|
SourceId = oldObj.Id,
|
|
Description = await Translate("WorkOrderItemPart") + $" {oldObj.Serials} " + await Translate("EventDeleted")
|
|
};
|
|
if (await pib.CreateAsync(pi) == null)
|
|
{
|
|
if (pib.HasErrors)
|
|
{
|
|
if (pib.Errors.Count == 1 && pib.Errors[0].Code == ApiErrorCode.INSUFFICIENT_INVENTORY)
|
|
{
|
|
AddError(pib.Errors[0].Code, "Quantity", pib.Errors[0].Message);
|
|
return;
|
|
}
|
|
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}");
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
else
|
|
{ //Consume serial numbers from part
|
|
if (!string.IsNullOrWhiteSpace(oldObj.Serials))
|
|
await PartBiz.RemoveSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId);
|
|
}
|
|
//<<<<<<<<<<<<<<<<<<<<<< NEGATIVE BLOCK
|
|
}
|
|
else
|
|
{
|
|
//was consumed inventory so return it
|
|
dtInternalPartInventory pi =
|
|
new dtInternalPartInventory
|
|
{
|
|
PartId = oldObj.PartId,
|
|
PartWarehouseId = oldObj.PartWarehouseId,
|
|
Quantity = oldObj.Quantity,//was originally positive and was flipped to consume inventory so leave positive to add back to inventory
|
|
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
|
|
{
|
|
|
|
if (newObj.Quantity < 0)
|
|
{
|
|
//NEGATIVE BLOCK >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
//NOTES: new record with negative quantity, this should only be a case where a customer is returning unused parts that were originally consumed by them on a work order
|
|
//this already requires the inventory full role during validation so if we're here it's allowed to happen
|
|
//in v7 this was openly allowed by anyone who could make a work order but it was a loophole to circumvent inventory adjustment system
|
|
//in v8 we allow it only for the narrowly defined pupose of reversing a prior work order consumption of parts but it's not really enforced in code other than requiring a
|
|
//inventory full role to do it so that the inventory person is at least involved in what's happening and can think it through
|
|
|
|
//RETURN INVENTORY
|
|
dtInternalPartInventory pi =
|
|
new dtInternalPartInventory
|
|
{
|
|
PartId = newObj.PartId,
|
|
PartWarehouseId = newObj.PartWarehouseId,
|
|
Quantity = newObj.Quantity * -1,//is negative, needs to be positive as it's adding inventory
|
|
SourceType = AyaType.WorkOrderItemPart,
|
|
SourceId = newObj.Id,
|
|
Description = await Translate("WorkOrderItemPart") + $" {newObj.Serials} " + await Translate("EventCreated")
|
|
};
|
|
if (await pib.CreateAsync(pi) == null)
|
|
{
|
|
if (pib.HasErrors)
|
|
{
|
|
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}");
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
else
|
|
{ //Append serial numbers from part being returned
|
|
if (!string.IsNullOrWhiteSpace(newObj.Serials))
|
|
await PartBiz.AppendSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId);
|
|
}
|
|
|
|
//<<<<<<<<<<<<<<<<<<<<<< NEGATIVE BLOCK
|
|
}
|
|
else
|
|
{
|
|
//MAIN ROUTE DURING MIGRATE
|
|
//CONSUME INVENTORY
|
|
dtInternalPartInventory pi =
|
|
new dtInternalPartInventory
|
|
{
|
|
PartId = newObj.PartId,
|
|
PartWarehouseId = newObj.PartWarehouseId,
|
|
Quantity = newObj.Quantity * -1,//is positive needs to be negative as it's consuming inventory
|
|
SourceType = AyaType.WorkOrderItemPart,
|
|
SourceId = newObj.Id,
|
|
Description = await Translate("WorkOrderItemPart") + $" {newObj.Serials} " + await Translate("EventCreated")
|
|
};
|
|
if (await pib.CreateAsync(pi) == null)
|
|
{
|
|
if (pib.HasErrors)
|
|
{
|
|
if (pib.Errors.Count == 1 && pib.Errors[0].Code == ApiErrorCode.INSUFFICIENT_INVENTORY)
|
|
{
|
|
AddError(pib.Errors[0].Code, "Quantity", pib.Errors[0].Message);
|
|
return;
|
|
}
|
|
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}");
|
|
return;
|
|
}
|
|
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)
|
|
{
|
|
//QUANTITY OR PART CHANGE?
|
|
if (newObj.PartId != oldObj.PartId || newObj.Quantity != oldObj.Quantity)
|
|
{
|
|
//OUT with the old object SAME AS DELETE ABOVE
|
|
if (oldObj.Quantity != 0)//zero quantity doesn't affect inventory or serials
|
|
{
|
|
if (oldObj.Quantity < 0)
|
|
{
|
|
//NEGATIVE BLOCK >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
//Was negative so add back to inventory / serials
|
|
dtInternalPartInventory pi =
|
|
new dtInternalPartInventory
|
|
{
|
|
PartId = oldObj.PartId,
|
|
PartWarehouseId = oldObj.PartWarehouseId,
|
|
Quantity = oldObj.Quantity,//was originally returned (negative) and added to inventory so to reverse just use as is to remain negative and consume out of inventory
|
|
SourceType = AyaType.WorkOrderItemPart,
|
|
SourceId = oldObj.Id,
|
|
Description = await Translate("WorkOrderItemPart") + $" {oldObj.Serials} " + await Translate("EventDeleted")
|
|
};
|
|
if (await pib.CreateAsync(pi) == null)
|
|
{
|
|
if (pib.HasErrors)
|
|
{
|
|
if (pib.Errors.Count == 1 && pib.Errors[0].Code == ApiErrorCode.INSUFFICIENT_INVENTORY)
|
|
{
|
|
AddError(pib.Errors[0].Code, "Quantity", pib.Errors[0].Message);
|
|
return;
|
|
}
|
|
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}");
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
else
|
|
{ //Consume serial numbers from part
|
|
if (!string.IsNullOrWhiteSpace(oldObj.Serials))
|
|
await PartBiz.RemoveSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId);
|
|
}
|
|
//<<<<<<<<<<<<<<<<<<<<<< NEGATIVE BLOCK
|
|
}
|
|
else
|
|
{
|
|
dtInternalPartInventory piOld = new dtInternalPartInventory
|
|
{
|
|
PartId = oldObj.PartId,
|
|
PartWarehouseId = oldObj.PartWarehouseId,
|
|
Quantity = oldObj.Quantity,//was originally positive and was flipped to consume inventory so leave positive to add back to inventory
|
|
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 object
|
|
if (newObj.Quantity != 0)//zero quantity is considered to be a placeholder and no serials will be consumed, nor inventory affected
|
|
{
|
|
if (newObj.Quantity < 0)
|
|
{
|
|
//NEGATIVE BLOCK >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
//RETURN INVENTORY
|
|
dtInternalPartInventory pi =
|
|
new dtInternalPartInventory
|
|
{
|
|
PartId = newObj.PartId,
|
|
PartWarehouseId = newObj.PartWarehouseId,
|
|
Quantity = newObj.Quantity * -1,//is negative, needs to be positive as it's adding inventory
|
|
SourceType = AyaType.WorkOrderItemPart,
|
|
SourceId = newObj.Id,
|
|
Description = await Translate("WorkOrderItemPart") + $" {newObj.Serials} " + await Translate("EventCreated")
|
|
};
|
|
if (await pib.CreateAsync(pi) == null)
|
|
{
|
|
if (pib.HasErrors)
|
|
{
|
|
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}");
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
else
|
|
{ //Append serial numbers from part being returned
|
|
if (!string.IsNullOrWhiteSpace(newObj.Serials))
|
|
await PartBiz.AppendSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId);
|
|
}
|
|
|
|
//<<<<<<<<<<<<<<<<<<<<<< NEGATIVE BLOCK
|
|
}
|
|
else
|
|
{
|
|
|
|
dtInternalPartInventory piNew = new dtInternalPartInventory
|
|
{
|
|
PartId = newObj.PartId,
|
|
PartWarehouseId = newObj.PartWarehouseId,
|
|
Quantity = newObj.Quantity * -1,//is positive needs to be negative as it's consuming inventory
|
|
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;
|
|
if (pib.HasErrors)
|
|
{
|
|
if (pib.Errors.Count == 1 && pib.Errors[0].Code == ApiErrorCode.INSUFFICIENT_INVENTORY)
|
|
{
|
|
AddError(pib.Errors[0].Code, "Quantity", pib.Errors[0].Message);
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}//end block for quantity or part change
|
|
else if (newObj.Serials != oldObj.Serials)//SERIALS CHANGE ONLY not quantity or partid so not handled already above
|
|
{
|
|
//NOTE: zero quantity is considered to be a placeholder and no serials will be consumed (hence not returned either)
|
|
|
|
|
|
//Old object had serials?
|
|
if (!string.IsNullOrWhiteSpace(oldObj.Serials))
|
|
{
|
|
//Reverse old object serials transaction...
|
|
if (oldObj.Quantity > 0)//old object removed serials to add them back
|
|
await PartBiz.AppendSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId);
|
|
if (oldObj.Quantity < 0)//old object added to serials so remove them
|
|
await PartBiz.RemoveSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId);
|
|
}
|
|
|
|
//New object has serials?
|
|
if (!string.IsNullOrWhiteSpace(newObj.Serials))
|
|
{
|
|
//do new object serials transaction
|
|
if (newObj.Quantity > 0)//consuming serials
|
|
await PartBiz.RemoveSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId);
|
|
if (newObj.Quantity < 0)//returning serials
|
|
await PartBiz.AppendSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId);
|
|
}
|
|
}
|
|
}//end MODIFIED block
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task PartValidateAsync(WorkOrderItemPart proposedObj, WorkOrderItemPart currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//Parts: no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
if (proposedObj.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 (!await BizObjectExistsInDatabase.ExistsAsync(AyaType.Part, proposedObj.PartId, ct))
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "PartId");
|
|
return;
|
|
}
|
|
|
|
if (AyaNova.Util.ServerGlobalBizSettings.Cache.UseInventory)
|
|
if (proposedObj.Quantity < 0 && !UserHasInventoryFullRole)//negative quantities are not allowed unless the user has inventory full role
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Quantity", await Translate("InventoryRoleRequired"));
|
|
|
|
//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
|
|
|
|
}
|
|
}
|
|
|
|
|
|
private void PartValidateCanDelete(WorkOrderItemPart obj)
|
|
{
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//Parts: no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemPart oProposed = (WorkOrderItemPart)proposedObj;
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name / tags field itself
|
|
oProposed.Tags = WorkorderInfo.Tags;
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
}//end of process notifications
|
|
|
|
#endregion work order item PARTS level
|
|
|
|
|
|
/*
|
|
██████╗ █████╗ ██████╗ ████████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗███████╗███████╗████████╗███████╗
|
|
██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝ ██╔══██╗██╔════╝██╔═══██╗██║ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝
|
|
██████╔╝███████║██████╔╝ ██║█████╗██████╔╝█████╗ ██║ ██║██║ ██║█████╗ ███████╗ ██║ ███████╗
|
|
██╔═══╝ ██╔══██║██╔══██╗ ██║╚════╝██╔══██╗██╔══╝ ██║▄▄ ██║██║ ██║██╔══╝ ╚════██║ ██║ ╚════██║
|
|
██║ ██║ ██║██║ ██║ ██║ ██║ ██║███████╗╚██████╔╝╚██████╔╝███████╗███████║ ██║ ███████║
|
|
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝
|
|
*/
|
|
|
|
#region 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
|
|
{
|
|
await ct.WorkOrderItemPartRequest.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
|
|
await PartRequestPopulateVizFields(newObject);
|
|
await PartRequestHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemPartRequest> PartRequestGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
if (UserIsSubContractorRestricted) //no access allowed at all
|
|
return null;
|
|
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;
|
|
}
|
|
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 PartRequestPopulateVizFields(putObject);
|
|
await PartRequestHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> PartRequestDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
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);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await PartRequestHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
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;
|
|
|
|
if (o.RequestedByUserId != null)
|
|
{
|
|
o.RequestedByUserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.RequestedByUserId).Select(x => x.Name).FirstOrDefaultAsync();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task PartRequestValidateAsync(WorkOrderItemPartRequest proposedObj, WorkOrderItemPartRequest currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//PartRequests: no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
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 (!await BizObjectExistsInDatabase.ExistsAsync(AyaType.Part, proposedObj.PartId, ct))
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "PartId");
|
|
return;
|
|
}
|
|
|
|
if (proposedObj.Quantity < 0)//negative quantities are not allowed
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Quantity");
|
|
|
|
|
|
|
|
//UGLY workaround kind of, in order to avoid some poitem fuckery related to the dual reciprocal link between woitempartrequests adn poitems
|
|
//set the new item received quantity to zero if there is no poitemid (this is because the db itself will set the poitemid if the poitem is delete but not update the quantity received)
|
|
if (proposedObj.PurchaseOrderItemId == null && proposedObj.Received != 0)
|
|
proposedObj.Received = 0;//not a biz rule but a biz adaptation
|
|
|
|
//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
|
|
}
|
|
}
|
|
|
|
private void PartRequestValidateCanDelete(WorkOrderItemPartRequest obj)
|
|
{
|
|
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//PartRequests: no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemPartRequest oProposed = (WorkOrderItemPartRequest)proposedObj;
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).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
|
|
{
|
|
await ct.WorkOrderItemScheduledUser.AddAsync(newObject);
|
|
await ct.SaveChangesAsync();
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
|
|
await ScheduledUserPopulateVizFields(newObject);
|
|
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemScheduledUser> ScheduledUserGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var ret = await ct.WorkOrderItemScheduledUser.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
|
|
if (UserIsRestrictedType && ret.UserId != UserId)//restricted users can only see their own
|
|
return null;
|
|
if (logTheGetEvent && ret != null)
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
|
|
return ret;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//UPDATE
|
|
//
|
|
internal async Task<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;
|
|
}
|
|
await ScheduledUserValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await ScheduledUserExistsAsync(putObject.Id))
|
|
AddError(ApiErrorCode.NOT_FOUND);
|
|
else
|
|
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
|
|
return null;
|
|
}
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
|
|
await ScheduledUserPopulateVizFields(putObject);
|
|
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//UPDATE schedule only
|
|
//
|
|
internal async Task<bool> ScheduledUserPutNewScheduleTimeAsync(ScheduleItemAdjustParams p)
|
|
{
|
|
WorkOrderItemScheduledUser dbObject = await ct.WorkOrderItemScheduledUser.SingleOrDefaultAsync(z => z.Id == p.Id);
|
|
if (dbObject == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return false;
|
|
}
|
|
|
|
dbObject.StartDate = p.Start;
|
|
dbObject.StopDate = p.End;
|
|
dbObject.UserId = p.UserId;
|
|
|
|
await ScheduledUserValidateAsync(dbObject, dbObject);
|
|
if (HasErrors) return false;
|
|
// ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await ScheduledUserExistsAsync(dbObject.Id))
|
|
AddError(ApiErrorCode.NOT_FOUND);
|
|
else
|
|
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
|
|
return false;
|
|
}
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, dbObject.AyaType, AyaEvent.Modified), ct);
|
|
await ScheduledUserPopulateVizFields(dbObject);
|
|
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, dbObject);
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> ScheduledUserDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
|
|
var dbObject = await ScheduledUserGetAsync(id, false);
|
|
ScheduledUserValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.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);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET Schedule view "more info" display values
|
|
//
|
|
internal async Task<object> ScheduledUserGetScheduleInfoViewAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
var w = await WorkOrderGetPartialAsync(AyaType.WorkOrderItemScheduledUser, id, false, true);
|
|
if (UserIsRestrictedType && w.Items[0].ScheduledUsers[0].UserId != UserId)//restricted users can only see their own
|
|
return null;
|
|
string WoStatusName = string.Empty;
|
|
string WoStatusColor = string.Empty;
|
|
bool WoStatusCompleted = false;
|
|
bool WoStatusLocked = false;
|
|
bool HasWoStatus = false;
|
|
if (w.States.Count > 0)
|
|
{
|
|
var st = w.States[w.States.Count - 1];
|
|
HasWoStatus = true;
|
|
WoStatusName = st.NameViz;
|
|
WoStatusColor = st.ColorViz;
|
|
WoStatusCompleted = st.CompletedViz;
|
|
WoStatusLocked = st.LockedViz;
|
|
}
|
|
return new
|
|
{
|
|
serial = w.Serial,
|
|
w.CustomerViz,
|
|
wosummary = w.Notes,
|
|
haswostatus = HasWoStatus,
|
|
wostatus = WoStatusName,
|
|
wostatuscolor = WoStatusColor,
|
|
wostatuscompleted = WoStatusCompleted,
|
|
wostatuslocked = WoStatusLocked,
|
|
woitemnotes = w.Items[0].Notes,
|
|
woitemtechnotes = w.Items[0].TechNotes,
|
|
woitemstatus = w.Items[0].WorkOrderItemStatusNameViz,
|
|
woitemstatuscolor = w.Items[0].WorkOrderItemStatusColorViz,
|
|
woitempriority = w.Items[0].WorkOrderItemPriorityNameViz,
|
|
woitemprioritycolor = w.Items[0].WorkOrderItemPriorityColorViz,
|
|
scheduser = w.Items[0].ScheduledUsers[0].UserViz,
|
|
w.Items[0].ScheduledUsers[0].StartDate,
|
|
w.Items[0].ScheduledUsers[0].StopDate,
|
|
qty = w.Items[0].ScheduledUsers[0].EstimatedQuantity,
|
|
rate = w.Items[0].ScheduledUsers[0].ServiceRateViz,
|
|
wotags = w.Tags,
|
|
woitemtags = w.Items[0].Tags
|
|
};
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//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 || ServerBootConfig.MIGRATING) 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
|
|
}
|
|
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//Scheduled Users: view only where they are the selected User and convert to labor record
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
//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.EstimatedQuantity < 0)//negative quantities are not allowed
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "EstimatedQuantity");
|
|
|
|
|
|
//Start date AND end date must both be null or both contain values
|
|
if (proposedObj.StartDate == null && proposedObj.StopDate != null)
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "StopDate");
|
|
|
|
if (proposedObj.StartDate != null && proposedObj.StopDate == null)
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "StopDate");
|
|
|
|
//Start date before end date
|
|
if (proposedObj.StartDate != null && proposedObj.StopDate != null)
|
|
if (proposedObj.StartDate > proposedObj.StopDate)
|
|
AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "StartDate");
|
|
|
|
//Scheduling conflict?
|
|
if (!AyaNova.Util.ServerGlobalBizSettings.Cache.AllowScheduleConflicts
|
|
&& proposedObj.IsPMGenerated == false
|
|
&& proposedObj.UserId != null
|
|
&& proposedObj.StartDate != null
|
|
&& proposedObj.StopDate != null
|
|
&& (isNew
|
|
|| (proposedObj.StartDate != currentObj.StartDate)
|
|
|| (proposedObj.StopDate != currentObj.StopDate)
|
|
|| (proposedObj.UserId != currentObj.UserId)
|
|
))
|
|
{
|
|
if (await ct.WorkOrderItemScheduledUser.AnyAsync(x => x.Id != proposedObj.Id
|
|
&& x.UserId == proposedObj.UserId
|
|
&& x.StartDate <= proposedObj.StopDate
|
|
&& proposedObj.StartDate <= x.StopDate))
|
|
{
|
|
AddError(ApiErrorCode.VALIDATION_FAILED, "StartDate", await Translate("ScheduleConflict"));
|
|
AddError(ApiErrorCode.VALIDATION_FAILED, "StopDate", await Translate("ScheduleConflict"));
|
|
}
|
|
}
|
|
//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
|
|
|
|
}
|
|
}
|
|
|
|
private void ScheduledUserValidateCanDelete(WorkOrderItemScheduledUser obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//Scheduled Users: view only where they are the selected User and convert to labor record
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemScheduledUser oProposed = (WorkOrderItemScheduledUser)proposedObj;
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name field itself
|
|
oProposed.Tags = WorkorderInfo.Tags; //for notification purposes because has no tag field itself
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
|
|
//## 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
|
|
{
|
|
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 TaskPopulateVizFields(newObject);
|
|
await TaskHandlePotentialNotificationEvent(AyaEvent.Created, 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;
|
|
}
|
|
await TaskValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await TaskExistsAsync(putObject.Id))
|
|
AddError(ApiErrorCode.NOT_FOUND);
|
|
else
|
|
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
|
|
return null;
|
|
}
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
|
|
await TaskSearchIndexAsync(dbObject, false);
|
|
await TaskPopulateVizFields(putObject);
|
|
await TaskHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> TaskDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
var dbObject = await TaskGetAsync(id, false);
|
|
TaskValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.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);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await TaskHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
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 || ServerBootConfig.MIGRATING) 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 (isNew && UserIsRestrictedType)
|
|
{
|
|
//restricted users are not allowed to make new task entries only fill them out
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
if (!isNew && UserIsRestrictedType)
|
|
{
|
|
//Existing record so just make sure they haven't changed the not changeable fields from the db version
|
|
|
|
//* Tasks: view and edit existing tasks, set completion type and date only, no add or remove or changing other fields
|
|
//note that UI will prevent this, this rule is only backup for 3rd party api users
|
|
if (currentObj.Task != proposedObj.Task) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "Task");
|
|
if (currentObj.CompletedByUserId != UserId) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "CompletedByUserId");
|
|
if (currentObj.Sequence != proposedObj.Sequence) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "Sequence");
|
|
}
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(proposedObj.Task))
|
|
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Task");
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.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
|
|
}
|
|
}
|
|
|
|
private void TaskValidateCanDelete(WorkOrderItemTask obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//restricted users are not allowed to delete a task only fill them out
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemTask oProposed = (WorkOrderItemTask)proposedObj;
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name / tags field itself
|
|
oProposed.Tags = WorkorderInfo.Tags;
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
}//end of process notifications
|
|
|
|
|
|
#endregion work order item TASK level
|
|
|
|
|
|
/*
|
|
████████╗██████╗ █████╗ ██╗ ██╗███████╗██╗
|
|
╚══██╔══╝██╔══██╗██╔══██╗██║ ██║██╔════╝██║
|
|
██║ ██████╔╝███████║██║ ██║█████╗ ██║
|
|
██║ ██╔══██╗██╔══██║╚██╗ ██╔╝██╔══╝ ██║
|
|
██║ ██║ ██║██║ ██║ ╚████╔╝ ███████╗███████╗
|
|
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝
|
|
*/
|
|
|
|
#region 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 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 TravelPopulateVizFields(newObject);
|
|
await TravelHandlePotentialNotificationEvent(AyaEvent.Created, 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 (UserIsRestrictedType && ret.UserId != UserId)
|
|
{
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return null;
|
|
}
|
|
if (logTheGetEvent && ret != null)
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
|
|
return ret;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//UPDATE
|
|
//
|
|
internal async Task<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;
|
|
}
|
|
|
|
await TravelValidateAsync(putObject, dbObject);
|
|
if (HasErrors) return null;
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await TravelExistsAsync(putObject.Id))
|
|
AddError(ApiErrorCode.NOT_FOUND);
|
|
else
|
|
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
|
|
return null;
|
|
}
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
|
|
await TravelSearchIndexAsync(putObject, false);
|
|
await TravelPopulateVizFields(putObject);
|
|
await TravelHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> TravelDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
var dbObject = await TravelGetAsync(id, false);
|
|
TravelValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.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);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await TravelHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
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.TaxCodeViz = Tax.Name;
|
|
|
|
o.PriceViz = 0;
|
|
if (Rate != null)
|
|
{
|
|
o.CostViz = Rate.Cost;
|
|
o.ListPriceViz = Rate.Charge;
|
|
o.UnitOfMeasureViz = Rate.Unit;
|
|
o.PriceViz = Rate.Charge;//default price used if not manual or contract override
|
|
}
|
|
|
|
//manual price overrides anything
|
|
if (o.PriceOverride != null)
|
|
o.PriceViz = (decimal)o.PriceOverride;
|
|
else
|
|
{
|
|
//not manual so could potentially have a contract adjustment
|
|
var c = await 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 = MoneyUtil.Round(o.CostViz + (o.CostViz * pct));
|
|
else if (cot == ContractOverrideType.PriceDiscount)
|
|
o.PriceViz = MoneyUtil.Round(o.ListPriceViz - (o.ListPriceViz * pct));
|
|
}
|
|
}
|
|
}
|
|
|
|
//Calculate totals and taxes
|
|
//NET
|
|
o.NetViz = MoneyUtil.Round(o.PriceViz * o.TravelRateQuantity);
|
|
|
|
//TAX
|
|
o.TaxAViz = 0;
|
|
o.TaxBViz = 0;
|
|
if (Tax != null)
|
|
{
|
|
if (Tax.TaxAPct != 0)
|
|
{
|
|
o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100));
|
|
}
|
|
if (Tax.TaxBPct != 0)
|
|
{
|
|
if (Tax.TaxOnTax)
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100));
|
|
}
|
|
else
|
|
{
|
|
o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100));
|
|
}
|
|
}
|
|
}
|
|
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
|
|
|
|
//RESTRICTIONS ON COST VISIBILITY?
|
|
if (!UserCanViewLaborOrTravelRateCosts)
|
|
{
|
|
o.CostViz = 0;
|
|
}
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task TravelValidateAsync(WorkOrderItemTravel proposedObj, WorkOrderItemTravel currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) 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 (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId)))
|
|
{
|
|
//no edits allowed on other people's records
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED);
|
|
return;
|
|
}
|
|
|
|
if (proposedObj.TravelRateQuantity < 0)//negative quantities are not allowed
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "TravelRateQuantity");
|
|
|
|
if (proposedObj.NoChargeQuantity < 0)//negative quantities are not allowed
|
|
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "NoChargeQuantity");
|
|
|
|
//Any form customizations to validate?
|
|
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.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
|
|
}
|
|
}
|
|
|
|
private void TravelValidateCanDelete(WorkOrderItemTravel obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//Travels: add (no user selection defaults to themselves), remove, view and edit only when they are the selected User
|
|
if (obj.UserId != UserId)
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemTravel oProposed = (WorkOrderItemTravel)proposedObj;
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
oProposed.Name = WorkorderInfo.Serial.ToString();//for notification purposes because has no name / tags field itself
|
|
oProposed.Tags = WorkorderInfo.Tags;
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
}//end of process notifications
|
|
|
|
|
|
#endregion work order item TRAVEL level
|
|
|
|
|
|
/*
|
|
██╗ ██╗███╗ ██╗██╗████████╗
|
|
██║ ██║████╗ ██║██║╚══██╔══╝
|
|
██║ ██║██╔██╗ ██║██║ ██║
|
|
██║ ██║██║╚██╗██║██║ ██║
|
|
╚██████╔╝██║ ╚████║██║ ██║
|
|
╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
|
|
*/
|
|
|
|
#region 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
|
|
{
|
|
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 UnitPopulateVizFields(newObject, false);
|
|
await UnitHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
|
|
return newObject;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// GET
|
|
//
|
|
internal async Task<WorkOrderItemUnit> UnitGetAsync(long id, bool logTheGetEvent = true)
|
|
{
|
|
if (UserIsSubContractorRestricted) //no access allowed at all
|
|
return null;
|
|
|
|
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;
|
|
ct.Replace(dbObject, putObject);
|
|
try
|
|
{
|
|
await ct.SaveChangesAsync();
|
|
}
|
|
catch (DbUpdateConcurrencyException)
|
|
{
|
|
if (!await UnitExistsAsync(putObject.Id))
|
|
AddError(ApiErrorCode.NOT_FOUND);
|
|
else
|
|
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
|
|
return null;
|
|
}
|
|
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
|
|
await UnitSearchIndexAsync(putObject, false);
|
|
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
|
|
await UnitPopulateVizFields(putObject, false);
|
|
await UnitHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
|
|
return putObject;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//DELETE
|
|
//
|
|
internal async Task<bool> UnitDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
|
|
{
|
|
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
|
|
var dbObject = await UnitGetAsync(id, false);
|
|
UnitValidateCanDelete(dbObject);
|
|
if (HasErrors)
|
|
return false;
|
|
ct.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);
|
|
if (parentTransaction == null)
|
|
await transaction.CommitAsync();
|
|
await UnitHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
|
|
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.Wiki).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.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
|
|
return SearchParams;
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VIZ POPULATE
|
|
//
|
|
private async Task UnitPopulateVizFields(WorkOrderItemUnit o, bool populateForReporting)
|
|
{
|
|
var unitInfo = await ct.Unit.AsNoTracking()
|
|
.Where(x => x.Id == o.UnitId)
|
|
.Select(x => new { x.Serial, x.Description, x.UnitModelId, x.Address, x.City, x.Region, x.Country, x.Latitude, x.Longitude, x.Metered })
|
|
.FirstOrDefaultAsync();
|
|
o.UnitViz = unitInfo.Serial;
|
|
o.UnitDescriptionViz = unitInfo.Description;
|
|
o.UnitMeteredViz = unitInfo.Metered;
|
|
if (populateForReporting)
|
|
{
|
|
o.AddressViz = unitInfo.Address;
|
|
o.CityViz = unitInfo.City;
|
|
o.RegionViz = unitInfo.Region;
|
|
o.CountryViz = unitInfo.Country;
|
|
o.LatitudeViz = unitInfo.Latitude;
|
|
o.LongitudeViz = unitInfo.Longitude;
|
|
}
|
|
if (unitInfo.UnitModelId != null)
|
|
{
|
|
var unitModelInfo = await ct.UnitModel.AsNoTracking().Where(x => x.Id == unitInfo.UnitModelId).Select(x => new { x.Name, x.VendorId }).FirstOrDefaultAsync();
|
|
o.UnitModelNameViz = unitModelInfo.Name;
|
|
|
|
if (unitModelInfo.VendorId != null)
|
|
o.UnitModelVendorViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == unitModelInfo.VendorId).Select(x => x.Name).FirstOrDefaultAsync();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//VALIDATION
|
|
//
|
|
private async Task UnitValidateAsync(WorkOrderItemUnit proposedObj, WorkOrderItemUnit currentObj)
|
|
{
|
|
//skip validation if seeding
|
|
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
|
|
|
|
//run validation and biz rules
|
|
bool isNew = currentObj == null;
|
|
|
|
if (UserIsRestrictedType)
|
|
{
|
|
//Units: no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
if (proposedObj.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 (UserIsRestrictedType)
|
|
{
|
|
//Units: no edits allowed
|
|
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
|
|
return;
|
|
}
|
|
|
|
if (obj == null)
|
|
{
|
|
AddError(ApiErrorCode.NOT_FOUND, "id");
|
|
return;
|
|
}
|
|
|
|
//re-check rights here necessary due to traversal delete from Principle object
|
|
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.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 || ServerBootConfig.MIGRATING) return;
|
|
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
|
|
|
|
bool isNew = currentObj == null;
|
|
WorkOrderItemUnit oProposed = (WorkOrderItemUnit)proposedObj;
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
|
|
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
|
|
oProposed.Name = WorkorderInfo.Serial.ToString();//for notification purposes because has no name field itself
|
|
|
|
//STANDARD EVENTS FOR ALL OBJECTS
|
|
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
|
|
|
|
//SPECIFIC EVENTS FOR THIS OBJECT
|
|
|
|
}//end of process notifications
|
|
|
|
|
|
|
|
#endregion work order item LABOR level
|
|
|
|
|
|
|
|
#region Utility
|
|
public async Task<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, "Name");
|
|
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)
|
|
{
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct);
|
|
var WoContractId = await ct.WorkOrder.AsNoTracking().Where(z => z.Id == wid.ParentId).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 GetFullyPopulatedContractGraphFromIdAsync(id);
|
|
}
|
|
return mContractInEffect;
|
|
}
|
|
|
|
internal async Task<Contract> GetFullyPopulatedContractGraphFromIdAsync(long? id)
|
|
{
|
|
if (id == null) return null;
|
|
return await ct.Contract.AsSplitQuery().AsNoTracking()
|
|
.Include(c => c.ServiceRateItems)
|
|
.Include(c => c.TravelRateItems)
|
|
.Include(c => c.ContractPartOverrideItems)
|
|
.Include(c => c.ContractTravelRateOverrideItems)
|
|
.Include(c => c.ContractServiceRateOverrideItems)
|
|
.FirstOrDefaultAsync(z => z.Id == id);
|
|
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//GET CURRENT STATUS FOR WORKORDER FROM RELATIVE
|
|
//
|
|
|
|
//cache the state to save repeatedly fetching it for this operation which could be called multiple times in a flowv
|
|
internal WorkOrderStatus mCurrentWorkOrderStatus = null;
|
|
|
|
internal async Task<WorkOrderStatus> GetCurrentWorkOrderStatusFromRelatedAsync(AyaType ayaType, long id)
|
|
{
|
|
if (mCurrentWorkOrderStatus == null)
|
|
{
|
|
var wid = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct);
|
|
var stat = await ct.WorkOrderState.AsNoTracking()
|
|
.Where(z => z.WorkOrderId == wid.ParentId)
|
|
.OrderByDescending(z => z.Created)
|
|
.Take(1)
|
|
.FirstOrDefaultAsync();
|
|
|
|
//no state set yet?
|
|
if (stat == null)
|
|
mCurrentWorkOrderStatus = new WorkOrderStatus() { Id = -1, Locked = false, Completed = false };
|
|
else
|
|
mCurrentWorkOrderStatus = await ct.WorkOrderStatus.AsNoTracking().Where(z => z.Id == stat.WorkOrderStatusId).FirstAsync();//this should never not be null
|
|
}
|
|
return mCurrentWorkOrderStatus;
|
|
}
|
|
|
|
// internal static async Task<WorkOrderStatus> GetCurrentWorkOrderStatusFromRelatedAsync(AyaType ayaType, long id, AyContext ct)
|
|
// {
|
|
// //static method
|
|
// var wid = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct);
|
|
// var stat = await ct.WorkOrderState.AsNoTracking()
|
|
// .Where(z => z.WorkOrderId == wid.WorkOrderId)
|
|
// .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
|
|
|