437 lines
20 KiB
C#
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
|
|
|