Files
BusinessMgr-2019/SaBusMgrStandardDbas/Dbas/Mgr/SbmStockMgr_Dbas.cs
T

331 lines
14 KiB
C#

using Sarand.BusinessMgr.Net.Standard.Base.App;
using Sarand.BusinessMgr.Net.Standard.Base.Bus;
using Sarand.BusinessMgr.Net.Standard.Base.Core;
using Sarand.BusinessMgr.Net.Standard.Base.Mgr;
using Sarand.BusinessMgr.Net.Standard.Dbas.Data;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Dbas;
using Sarand.Net.Standard.Base.Utils;
using Sarand.Net.Standard.Dbas.Base;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Sarand.BusinessMgr.Net.Standard.Dbas.Mgr
{
//=======================================================================
internal partial class SbmStockMgr_Dbas : SbmStockMgr
{
//-------------------------------------------------------------------
// Data members.
public class Members
{
public SacsDatabase db = null;
public SbmDbDataItem.SbmDatabaseInfo dbInfo;
}
private readonly Members mbr = new Members();
//-------------------------------------------------------------------
// Construction.
public SbmStockMgr_Dbas(SbmInternals internals) :
base(internals)
{
Initialise();
}
public SbmStockMgr_Dbas(ISbmUserOrganisationContext userContext) :
base(userContext)
{
Initialise();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
}
base.Dispose(disposing);
}
//-------------------------------------------------------------------
// Initialisation.
private void Initialise()
{
mbr.db = (SbmGlobals.Application.GetBusMgrDatabase(SessionContext) as SacsDatabase);
mbr.dbInfo = new SbmDbDataItem.SbmDatabaseInfo(mbr.db.DatabaseName, mbr.db.GetTableDfns());
}
//-------------------------------------------------------------------
// Properties.
protected ISacsDatabase Database { get { return mbr.db; } }
protected SbmDbDataItem.SbmDatabaseInfo DbInfo { get { return mbr.dbInfo; } }
//-------------------------------------------------------------------
// Utilities.
public override bool StockDatabaseIsValid()
{
ClearErrorState();
if (!Database.TableExists(SbmDbDfn.GetTableName(SbmDbDfn.TableIdx.Location))) return SetErrorState("Location table does not exist");
if (!Database.TableExists(SbmDbDfn.GetTableName(SbmDbDfn.TableIdx.Movement))) return SetErrorState("Movement table does not exist");
if (!Database.TableExists(SbmDbDfn.GetTableName(SbmDbDfn.TableIdx.StockLevel))) return SetErrorState("Stock Level table does not exist");
return true;
}
public override bool ColumnExists(SbmDbDfn.ColumnIdx columnIdx)
{
SbmDbDfn.TableIdx tableIdx = SbmDbDfn.GetTableForColumn(columnIdx);
String schemaName = SbmDbDfn.GetSchemaName(tableIdx, false);
String tableName = SbmDbDfn.GetTableName(tableIdx, false);
String columnName = SbmDbDfn.GetColumnName(columnIdx, false);
return DbInfo.ColumnExists(schemaName, tableName, columnName);
}
//-------------------------------------------------------------------
// Stock Calculations.
public override bool CalculateStockLevels(IEnumerable<SysId> commodityIds, DbUpdateInfo dbUpdateInfo)
{
bool calculate = true;
List<SysId> commodityIdsDone = new List<SysId>();
foreach (SysId commodityId in commodityIds)
{
if (SysId.IsValid(commodityId) && !commodityIdsDone.Contains(commodityId))
{
calculate = CalculateStockLevels(commodityId, dbUpdateInfo);
commodityIdsDone.Add(commodityId);
}
}
return calculate;
}
public override bool CalculateStockLevels(SysId commodityId, DbUpdateInfo dbUpdateInfo)
{
ClearErrorState();
if (!SysId.IsValid(commodityId)) return SetErrorState("Commodity must be identified for stock calculation");
if (!TableExists(SbmDbDfn.TableIdx.Movement)) return true;
// Create a local Stock Manager to avoid interferring with this one:
DateTime dToday = DateTime.Today;
DateTime dMovementsFrom = DateTime.MinValue;
List<SbmStockLevel> stockTakes = new List<SbmStockLevel>();
Dictionary<SysId, Decimal> lLocationQuantity = new Dictionary<SysId, Decimal>();
//---------------------------------------------------------------------
// Identify the most recent Stock Take by location.
if (!HasErrorState())
{
SbmStockLevel.Params prmStockLevel = new SbmStockLevel.Params();
prmStockLevel.CommodityId = commodityId;
prmStockLevel.RestrictToCalculateStockLocations = true;
prmStockLevel.Type = Sbm.StockLevelType.StockTake;
prmStockLevel.RecordedTo = dToday;
prmStockLevel.MostRecentOnly = true;
prmStockLevel.StatusCat = SbmStockLevel.StatusCat.ConfirmedOnly;
stockTakes.AddRange(GetStockLevelList(prmStockLevel));
// Identify the most recent stock take for each location.
// NB The filter parameters above should already have done this:
for (int i = 0; i < stockTakes.Count; i++)
{
// Retrieve the details for this stock take:
SbmStockLevel stockLevel = stockTakes[i];
// Check whether there is another stock take for this location.
// This logic should not identify more than one stock take for any of the locations:
for (int j = i + 1; j < stockTakes.Count; j++)
{
SbmStockLevel stockLevelCompare = stockTakes[j];
if (stockLevelCompare.CommodityId != stockLevel.CommodityId) continue;
if (stockLevelCompare.LocationId != stockLevel.LocationId) continue;
if (stockLevelCompare.Recorded > stockLevel.Recorded)
{
// Remove the original item item from the list and reset position:
SaLogger.Debug($"{SysUtil.Fn} Duplicate stock level found for location [A].");
stockLevel = null;
stockTakes.RemoveAt(i);
i--;
break;
}
if (stockLevelCompare.Recorded <= stockLevel.Recorded)
{
// Remove this item from the list and reset position:
SaLogger.Debug("SapStockMgr_Odbc::CalculateStockLevel Duplicate stock level found for location [B].\n");
stockLevelCompare = null;
stockTakes.RemoveAt(j);
j--;
}
}
// If this stock take is still valid, make a note of its details:
if (stockLevel != null)
{
if (!lLocationQuantity.ContainsKey(stockLevel.LocationId))
lLocationQuantity[stockLevel.LocationId] = stockLevel.Quantity;
}
}
}
//---------------------------------------------------------------------
// Retrieve all stock movements since the most recent stock take:
if (!HasErrorState())
{
// Common database definitions:
String tableMovement = SbmDbDfn.GetTableName(SbmDbDfn.TableIdx.Movement);
String colMove_ItemType = SbmDbDfn.GetColumnName(SbmDbDfn.ColumnIdx.Movement_ItemType);
String colMove_ItemId = SbmDbDfn.GetColumnName(SbmDbDfn.ColumnIdx.Movement_ItemId);
String colMove_LocationIdFrom = SbmDbDfn.GetColumnName(SbmDbDfn.ColumnIdx.Movement_LocationIdFrom);
String colMove_LocationIdTo = SbmDbDfn.GetColumnName(SbmDbDfn.ColumnIdx.Movement_LocationIdTo);
String colMove_Quantity = SbmDbDfn.GetColumnName(SbmDbDfn.ColumnIdx.Movement_Quantity);
String colMove_Movement = SbmDbDfn.GetColumnName(SbmDbDfn.ColumnIdx.Movement_Movement);
String colMove_Status = SbmDbDfn.GetColumnName(SbmDbDfn.ColumnIdx.Movement_Status);
// Common movement filter parameters:
SbmMovement.Params prmMovement = new SbmMovement.Params();
prmMovement.ItemType = Sbm.ItemType.Commodity;
prmMovement.ItemIds.Add(commodityId);
prmMovement.StatusCat = SbmMovement.StatusCat.ActionedOnly;
//---------------------------------------------------------------
// Identify stock movements to this location (incrementing stock):
DbQuery qryIncrMove = new DbQuery(Database.DbmsType);
qryIncrMove.DistinctRows = true;
qryIncrMove.Tables.Add(tableMovement);
qryIncrMove.Columns.Add(colMove_LocationIdTo);
qryIncrMove.Columns.Add("SUM(" + colMove_Quantity + ")", "TotalQuantity");
qryIncrMove.Filters.AddRange(GetMovementQueryFilter(prmMovement));
// Filter the 'To' location to stock calculation locations:
List<String> lFilterLocation = new List<string>();
// The location is either one that we haven't seen before (ie. not in the 'Location Quantity' list):
lFilterLocation.Add(colMove_LocationIdTo + " IN (-1" + GetLocationIds_Calc().Select(id => SysId.ToString(id)).Concat("", ",") + ") AND " + colMove_LocationIdTo + " NOT IN (0" + lLocationQuantity.Keys.Select(id => SysId.ToString(id)).Concat("", ",") + ")");
// Or we have some record of it already (where we are only interested in recent movements if a stock take has occurred):
foreach (SysId locationId in lLocationQuantity.Keys)
{
SbmStockLevel stockTake = stockTakes.GetByLocationId(locationId);
if (stockTake != null)
// Location has a stock take so restrict to more recent than this date:
lFilterLocation.Add($"{colMove_LocationIdTo}={SysId.ToLong(locationId)} AND {colMove_Movement}>={DbQuery.FormatDateForCondition(DateUtil.IncrementDate(stockTake.Recorded.Date, 1))}");
else
// Location does not have a stock take so retrieve all movements:
lFilterLocation.Add($"{colMove_LocationIdTo}={SysId.ToLong(locationId)}");
}
qryIncrMove.Filters.Add(lFilterLocation.Concat(" OR ", "(", ")"));
qryIncrMove.GroupBy.Add(colMove_LocationIdTo);
using (DbResultSet resultSet = Database.ExecuteQuery(qryIncrMove.BuildQuery()))
{
while (resultSet != null && resultSet.HasRows && resultSet.NextRecord())
{
NamedValue.Map values = resultSet.GetRecordValues();
SysId idLocation = new SysId(values.GetLong(colMove_LocationIdTo));
decimal quantity = values.GetDecimal("TotalQuantity");
if (lLocationQuantity.ContainsKey(idLocation))
lLocationQuantity[idLocation] += quantity;
else
lLocationQuantity[idLocation] = quantity;
}
}
//---------------------------------------------------------------
// Identify stock movements from this location (decrementing stock):
DbQuery qryDecrMove = new DbQuery(Database.DbmsType);
qryDecrMove.DistinctRows = true;
qryDecrMove.Tables.Add(tableMovement);
qryDecrMove.Columns.Add(colMove_LocationIdFrom);
qryDecrMove.Columns.Add("SUM(" + colMove_Quantity + ")", "TotalQuantity");
qryDecrMove.Filters.AddRange(GetMovementQueryFilter(prmMovement));
// The location is either one that we haven't seen before (ie. not in the 'Location Quantity' list):
lFilterLocation.Clear();
lFilterLocation.Add($"{colMove_LocationIdFrom} IN (-1{GetLocationIds_Calc().Select(id => SysId.ToString(id)).Concat("", ",")}) AND {colMove_LocationIdFrom} NOT IN (0{lLocationQuantity.Keys.Select(id => SysId.ToString(id)).Concat("", ",")})");
foreach (SysId locationId in lLocationQuantity.Keys)
{
SbmStockLevel stockTake = stockTakes.GetByLocationId(locationId);
if (stockTake != null)
// Location has a stock take so restrict to more recent than this date:
lFilterLocation.Add($"{colMove_LocationIdFrom}={SysId.ToLong(locationId)} AND {colMove_Movement}>={DbQuery.FormatDateForCondition(DateUtil.IncrementDate(stockTake.Recorded, 1))}");
else
// Location does not have a stock take so retrieve all movements:
lFilterLocation.Add($"{colMove_LocationIdFrom}={SysId.ToLong(locationId)}");
}
qryDecrMove.Filters.Add(lFilterLocation.Concat(" OR ", "(", ")"));
qryDecrMove.GroupBy.Add(colMove_LocationIdFrom);
using (DbResultSet resultSet = Database.ExecuteQuery(qryDecrMove.BuildQuery()))
{
while (resultSet != null && resultSet.HasRows && resultSet.NextRecord())
{
NamedValue.Map values = resultSet.GetRecordValues();
SysId idLocation = new SysId(values.GetLong(colMove_LocationIdFrom));
decimal quantity = values.GetDecimal("TotalQuantity");
if (lLocationQuantity.ContainsKey(idLocation))
lLocationQuantity[idLocation] -= quantity;
else
lLocationQuantity[idLocation] = -quantity;
}
}
//---------------------------------------------------------------
}
//---------------------------------------------------------------------
// Calculate the 'Current Level' stock level for each location:
if (!HasErrorState())
{
HashSet<SysId> lLocationIdUpdated = new HashSet<SysId>();
SbmStockLevel.Params prmStkLvl = new SbmStockLevel.Params();
prmStkLvl.CommodityId = commodityId;
prmStkLvl.Type = Sbm.StockLevelType.CurrentLevel;
prmStkLvl.RestrictToCalculateStockLocations = true;
foreach (SbmStockLevel stockLevel in GetStockLevelList(prmStkLvl))
{
SysId locationId = stockLevel.LocationId;
// Has this location been updated already:
if (!lLocationIdUpdated.Contains(locationId))
{
// Update the stock level's attributes:
stockLevel.Recorded = dToday;
stockLevel.Quantity = 0.0m;
if (lLocationQuantity.ContainsKey(locationId))
stockLevel.Quantity = lLocationQuantity[locationId];
stockLevel.Status = SaStatus.Current;
// Store the stock level:
if (UpdateStockLevel(stockLevel, dbUpdateInfo))
{
// Make a note of this location:
lLocationIdUpdated.Add(locationId);
}
else SetErrorState("Failed to update stock level");
}
else
{
// Location has laready been updated - remove this stock level:
DeleteStockLevel(stockLevel, dbUpdateInfo);
}
}
if (!HasErrorState())
{
// Create current stock levels for any remaining locations:
foreach (SysId locationId in lLocationQuantity.Keys)
{
decimal quantity = lLocationQuantity[locationId];
if (!lLocationIdUpdated.Contains(locationId) && GetLocationIds_Calc().Contains(locationId))
{
SbmStockLevel stockLevel = CreateStockLevel(commodityId, locationId, Sbm.StockLevelType.CurrentLevel, Sbm.StockLevelSource.System);
stockLevel.Recorded = dToday;
stockLevel.Quantity = quantity;
stockLevel.Status = SaStatus.Current;
if (InsertStockLevel(stockLevel, dbUpdateInfo))
{
}
else SetErrorState("Failed to add stock level");
}
}
}
}
//---------------------------------------------------------------------
return OK;
}
//-------------------------------------------------------------------
}
//=======================================================================
}