Files
raven/server/AyaNova/biz/PMBiz.cs
2021-07-30 15:30:57 +00:00

6165 lines
284 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
}
/*
██████╗ ███╗ ███╗
██╔══██╗████╗ ████║
██████╔╝██╔████╔██║
██╔═══╝ ██║╚██╔╝██║
██║ ██║ ╚═╝ ██║
╚═╝ ╚═╝ ╚═╝
*/
#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;
}
}
}
//quote 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);//doing this here ahead of notification because notification may require the viz field lookup anyway and afaict no harm in it
await PMHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> PMDeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
try
{
PM dbObject = await ct.PM.AsNoTracking().Where(z => z.Id == id).FirstOrDefaultAsync();// PMGetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
PMValidateCanDelete(dbObject);
if (HasErrors)
return false;
//collect the child id's to delete
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);
}
catch
{
//NOTE: no need to rollback the transaction, it will auto-rollback if not committed and it is disposed when it goes out of scope either way
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task 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 (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)
{
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;
}
public void DigestSearchText(PM obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes).AddText(obj.Serial).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// "The Andy" notification helper
//
// (for now this is only for the notification exceeds total so only need one grand total of
// line totals, if in future need more can return a Record object instead with split out
// taxes, net etc etc)
//
private async Task<decimal> WorkorderGrandTotalAsync(long workOrderId, AyContext ct)
{
var wo = await ct.PM.AsNoTracking().AsSplitQuery()
.Include(w => w.Items.OrderBy(item => item.Sequence))
.ThenInclude(wi => wi.Expenses)
.Include(w => w.Items)
.ThenInclude(wi => wi.Labors)
.Include(w => w.Items)
.ThenInclude(wi => wi.Loans)
.Include(w => w.Items)
.ThenInclude(wi => wi.Parts)
.Include(w => w.Items)
.ThenInclude(wi => wi.Travels)
.Include(w => w.Items)
.ThenInclude(wi => wi.OutsideServices)
.SingleOrDefaultAsync(z => z.Id == workOrderId);
if (wo == null) return 0m;
decimal GrandTotal = 0m;
//update pricing
foreach (PMItem wi in wo.Items)
{
foreach (PMItemExpense o in wi.Expenses)
await ExpensePopulateVizFields(o, true);
foreach (PMItemLabor o in wi.Labors)
await LaborPopulateVizFields(o, true);
foreach (PMItemLoan o in wi.Loans)
await LoanPopulateVizFields(o, null, true);
foreach (PMItemPart o in wi.Parts)
await PartPopulateVizFields(o, true);
foreach (PMItemTravel o in wi.Travels)
await TravelPopulateVizFields(o, true);
foreach (PMItemOutsideService o in wi.OutsideServices)
await OutsideServicePopulateVizFields(o, true);
}
foreach (PMItem wi in wo.Items)
{
foreach (PMItemExpense o in wi.Expenses)
GrandTotal += o.LineTotalViz;
foreach (PMItemLabor o in wi.Labors)
GrandTotal += o.LineTotalViz;
foreach (PMItemLoan o in wi.Loans)
GrandTotal += o.LineTotalViz;
foreach (PMItemPart o in wi.Parts)
GrandTotal += o.LineTotalViz;
foreach (PMItemTravel o in wi.Travels)
GrandTotal += o.LineTotalViz;
foreach (PMItemOutsideService o in wi.OutsideServices)
GrandTotal += o.LineTotalViz;
}
return GrandTotal;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//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) 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))
{
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"));
}
/*
todo: quote status list first, it's a table of created items, keep properties from v7 but add the following properties:
SelectRoles - who can select the status (still shows if they can't select but that's the current status, like active does)
This is best handled at the client. It prefetches all the status out of the normal picklist process, more like how other things are separately handled now without a picklist
client then knows if a status is available or not and can process to only present available ones
#### Server can use a biz rule to ensure that it can't be circumvented
UI defaults to any role
DeselectRoles - who can unset this status (important for process control)
UI defaults to any role
CompletedStatus bool - this is a final status indicating all work on the quote is completed, affects notification etc
UI defaults to false but when set to true auto sets lockworkorder to true (but user can just unset lockworkorder)
LockWorkorder - this status is considered read only and the quote is locked
Just a read only thing, can just change status to "unlock" it
to support states where no one should work on a wo for whatever reason but it's not necessarily completed
e.g. "Hold for inspection", "On hold" generally etc
*/
// //Name required
// if (string.IsNullOrWhiteSpace(proposedObj.Name))
// AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
// //If name is otherwise OK, check that name is unique
// if (!PropertyHasErrors("Name"))
// {
// //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
// if (await ct.PM.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id))
// {
// AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
// }
// }
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString());
if (FormCustomization != null)
{
//Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required
//validate users choices for required non custom fields
RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);
//validate custom fields
CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void 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
//these are examples copied from customer for when other objects are actually referencing them
// if (await ct.User.AnyAsync(m => m.CustomerId == inObj.Id))
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("User"));
// if (await ct.Unit.AnyAsync(m => m.CustomerId == inObj.Id))
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Unit"));
// if (await ct.CustomerServiceRequest.AnyAsync(m => m.CustomerId == inObj.Id))
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("CustomerServiceRequest"));
// if (await ct.PurchaseOrder.AnyAsync(m => m.DropShipToCustomerId == inObj.Id))
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("PurchaseOrder"));
}
//############### NOTIFICATION TODO
/*
todo: quote notifications remove #30 and #32 as redundant
WorkorderStatusChange = 4,//* Workorder object, any *change* of status including from no status (new) to a specific conditional status ID value
WorkorderStatusAge = 24,//* Workorder object Created / Updated, conditional on exact status selected IdValue, Tags conditional, advance notice can be set
//THESE TWO ARE REDUNDANT:
this is actually workorderstatuschange because can just pick any status under workorderstatuschange to be notified about
WorkorderCompleted = 30, //*travel work order is set to any status that is flagged as a "Completed" type of status. Customer & User
//This one could be accomplished with WorkorderStatusAge, just pick a Completed status and set a time frame and wala!
WorkorderCompletedFollowUp = 32, //* travel quote closed status follow up again after this many TIMESPAN
todo: CHANGE WorkorderCompletedStatusOverdue = 15,//* Workorder object not set to a "Completed" flagged quote status type in selected time span from creation of quote
Change this to a new type that is based on so many days *without* being set to a particular status
but first check if tied to contract response time stuff, how that's handled
that's closeby date in v7 but isn't that deprecated now without a "close"?
maybe I do need the Completed status bool thing above
*/
////////////////////////////////////////////////////////////////////////////////////////////////
// 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)
{
//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));
#region unnecessary shit removed
//This is unnecessary because the re-ordering bit is only needed when the records are fetched in batches directly from the sql server as they
//return in db natural order and need to be put back into the same order as the ID List
//Here in the quote however, this code is fetching individually one at a time so they are always going to be in the correct order so this re-ordering is unnecessary
//I'm keeping this here for future reference when I ineveitably wonder what the hell is happening here :)
//order the results back into original
//IEnumerable<PM> orderedList = null;
//TODO: WHAT IS THIS BATCH RESULT ORDERING CODE REALLY DOING AND CAN IT BE REMOVED / CHANGED????
//isn't it alredy working in order? If not maybe simply reversed so reverse it again before querying above or...??
//todo: can't assume the grandchild item is index 0 anymore as we might have multiple of them if includedescendants is true
//so need to find index first then do this
// switch (dataListSelectedRequest.AType)
// {
// case AyaType.PM:
// orderedList = from id in batch join z in batchResults on id equals z.Id select z;
// break;
// case AyaType.PMItem:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Id select z;
// break;
// case AyaType.PMItemExpense:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Expenses[0].Id select z;
// break;
// case AyaType.PMItemLabor:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Labors[0].Id select z;
// break;
// case AyaType.PMItemLoan:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Loans[0].Id select z;
// break;
// case AyaType.PMItemPart:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Parts[0].Id select z;
// break;
// case AyaType.PMItemPartRequest:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].PartRequests[0].Id select z;
// break;
// case AyaType.PMItemScheduledUser:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].ScheduledUsers[0].Id select z;
// break;
// case AyaType.PMItemTask:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Tasks[0].Id select z;
// break;
// case AyaType.PMItemTravel:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Travels[0].Id select z;
// break;
// case AyaType.PMItemOutsideService:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].OutsideServices[0].Id select z;
// break;
// case AyaType.PMItemUnit:
// orderedList = from id in batch join z in batchResults on id equals z.Items[0].Units[0].Id select z;
// break;
// }
//foreach (PM w in orderedList)
#endregion unnecessary shit
foreach (PM w in batchResults)
{
var jo = JObject.FromObject(w);
//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);
}
}
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);
}
//popup Alert notes
//Customer notes first then others below
var custInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => new { AlertViz = x.PopUpNotes, x.TechNotes, CustomerViz = x.Name }).FirstOrDefaultAsync();
if (!string.IsNullOrWhiteSpace(custInfo.AlertViz))
{
o.AlertViz = $"{await Translate("Customer")} - {await Translate("AlertNotes")}\n{custInfo.AlertViz}\n\n";
}
if (!string.IsNullOrWhiteSpace(custInfo.TechNotes))
{
o.CustomerTechNotesViz = $"{await Translate("CustomerTechNotes")}\n{custInfo.TechNotes}\n\n";
}
o.CustomerViz = custInfo.CustomerViz;
if (o.ProjectId != null)
o.ProjectViz = await ct.Project.AsNoTracking().Where(x => x.Id == o.ProjectId).Select(x => x.Name).FirstOrDefaultAsync();
if (o.ContractId != null)
{
var contractVizFields = await ct.Contract.AsNoTracking().Where(x => x.Id == o.ContractId).Select(x => new { Name = x.Name, AlertNotes = x.AlertNotes }).FirstOrDefaultAsync();
o.ContractViz = contractVizFields.Name;
if (!string.IsNullOrWhiteSpace(contractVizFields.AlertNotes))
{
o.AlertViz += $"{await Translate("Contract")}\n{contractVizFields.AlertNotes}\n\n";
}
}
else
o.ContractViz = "-";
}
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest)
{
//for now just re-use the report data code
//this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
return await GetReportData(dataListSelectedRequest);
}
// public async Task<List<string>> ImportData(JArray ja)
// {
// List<string> ImportResult = new List<string>();
// string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}";
// var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
// foreach (JObject j in ja)
// {
// var w = j.ToObject<PM>(jsset);
// if (j["CustomFields"] != null)
// w.CustomFields = j["CustomFields"].ToString();
// w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary
// var res = await PMCreateAsync(w);
// if (res == null)
// {
// ImportResult.Add($"* {w.Serial} - {this.GetErrorsAsString()}");
// this.ClearErrors();
// }
// else
// {
// ImportResult.Add($"{w.Serial} - ok");
// }
// }
// return ImportResult;
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
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.Widget.Select(z => z.Id).ToListAsync();
bool SaveIt = false;
foreach (long id in idList)
{
try
{
SaveIt = false;
ClearErrors();
ICoreBizObjectModel o = null;
//save a fetch if it's a delete
if (job.SubType != JobSubType.Delete)
o = await 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) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
PM oProposed = (PM)proposedObj;
proposedObj.Name = oProposed.Serial.ToString();
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);//Note: will properly handle all delete events and event removal if deleted
//SPECIFIC EVENTS FOR THIS OBJECT
// PM oCurrent = null;
// bool SameTags = true;
// if (currentObj != null)
// {
// oCurrent = (PM)currentObj;
// SameTags = NotifyEventHelper.TwoObjectsHaveSameTags(proposedObj.Tags, currentObj.Tags);
// }
}//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 ItemHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await ItemPopulateVizFields(newObject, false);
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 ItemHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await ItemPopulateVizFields(putObject, false);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> ItemDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await ct.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);
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
private async Task ItemSearchIndexAsync(PMItem obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.PMItem);
SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> ItemGetSearchResultSummary(long id)
{
var obj = await ct.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.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task ItemPopulateVizFields(PMItem o, bool populateForReporting)
{
// if (o.PMOverseerId != null)
// o.PMOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.PMOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
foreach (var v in o.Expenses)
await ExpensePopulateVizFields(v);
foreach (var v in o.Labors)
await LaborPopulateVizFields(v);
foreach (var v in o.Loans)
await LoanPopulateVizFields(v);
foreach (var v in o.OutsideServices)
await OutsideServicePopulateVizFields(v);
foreach (var v in o.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 quote id
if (proposedObj.PMId == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "PMId");
else if (!await PMExistsAsync(proposedObj.PMId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "PMId");
}
//summary is required now, this is a change from v7
//I did this because it is required in terms of hiding on the form so it also
//is required to have a value. This is really because the form field customization I took away the hideable field
//maybe I should add that feature back?
if (proposedObj.PMId == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "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))//negative quantities are not allowed
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) 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
oProposed.Name = WorkorderInfo.Serial.ToString();
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
//## DELETED EVENTS
//any event added below needs to be removed, so
//just blanket remove any event for this object of eventtype that would be added below here
//do it regardless any time there's an update and then
//let this code below handle the refreshing addition that could have changes
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.ContractExpiring);
//## CREATED / MODIFIED EVENTS
if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
{
//todo: fix etc, tons of shit here incoming
}
}//end of process notifications
#endregion work order item level
/*
███████╗██╗ ██╗██████╗ ███████╗███╗ ██╗███████╗███████╗███████╗
██╔════╝╚██╗██╔╝██╔══██╗██╔════╝████╗ ██║██╔════╝██╔════╝██╔════╝
█████╗ ╚███╔╝ ██████╔╝█████╗ ██╔██╗ ██║███████╗█████╗ ███████╗
██╔══╝ ██╔██╗ ██╔═══╝ ██╔══╝ ██║╚██╗██║╚════██║██╔══╝ ╚════██║
███████╗██╔╝ ██╗██║ ███████╗██║ ╚████║███████║███████╗███████║
╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚══════╝╚══════╝
*/
#region 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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await ExpensePopulateVizFields(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 ExpenseHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await ExpensePopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> ExpenseDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await ExpenseGetAsync(id, false);
ExpenseValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.PMItemExpense.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.PMItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task ExpenseSearchIndexAsync(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)
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
}
TaxCode Tax = null;
if (o.ChargeTaxCodeId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.ChargeTaxCodeId);
if (Tax != null)
o.TaxCodeViz = Tax.Name;
//Calculate totals and taxes
if (o.ChargeToCustomer)
{
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.ChargeAmount * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.ChargeAmount + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.ChargeAmount * (Tax.TaxBPct / 100);
}
}
o.LineTotalViz = o.ChargeAmount + o.TaxAViz + o.TaxBViz;
}
else
{
o.LineTotalViz = o.ChargeAmount + o.TaxPaid;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ExpenseValidateAsync(PMItemExpense proposedObj, PMItemExpense currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) 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) 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 LaborBizActionsAsync(AyaEvent.Created, newObject, null, null);
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await LaborHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await LaborPopulateVizFields(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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await LaborHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await LaborPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> LaborDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await LaborGetAsync(id, false);
LaborValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.PMItemLabor.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.PMItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
// await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await LaborHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task LaborSearchIndexAsync(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)
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
}
ServiceRate Rate = null;
if (o.ServiceRateId != null)
{
Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.ServiceRateId);
o.ServiceRateViz = Rate.Name;
}
TaxCode Tax = null;
if (o.TaxCodeSaleId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId);
if (Tax != null)
o.TaxCodeViz = Tax.Name;
o.PriceViz = 0;
if (Rate != null)
{
o.CostViz = Rate.Cost;
o.ListPriceViz = Rate.Charge;
o.UnitOfMeasureViz = Rate.Unit;
o.PriceViz = Rate.Charge;//default price used if not manual or contract override
}
//manual price overrides anything
if (o.PriceOverride != null)
o.PriceViz = (decimal)o.PriceOverride;
else
{
//not manual so could potentially have a contract adjustment
var c = await 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 = o.CostViz + (o.CostViz * pct);
else if (cot == ContractOverrideType.PriceDiscount)
o.PriceViz = o.ListPriceViz - (o.ListPriceViz * pct);
}
}
}
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz * o.ServiceRateQuantity;
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
//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) 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
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
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) 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
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item LABOR level
/*
██╗ ██████╗ █████╗ ███╗ ██╗
██║ ██╔═══██╗██╔══██╗████╗ ██║
██║ ██║ ██║███████║██╔██╗ ██║
██║ ██║ ██║██╔══██║██║╚██╗██║
███████╗╚██████╔╝██║ ██║██║ ╚████║
*/
#region 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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await LoanHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await LoanPopulateVizFields(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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await LoanHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await LoanPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> LoanDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await LoanGetAsync(id, false);
LoanValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.PMItemLoan.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.PMItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await LoanHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task LoanSearchIndexAsync(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, List<NameIdItem> loanUnitRateEnumList = null, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (loanUnitRateEnumList == null)
loanUnitRateEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList(
StringUtil.TrimTypeName(typeof(LoanUnitRateUnit).ToString()),
UserTranslationId,
CurrentUserRoles);
o.UnitOfMeasureViz = loanUnitRateEnumList.Where(x => x.Id == (long)o.Rate).Select(x => x.Name).First();
}
LoanUnit loanUnit = await ct.LoanUnit.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.LoanUnitId);
o.LoanUnitViz = loanUnit.Name;
TaxCode Tax = null;
if (o.TaxCodeId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeId);
if (Tax != null)
o.TaxCodeViz = Tax.Name;
//manual price overrides anything
o.PriceViz = o.ListPrice;
if (o.PriceOverride != null)
o.PriceViz = (decimal)o.PriceOverride;
//Currently not contract discounted so no further calcs need apply to priceViz
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz * o.Quantity;
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
//RESTRICTED COST FIELD??
if (!UserCanViewLoanerCosts)
o.Cost = 0;//cost already used in calcs and will not be updated on any update operation so this ensures the cost isn't sent over the wire
}
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task LoanBizActionsAsync(AyaEvent ayaEvent, 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) 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
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
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) 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);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await OutsideServicePopulateVizFields(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;
}
// dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await OutsideServiceValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await OutsideServiceExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await OutsideServiceSearchIndexAsync(putObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await OutsideServicePopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> OutsideServiceDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await OutsideServiceGetAsync(id, false);
OutsideServiceValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.PMItemOutsideService.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.PMItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task OutsideServiceSearchIndexAsync(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)
o.UnitViz = await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync();
if (o.VendorSentToId != null)
o.VendorSentToViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentToId).Select(x => x.Name).FirstOrDefaultAsync();
if (o.VendorSentViaId != null)
o.VendorSentViaViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentViaId).Select(x => x.Name).FirstOrDefaultAsync();
}
TaxCode Tax = null;
if (o.TaxCodeId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeId);
if (Tax != null)
o.TaxCodeViz = Tax.Name;
o.CostViz = o.ShippingCost + o.RepairCost;
o.PriceViz = o.ShippingPrice + o.RepairPrice;
//Currently not contract discounted so no further calcs need apply to priceViz
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz;//just for standardization, no quantity so is redundant but reporting easier if all the same
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task OutsideServiceValidateAsync(PMItemOutsideService proposedObj, PMItemOutsideService currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) 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
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
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) 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
//## CREATED
if (ayaEvent == AyaEvent.Created)
{
//OutsideServiceOverdue
if (oProposed.ETADate != null)
{
//Conditions: tags + time delayed eta value
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceOverdue).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.OutsideServiceOverdue,
UserId = sub.UserId,
AyaType = AyaType.PMItemOutsideService,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
EventDate = (DateTime)oProposed.ETADate,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//OutsideServiceOverdue
//OutsideServiceReceived (here because it's possible a outside service is entered new with both an eta and received date if entered after the fact)
if (oProposed.ReturnDate != null)
{
//Clear overdue ones as it's now received
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue);
//Conditions: tags
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceReceived).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.OutsideServiceReceived,
UserId = sub.UserId,
AyaType = AyaType.PMItemOutsideService,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//OutsideServiceReceived
}
//## MODIFIED
if (ayaEvent == AyaEvent.Modified)
{
PMItemOutsideService oCurrent = (PMItemOutsideService)currentObj;
//OutsideServiceOverdue
//if modified then remove any potential prior ones in case irrelevant
if (oProposed.ETADate != oCurrent.ETADate)
{
//eta changed, so first of all remove any prior ones
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue);
//now can go ahead and add back again as appropriate
if (oProposed.ETADate != null)
{
//Conditions: tags + time delayed eta value
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceOverdue).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.OutsideServiceOverdue,
UserId = sub.UserId,
AyaType = AyaType.PMItemOutsideService,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
EventDate = (DateTime)oProposed.ETADate,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//OutsideServiceOverdue
}
//OutsideServiceReceived
if (oProposed.ReturnDate != oCurrent.ReturnDate && oProposed.ReturnDate != null)//note that this is an instant notification type so no need to clear older ones like above which is time delayed
{
//Clear overdue ones as it's now received
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue);
//Conditions: tags
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceReceived).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.OutsideServiceReceived,
UserId = sub.UserId,
AyaType = AyaType.PMItemOutsideService,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//OutsideServiceReceived
}
}//end of process notifications
#endregion work order item OUTSIDE SERVICE level
/*
██████╗ █████╗ ██████╗ ████████╗███████╗
██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝
██████╔╝███████║██████╔╝ ██║ ███████╗
██╔═══╝ ██╔══██║██╔══██╗ ██║ ╚════██║
██║ ██║ ██║██║ ██║ ██║ ███████║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝
*/
#region 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);
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await PartPopulateVizFields(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;
}
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
//dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await PartValidateAsync(putObject, dbObject);
if (HasErrors) return null;
await PartBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
if (HasErrors)
{
await transaction.RollbackAsync();
return null;
}
}
catch (DbUpdateConcurrencyException)
{
if (!await PartExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await PartSearchIndexAsync(putObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
await PartHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await transaction.CommitAsync();
await PartPopulateVizFields(putObject);
return putObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> PartDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
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, "woitem:" + dbObject.PMItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await PartHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task PartSearchIndexAsync(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)
o.PartWarehouseViz = await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync();
}
Part part = null;
if (o.PartId != 0)
part = await ct.Part.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.PartId);
else
return;//this should never happen but this is insurance in case it does
o.PartViz = part.PartNumber;
o.PartNameViz = part.Name;
o.UpcViz = part.UPC;
TaxCode Tax = null;
if (o.TaxPartSaleId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxPartSaleId);
if (Tax != null)
o.TaxCodeViz = Tax.Name;
o.PriceViz = 0;
if (part != null)
{
//COST & PRICE NOT SET HERE, SET IN BIZACTIONS SNAPSHOTTED
// o.CostViz = part.Cost;
// o.ListPriceViz = part.Retail;
o.UnitOfMeasureViz = part.UnitOfMeasure;
o.PriceViz = o.ListPrice;//default price used if not manual or contract override
}
//manual price overrides anything
if (o.PriceOverride != null)
o.PriceViz = (decimal)o.PriceOverride;
else
{
//not manual so could potentially have a contract adjustment
var c = await 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 = o.Cost + (o.Cost * pct);
else if (cot == ContractOverrideType.PriceDiscount)
o.PriceViz = o.ListPrice - (o.ListPrice * pct);
}
}
}
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz * o.Quantity;
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
//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) 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
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
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) 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
{
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.PMItemScheduledUser.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
//await ScheduledUserSearchIndexAsync(newObject, true);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await ScheduledUserPopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<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;
}
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await ScheduledUserValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await ScheduledUserExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
// await ScheduledUserSearchIndexAsync(dbObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await ScheduledUserPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> ScheduledUserDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await ScheduledUserGetAsync(id, false);
ScheduledUserValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.PMItemScheduledUser.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.PMItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task ScheduledUserPopulateVizFields(PMItemScheduledUser o)
{
if (o.UserId != null)
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
if (o.ServiceRateId != null)
o.ServiceRateViz = await ct.ServiceRate.AsNoTracking().Where(x => x.Id == o.ServiceRateId).Select(x => x.Name).FirstOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ScheduledUserValidateAsync(PMItemScheduledUser proposedObj, PMItemScheduledUser currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) 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");
//Scheduling conflict?
if (!AyaNova.Util.ServerGlobalBizSettings.Cache.AllowScheduleConflicts
&& proposedObj.UserId != null
&& proposedObj.StartDate != null
&& proposedObj.StopDate != null
&& (isNew
|| (proposedObj.StartDate != currentObj.StartDate)
|| (proposedObj.StopDate != currentObj.StopDate)
|| (proposedObj.UserId != currentObj.UserId)
))
{
if (await ct.PMItemScheduledUser.AnyAsync(x => x.Id != proposedObj.Id
&& x.UserId == proposedObj.UserId
&& x.StartDate <= proposedObj.StopDate
&& proposedObj.StartDate <= x.StopDate))
{
AddError(ApiErrorCode.VALIDATION_FAILED, "StartDate", await Translate("ScheduleConflict"));
AddError(ApiErrorCode.VALIDATION_FAILED, "StopDate", await Translate("ScheduleConflict"));
}
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.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
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
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) 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
//## CREATED / UPDATED - ScheduledOnWorkorder event
//Note: scheduled on quote is immediate so same process regardless if modified or updated
//because modified changes nearly all affect user so decision is just send it no matter what as any difference is enough to send
if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
{
//this block is entirely about //ScheduledOnWorkorder event
if (oProposed.UserId != null)
{
//Conditions: userid match and tags
//delivery is immediate so no need to remove old ones of this kind
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorder && z.UserId == oProposed.UserId).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ScheduledOnWorkorder,
UserId = sub.UserId,
AyaType = AyaType.PMItemScheduledUser,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//ScheduledOnWorkorder
}
//---------------------------------------------------------------------------------------------------------------------------------------------
//## CREATED
if (ayaEvent == AyaEvent.Created)
{
//ScheduledOnWorkorderImminent
if (oProposed.UserId != null && oProposed.StartDate != null)
{
//Conditions: userid match and tags + time delayed age value
//delivery is delayed so need to remove old ones of this kind on update
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorderImminent && z.UserId == oProposed.UserId).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ScheduledOnWorkorderImminent,
UserId = sub.UserId,
AyaType = AyaType.PMItemScheduledUser,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
EventDate = (DateTime)oProposed.StartDate,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//ScheduledOnWorkorderImminent
}
//## MODIFIED
if (ayaEvent == AyaEvent.Modified)
{
//ScheduledOnWorkorderImminent
//Always clear any old ones for this object as they are all irrelevant the moment changed:
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.ScheduledOnWorkorderImminent);
if (oProposed.UserId != null && oProposed.StartDate != null)
{
//Conditions: userid match and tags + time delayed age value
//delivery is delayed so need to remove old ones of this kind on update
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorderImminent && z.UserId == oProposed.UserId).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ScheduledOnWorkorderImminent,
UserId = sub.UserId,
AyaType = AyaType.PMItemScheduledUser,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
EventDate = (DateTime)oProposed.StartDate,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//ScheduledOnWorkorderImminent
}
}//end of process notifications
#endregion work order item SCHEDULED USER level
/*
████████╗ █████╗ ███████╗██╗ ██╗
╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
██║ ███████║███████╗█████╔╝
██║ ██╔══██║╚════██║██╔═██╗
██║ ██║ ██║███████║██║ ██╗
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
*/
#region 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
{
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await TaskHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await TaskPopulateVizFields(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;
}
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
//dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await TaskValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await TaskExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await TaskSearchIndexAsync(dbObject, false);
// await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
await TaskHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await TaskPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> TaskDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await TaskGetAsync(id, false);
TaskValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.PMItemTask.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.PMItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await TaskHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task TaskSearchIndexAsync(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, List<NameIdItem> taskCompletionTypeEnumList = null)
{
if (o.CompletedByUserId != null)
o.CompletedByUserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.CompletedByUserId).Select(x => x.Name).FirstOrDefaultAsync();
if (taskCompletionTypeEnumList == null)
taskCompletionTypeEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList(
StringUtil.TrimTypeName(typeof(WorkorderItemTaskCompletionType).ToString()),
UserTranslationId,
CurrentUserRoles);
o.StatusViz = taskCompletionTypeEnumList.Where(x => x.Id == (long)o.Status).Select(x => x.Name).First();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task TaskValidateAsync(PMItemTask proposedObj, PMItemTask currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) 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
//validate custom fields
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
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) 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 TravelHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await TravelPopulateVizFields(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 TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
await TravelHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await TravelPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> TravelDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await TravelGetAsync(id, false);
TravelValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.PMItemTravel.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.PMItemId.ToString(), ct);
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
// await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await TravelHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task TravelSearchIndexAsync(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)
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
}
TravelRate Rate = null;
if (o.TravelRateId != null)
{
Rate = await ct.TravelRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.TravelRateId);
o.TravelRateViz = Rate.Name;
}
TaxCode Tax = null;
if (o.TaxCodeSaleId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId);
if (Tax != null)
o.TaxCodeViz = Tax.Name;
o.PriceViz = 0;
if (Rate != null)
{
o.CostViz = Rate.Cost;
o.ListPriceViz = Rate.Charge;
o.UnitOfMeasureViz = Rate.Unit;
o.PriceViz = Rate.Charge;//default price used if not manual or contract override
}
//manual price overrides anything
if (o.PriceOverride != null)
o.PriceViz = (decimal)o.PriceOverride;
else
{
//not manual so could potentially have a contract adjustment
var c = await 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 = o.CostViz + (o.CostViz * pct);
else if (cot == ContractOverrideType.PriceDiscount)
o.PriceViz = o.ListPriceViz - (o.ListPriceViz * pct);
}
}
}
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz * o.TravelRateQuantity;
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
//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) 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
//validate custom fields
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
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) 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)
{
//todo: contract stuff and validation of no other existing contracted unit
//assumptions: this create only gets called if there is an existing woheader saved in all cases
await UnitValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
//await UnitBizActionsAsync(AyaEvent.Created, newObject, null, null);
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 UnitHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await UnitPopulateVizFields(newObject, false);
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 UnitHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await UnitPopulateVizFields(putObject, false);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> UnitDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await UnitGetAsync(id, false);
UnitValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.PMItemUnit.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.PMItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await UnitHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task UnitSearchIndexAsync(PMItemUnit obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> UnitGetSearchResultSummary(long id)
{
var obj = await UnitGetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
return SearchParams;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// //BIZ ACTIONS
// //
// //
// private async Task UnitBizActionsAsync(AyaEvent ayaEvent, PMItemUnit newObj, PMItemUnit 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;
// if (newOrChangedActiveUnitContract != null && (ayaEvent == AyaEvent.Modified || ayaEvent == AyaEvent.Created))//note: keeping this qualification defensively in case more biz actions added later
// {
// //set contract if applicable
// //Note: validation has already set neworchangeactiveunitcontract and only sets it if it's applicable
// //so in here we just need to apply that contract to the header
// //I've decided that it will attempt to set the header here now rather than after the unit has set
// //as it's more important to have the unit record be saved than to
// // //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;
// // }
// }
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task UnitPopulateVizFields(PMItemUnit o, bool populateForReporting)
{
var unitInfo = await ct.Unit.AsNoTracking()
.Where(x => x.Id == o.UnitId)
.Select(x => new { x.Serial, x.Description, x.UnitModelId, x.Address, x.City, x.Region, x.Country, x.Latitude, x.Longitude })
.FirstOrDefaultAsync();
o.UnitViz = unitInfo.Serial;
o.UnitDescriptionViz = unitInfo.Description;
if (populateForReporting)
{
o.AddressViz = unitInfo.Address;
o.CityViz = unitInfo.City;
o.RegionViz = unitInfo.Region;
o.CountryViz = unitInfo.Country;
o.LatitudeViz = unitInfo.Latitude;
o.LongitudeViz = unitInfo.Longitude;
}
if (unitInfo.UnitModelId != null)
{
var unitModelInfo = await ct.UnitModel.AsNoTracking().Where(x => x.Id == unitInfo.UnitModelId).Select(x => new { x.Name, x.VendorId, x.Number }).FirstOrDefaultAsync();
o.UnitModelNameViz = unitModelInfo.Name;
o.UnitModelModelNumberViz = unitModelInfo.Number;
if (unitModelInfo.VendorId != null)
o.UnitModelVendorViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == unitModelInfo.VendorId).Select(x => x.Name).FirstOrDefaultAsync();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task UnitValidateAsync(PMItemUnit proposedObj, PMItemUnit currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
// - A work order *MUST* have only one Unit with a Contract, if there is already a unit with a contract on this quote then a new one cannot be added and it will reject with a validation error
// a unit record is saved only *after* there is already a header (by api users and our client software) so can easily check and set here
//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) 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);
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 GenerateAsync(AyContext ct, ILogger log)
{
// #if (DEBUG)
// log.LogInformation("PMBiz - Generating");
// #endif
//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($"PMBiz - Found {l.Count} ready to generate PM items");
#endif
//process those items
foreach (long pmid in l)
{
#if (DEBUG)
log.LogInformation($"PMBiz - 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)
{
log.LogError($"PMBiz - PM was not fetchable when attempting to process PM id: {pmid}, deleted during processing?");
//todo: NOTIFY FAIL
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.LogTrace($"PMBiz:: 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($"PMBiz::GenerateAsync updated PM after successful generation {p.Serial}");
#endif
}
catch (Exception ex)
{
log.LogError(ex, $"PMBiz::GenerateAsync error updating PM after generation {p.Serial}");
//todo: NOTIFY FAIL
continue;
}
}
}
catch (Exception ex)
{
log.LogError(ex, $"PMBiz::GenerateAsync error generating Work order from PM {p.Serial}");
//todo: NOTIFY FAIL
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)
{
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);
foreach (PMItem pmi in p.Items)
{
var woi = new WorkOrderItem();
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;
foreach (PMItemUnit pmiunit in pmi.Units)
{
var woiunit = new WorkOrderItemUnit();
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);
}
foreach (PMItemScheduledUser pmsu in pmi.ScheduledUsers)
{
var wois = new WorkOrderItemScheduledUser();
wois.ServiceRateId = pmsu.ServiceRateId;
wois.StartDate = pmsu.StartDate;//DATE ADJUST
wois.StopDate = pmsu.StopDate;//DATE ADJUST
wois.Tags = pmsu.Tags;
wois.UserId = pmsu.UserId;
woi.ScheduledUsers.Add(wois);
}
foreach (PMItemPart pmp in pmi.Parts)
{
var wip = new WorkOrderItemPart();
wip.Description = pmp.Description;
wip.PartId = pmp.PartId;
wip.PartWarehouseId = pmp.PartWarehouseId;
wip.PriceOverride = pmp.PriceOverride;
wip.Quantity = pmp.Quantity;
//wip.Serials=pmp.Serials;
wip.Tags = pmp.Tags;
wip.TaxPartSaleId = pmp.TaxPartSaleId;
woi.Parts.Add(wip);
}
foreach (PMItemLoan pml in pmi.Loans)
{
var wil = new WorkOrderItemLoan();
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);
}
foreach (PMItemLabor pmlab in pmi.Labors)
{
var wilab = new WorkOrderItemLabor();
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);
}
foreach (PMItemTravel pmtrav in pmi.Travels)
{
var witrav = new WorkOrderItemTravel();
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);
}
foreach (PMItemTask pmt in pmi.Tasks)
{
var wit = new WorkOrderItemTask();
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);
}
foreach (PMItemExpense pme in pmi.Expenses)
{
var wie = new WorkOrderItemExpense();
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);
}
foreach (PMItemOutsideService pmo in pmi.OutsideServices)
{
var wio = new WorkOrderItemOutsideService();
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);
}
wo.Items.Add(woi);
}
WorkOrderBiz biz = WorkOrderBiz.GetBiz(ct);
var NewObject = await biz.WorkOrderCreateAsync(wo, false);
if (NewObject == null)
{
var err = $"PMBiz::NewServiceWorkOrderFromPMAsync error creating from PM {p.Serial}\r\n{biz.GetErrorsAsString()}";
log.LogError(err);
//todo: NOTIFY FAIL
return false;
}
#if (DEBUG)
log.LogInformation($"PMBiz::NewServiceWorkOrderFromPMAsync created new workorder {NewObject.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
{
throw;
}
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 gen service wo from pm
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons