Files
raven/server/AyaNova/biz/PartInventoryBiz.cs
2022-03-29 17:43:38 +00:00

437 lines
20 KiB
C#

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace AyaNova.Biz
{
internal class PartInventoryBiz : BizObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, IImportAbleObject
{
internal PartInventoryBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = AyaType.PartInventory;
}
internal static PartInventoryBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new PartInventoryBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new PartInventoryBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.PartInventory.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE (public facing adjustment version)
//
internal async Task<PartInventory> CreateAsync(dtPartInventory newDtObject)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
//get the last record if exists (will not if opening balance)
var LastEntry = await ct.PartInventory.OrderByDescending(m => m.EntryDate).FirstOrDefaultAsync(m => m.PartId == newDtObject.PartId && m.PartWarehouseId == newDtObject.PartWarehouseId);
PartInventory newObject = new PartInventory();
newObject.Description = newDtObject.Description;
newObject.EntryDate = DateTime.UtcNow;
newObject.PartId = newDtObject.PartId;
newObject.PartWarehouseId = newDtObject.PartWarehouseId;
newObject.SourceId = null;
newObject.SourceType = null;
newObject.Quantity = newDtObject.Quantity;
if (LastEntry != null)
{
newObject.LastEntryDate = LastEntry.EntryDate;
newObject.LastBalance = LastEntry.Balance;
newObject.Balance = LastEntry.Balance + newObject.Quantity;
}
else
{
newObject.Balance = newObject.Quantity;
}
await ValidateAsync(newObject);
if (HasErrors)
return null;
else
{
await ct.PartInventory.AddAsync(newObject);
await ct.SaveChangesAsync();
await transaction.CommitAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
await SearchIndexAsync(newObject, true);
return newObject;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE (internal version only called from within a transaction)
//
internal async Task<PartInventory> CreateAsync(dtInternalPartInventory newDtObject)
{
//get the last record if exists (will not if opening balance)
var LastEntry = await ct.PartInventory.OrderByDescending(m => m.EntryDate).FirstOrDefaultAsync(m => m.PartId == newDtObject.PartId && m.PartWarehouseId == newDtObject.PartWarehouseId);
PartInventory newObject = new PartInventory();
newObject.Description = newDtObject.Description;
newObject.PartId = newDtObject.PartId;
newObject.PartWarehouseId = newDtObject.PartWarehouseId;
newObject.SourceId = newDtObject.SourceId;
newObject.SourceType = newDtObject.SourceType;
newObject.Quantity = newDtObject.Quantity;
if (LastEntry != null)
{
newObject.LastEntryDate = LastEntry.EntryDate;
newObject.LastBalance = LastEntry.Balance;
newObject.Balance = LastEntry.Balance + newObject.Quantity;
}
else
{
newObject.Balance = newObject.Quantity;
}
newObject.EntryDate = DateTime.UtcNow;//validate is saying this is older than lastentrydate in a huge test migration on a crappy vm which is weird so moved here to last millisecond before validation
await ValidateAsync(newObject);
if (HasErrors)
return null;
else
{
await ct.PartInventory.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
await SearchIndexAsync(newObject, true);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<PartInventory> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.PartInventory.AsNoTracking().SingleOrDefaultAsync(m => m.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task SearchIndexAsync(PartInventory obj, bool isNew)
{
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType);
DigestSearchText(obj, SearchParams);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, AyaType specificType)
{
var obj = await GetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
}
public void DigestSearchText(PartInventory obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Description);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(PartInventory proposedObj)
{
//NOTE: Many of these errors wouldn't happen in the official ayanova UI (api developer error only) so are not translated
//Description required (only affordance to open the record)
if (string.IsNullOrWhiteSpace(proposedObj.Description))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Description");
if (!await BizObjectExistsInDatabase.ExistsAsync(AyaType.Part, proposedObj.PartId, ct))
{
AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"PartInventory Part specified doesn't exist [id:{proposedObj.PartId}]");
return;
}
if (!await BizObjectExistsInDatabase.ExistsAsync(AyaType.PartWarehouse, proposedObj.PartWarehouseId, ct))
{
AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"PartInventory PartWarehouse specified doesn't exist [id:{proposedObj.PartWarehouseId}]");
return;
}
//Source id and type must either both be null or neither be null
if ((proposedObj.SourceId == null && proposedObj.SourceType != null) || (proposedObj.SourceId != null && proposedObj.SourceType == null))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "generalerror", "Source type and id must be specified or both null for a manual adjustment");
return;
}
if (proposedObj.SourceType != null && proposedObj.SourceId != null && !await BizObjectExistsInDatabase.ExistsAsync((AyaType)proposedObj.SourceType, (long)proposedObj.SourceId, ct))
{
AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"PartInventory source object causing inventory change specified doesn't exist [type:{proposedObj.SourceType}, id:{proposedObj.SourceId}]");
return;
}
//New entry must have *something* that would affect quantities
if (proposedObj.Quantity == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Quantity");
return;
}
//values must add up
if (proposedObj.Balance != (proposedObj.Quantity + (proposedObj.LastBalance ?? 0)))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Balance", "Balance incorrect (LastBalance + Quantity not equal to Balance)");
return;
}
//Balance must not be negative
//return the current balance
if (proposedObj.Balance < 0)
{
string currentBalance = "0";
if (proposedObj.LastBalance != null)
currentBalance = ((decimal)proposedObj.LastBalance).ToString("G29");
AddError(ApiErrorCode.INSUFFICIENT_INVENTORY, "Balance", $"{currentBalance}");
return;
}
//Last entry date is newer than current entry date?
if (proposedObj.LastEntryDate != null && proposedObj.LastEntryDate > proposedObj.EntryDate)
{
AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "generalerror", $"LastEntryDate ({proposedObj.LastEntryDate.ToString()}) is newer than EntryDate ({proposedObj.EntryDate.ToString()})");
return;
}
//valid previous columns?
//either they're all null or none of them are null
if (!((proposedObj.LastEntryDate == null
&& proposedObj.LastBalance == null) ||
(proposedObj.LastEntryDate != null
&& proposedObj.LastBalance != null)
))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "generalerror", "Last* entries must be all empty (opening entry) or none of them empty (any later entry)");
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORTING
//
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
var idList = dataListSelectedRequest.SelectedRowIds;
JArray ReportData = new JArray();
using (var command = ct.Database.GetDbConnection().CreateCommand())
{
ct.Database.OpenConnection();
while (idList.Any())
{
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
//query for this batch, comes back in db natural order unfortunately
var batchResults = await ct.PartInventory.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync();
//order the results back into original
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
batchResults = null;
foreach (PartInventory w in orderedList)
{
if (!ReportRenderManager.KeepGoing(jobId)) return null;
await PopulateVizFields(w, command);
var jo = JObject.FromObject(w);
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
ReportData.Add(jo);
}
orderedList = null;
}
}
vc.Clear();
return ReportData;
}
private VizCache vc = new VizCache();
//populate viz fields from provided object
private async Task PopulateVizFields(PartInventory o, System.Data.Common.DbCommand cmd)
{
if (ayaTypesEnumList == null)
ayaTypesEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList(
StringUtil.TrimTypeName(typeof(AyaType).ToString()),
UserTranslationId,
CurrentUserRoles);
if (!vc.Has("partname", o.PartId))
{
var partInfo = await ct.Part.AsNoTracking().Where(x => x.Id == o.PartId).Select(x => new { x.Description, x.Name, x.UPC }).FirstOrDefaultAsync();
vc.Add(partInfo.Name, "partname", o.PartId);
vc.Add(partInfo.Description, "partdescription", o.PartId);
vc.Add(partInfo.UPC, "partupc", o.PartId);
}
o.PartDescriptionViz = vc.Get("partdescription", o.PartId);
o.PartNameViz = vc.Get("partname", o.PartId);
o.PartUpcViz = vc.Get("partupc", o.PartId);
if (o.PartWarehouseId != 0)
{
if (!vc.Has("partwarehouse", o.PartWarehouseId))
vc.Add(await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync(), "partwarehouse", o.PartWarehouseId);
o.PartWarehouseViz = vc.Get("partwarehouse", o.PartWarehouseId);
}
if (o.SourceType != null)
o.SourceTypeViz = ayaTypesEnumList.Where(x => x.Id == (long)o.SourceType).Select(x => x.Name).First();
if (o.SourceType != null && o.SourceId != null)
o.SourceViz = BizObjectNameFetcherDirect.Name((AyaType)o.SourceType, (long)o.SourceId, UserTranslationId, cmd);
if (o.SourceType != null && o.SourceId != null)
{
if (!vc.Has($"b{o.SourceType}{o.SourceId}"))
vc.Add(BizObjectNameFetcherDirect.Name((AyaType)o.SourceType, (long)o.SourceId, UserTranslationId, cmd), $"b{o.SourceType}{o.SourceId}");
o.SourceViz = vc.Get($"b{o.SourceType}{o.SourceId}");
}
}
private List<NameIdItem> ayaTypesEnumList = null;
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
//for now just re-use the report data code
//this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
return await GetReportData(dataListSelectedRequest, jobId);
}
public async Task<List<string>> ImportData(AyImportData importData)
{
List<string> ImportResult = new List<string>();
if (importData.DoUpdate)
{
ImportResult.Add($"❌ Inventory transactions can only be Imported, not Updated");
return ImportResult;
}
string ImportTag = ImportUtil.GetImportTag();
foreach (JObject j in importData.Data)
{
try
{
long ImportPartId = -1;//default meaning not included / don't set
if (j["PartViz"] != null)
{
if (!JsonUtil.JTokenIsNullOrEmpty(j["PartViz"]))
{
//a name was specified so attempt to find it
ImportPartId = await ct.Part.AsNoTracking().Where(z => z.Name == (string)j["PartViz"]).Select(x => x.Id).FirstOrDefaultAsync();
if (ImportPartId == 0)
AddError(ApiErrorCode.NOT_FOUND, "PartViz", $"'{(string)j["PartViz"]}'");
}
}
else
AddError(ApiErrorCode.VALIDATION_REQUIRED, "PartViz");
long ImportPartWarehouseId = -1;//default meaning not included / don't set
if (j["PartWarehouseViz"] != null)
{
if (!JsonUtil.JTokenIsNullOrEmpty(j["PartWarehouseViz"]))
{
//a name was specified so attempt to find it
ImportPartWarehouseId = await ct.PartWarehouse.AsNoTracking().Where(z => z.Name == (string)j["PartWarehouseViz"]).Select(x => x.Id).FirstOrDefaultAsync();
if (ImportPartWarehouseId == 0)
AddError(ApiErrorCode.NOT_FOUND, "PartWarehouseViz", $"'{(string)j["PartWarehouseViz"]}'");
}
}
else
AddError(ApiErrorCode.VALIDATION_REQUIRED, "PartWarehouseViz");
var ImportDescription = (string)j["Description"];
decimal ImportQuantity = 0;
if (j["Quantity"] != null)
{
ImportQuantity = (decimal)j["Quantity"];
}
else
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Quantity");
//import this record
if (string.IsNullOrWhiteSpace(ImportDescription))
ImportDescription = ImportTag;
var Target = new dtPartInventory() { Description = ImportDescription, PartId = ImportPartId, PartWarehouseId = ImportPartWarehouseId, Quantity = ImportQuantity };
//var Target = j.ToObject<dtPartInventory>();
var nameviz = $"{Target.Description} - part {(string)j["PartViz"]}, quantity: {(decimal)j["Quantity"]}";
var res = await CreateAsync(Target);
if (res == null)
{
ImportResult.Add($"❌ {nameviz}\r\n{this.GetErrorsAsString()}");
this.ClearErrors();
}
else
{
ImportResult.Add($"✔️ {nameviz}");
}
}
catch (Exception ex)
{
ImportResult.Add($"❌ Exception processing import\n record:{j.ToString()}\nError:{ex.Message}\nSource:{ex.Source}\nStack:{ex.StackTrace.ToString()}");
}
}
return ImportResult;
}
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons