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