Various bug fixs and improvements to stock SKU reconciliation

This commit is contained in:
Bobbie Hodgetts
2020-10-05 22:40:55 +01:00
parent cc2534a3e1
commit ddd2b62743
25 changed files with 1026 additions and 467 deletions

View File

@@ -1,86 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
namespace bnhtrade.Core.Logic.Stock
{
public class Reallocate
{
private string sqlConnectionString;
public Reallocate(string sqlConnectionString)
{
this.sqlConnectionString = sqlConnectionString;
}
public int StockReallocateByStockId(int journalTypeId, int stockId, int quantity, int debitStatusId, int creditStatusId,
DateTime entryDate = default(DateTime))
{
if (entryDate == default(DateTime))
{
entryDate = DateTime.Now;
}
// create the list
var posts = new List<(int statusId, int quantity)>();
posts.Add((debitStatusId, quantity));
posts.Add((creditStatusId, (quantity * -1)));
// execute
return Core.Stock.StockJournal.StockJournalInsert(sqlConnectionString, journalTypeId, stockId, posts, entryDate, false);
}
/// <summary>
/// Feed an skuId and quantity into function and the stock will be reallocated
/// </summary>
public List<(int StockJournalId, int Quantity)> StockReallocateBySkuNumber(int journalTypeId, string skuNumber, int quantity, int debitStatusId, int creditStatusId,
bool firstInFirstOut = true, DateTime entryDate = default(DateTime), bool reallocatePartialQuantity = false)
{
var returnList = new List<(int StockJournalId, int Quantity)>();
List<Tuple<int, DateTime, int>> list = Core.Stock.StockJournal.GetStockStatusBalanceBySkuNumber(sqlConnectionString, skuNumber, creditStatusId, entryDate, firstInFirstOut);
if (list == null || !list.Any())
{
return returnList;
}
// quantity check
int avaiableQuantity = 0;
foreach (Tuple<int, DateTime, int> item in list)
{
avaiableQuantity = avaiableQuantity + item.Item3;
}
if (avaiableQuantity < quantity && reallocatePartialQuantity == false)
{
return null;
}
// make the changes
using (TransactionScope scope = new TransactionScope())
{
foreach (Tuple<int, DateTime, int> item in list)
{
if (quantity > item.Item3)
{
int tempInt = StockReallocateByStockId(journalTypeId, item.Item1, item.Item3, debitStatusId, creditStatusId, entryDate);
quantity = quantity - item.Item3;
returnList.Add((tempInt, item.Item3));
}
else
{
int tempInt = StockReallocateByStockId(journalTypeId, item.Item1, quantity, debitStatusId, creditStatusId, entryDate);
returnList.Add((tempInt, quantity));
break;
}
}
scope.Complete();
return returnList;
}
}
}
}

View File

@@ -189,6 +189,26 @@ namespace bnhtrade.Core.Logic.Stock
return resultList;
}
/// <summary>
/// Retrive SKU Transaction by ID
/// </summary>
/// <param name="SkuTransactionId">SKU Transaction ID</param>
/// <param name="retriveTransactionTypeInfo"></param>
/// <returns></returns>
public List<Model.Stock.SkuTransaction> Read(List<int> SkuTransactionId, bool retriveTransactionTypeInfo = true)
{
var dbRead = new Data.Database.Stock.ReadSkuTransaction(sqlConnectionString);
var resultList = dbRead.Read(SkuTransactionId);
if (retriveTransactionTypeInfo)
{
var dbReadType = new Logic.Stock.SkuTransactionTypePersistance(sqlConnectionString);
dbReadType.GetBySkuTransaction(resultList);
}
return resultList;
}
/// <summary>
/// Validates and then updates a Stock SKU Tranaction
/// </summary>

View File

@@ -14,7 +14,7 @@ namespace bnhtrade.Core.Logic.Stock
private Logic.Stock.SkuTransactionPersistance dbSkuTransaction;
private Logic.Stock.SkuTransactionTypePersistance dbSkuTransactionType;
private Logic.Validate.SkuTransaction validateSkuTrans;
private Logic.Stock.Reallocate stockReallocate;
private Logic.Stock.StatusReallocate stockReallocate;
private Logic.Log.LogEvent logEvent;
private string err = "Reconcile Sku Transaction Exception: ";
@@ -26,7 +26,7 @@ namespace bnhtrade.Core.Logic.Stock
dbSkuTransactionType = new SkuTransactionTypePersistance(sqlConnectionString);
readShipmentInfo = new Data.Database.AmazonShipment.ReadShipmentInfo(sqlConnectionString);
validateSkuTrans = new Validate.SkuTransaction();
stockReallocate = new Logic.Stock.Reallocate(sqlConnectionString);
stockReallocate = new Logic.Stock.StatusReallocate(sqlConnectionString);
logEvent = new Log.LogEvent();
}
@@ -194,50 +194,52 @@ namespace bnhtrade.Core.Logic.Stock
}
// make the journal entries
var list = new List<(int StockJournalId, int Quantity)>();
var journalList = new List<(int StockJournalId, int Quantity)>();
if (transList[i].SkuTransactionType.FilterStockOnDateTime)
{
list = stockReallocate.StockReallocateBySkuNumber(
journalList = stockReallocate.BySkuNumber(
transList[i].TransactionDate,
transList[i].SkuTransactionType.StockJournalTypeId,
transList[i].SkuNumber,
transList[i].Quantity,
transList[i].SkuTransactionType.DebitStockStatusId.GetValueOrDefault(),
transList[i].SkuTransactionType.CreditStockStatusId.GetValueOrDefault(),
transList[i].SkuTransactionType.FirstInFirstOut,
transList[i].TransactionDate,
false);
true);
}
else
{
list = stockReallocate.StockReallocateBySkuNumber(
journalList = stockReallocate.BySkuNumber(
DateTime.UtcNow,
transList[i].SkuTransactionType.StockJournalTypeId,
transList[i].SkuNumber,
transList[i].Quantity,
transList[i].SkuTransactionType.DebitStockStatusId.GetValueOrDefault(),
transList[i].SkuTransactionType.CreditStockStatusId.GetValueOrDefault(),
transList[i].SkuTransactionType.FirstInFirstOut,
DateTime.UtcNow,
false);
true);
}
// insufficient balance available
if (list == null || !list.Any())
if (!journalList.Any())
{
// in special case (found inventory), continue
if (transList[i].SkuTransactionType.TypeCode.Contains("<AmazonReport><_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_><F>"))
{
ItemsCompleted++;
ItemsRemaining--;
continue;
}
else
{
ProgressMessage = "Insurficent status/location balance to relocate stock";
ProgressMessage = "Insurficent quantity at status/location to relocate stock";
recordSkip = recordSkip + 1;
break;
}
}
// fail safe
int qtyAllocated = list.Sum(c => c.Quantity);
int qtyAllocated = journalList.Sum(c => c.Quantity);
if (qtyAllocated > transList[i].Quantity)
{
throw new Exception(
@@ -248,40 +250,41 @@ namespace bnhtrade.Core.Logic.Stock
// update sku transaction table
int qtyRemain = qtyAllocated;
var newRecordList = new List<Model.Stock.SkuTransaction>();
for (int j = 0; j <= list.Count; j++)
for (int j = 0; j < journalList.Count; j++)
{
// update existing record
if (j == 0)
{
transList[i].Quantity = (short)list[j].Quantity;
transList[i].StockJournalId = list[j].StockJournalId;
transList[i].Quantity = (short)journalList[j].Quantity;
transList[i].StockJournalId = journalList[j].StockJournalId;
transList[i].IsProcessed = true;
dbSkuTransaction.Update(transList[i]);
}
// new record
else if (j < list.Count)
else
{
var newRecord = transList[i].Clone();
newRecord.Quantity = (short)list[j].Quantity;
newRecord.Quantity = (short)journalList[j].Quantity;
newRecord.IsProcessed = true;
newRecord.StockJournalId = list[j].StockJournalId;
newRecordList.Add(newRecord);
}
// new record, unallocated quantity
else if (qtyRemain > 0)
{
var newRecord = transList[i].Clone();
newRecord.Quantity = (short)qtyRemain;
newRecord.IsProcessed = false;
newRecord.StockJournalId = journalList[j].StockJournalId;
newRecordList.Add(newRecord);
}
if (j < list.Count)
{
qtyRemain = qtyRemain - list[j].Quantity;
}
qtyRemain = qtyRemain - journalList[j].Quantity;
}
// new record for unallocated quantity
if (qtyRemain > 0)
{
var newRecord = transList[i].Clone();
newRecord.Quantity = (short)qtyRemain;
newRecord.IsProcessed = false;
newRecordList.Add(newRecord);
}
// add new transactions to table
for (int j = 0; j < newRecordList.Count; j++)
{
@@ -448,7 +451,18 @@ namespace bnhtrade.Core.Logic.Stock
public void UnReconcileTransaction(int skuTransactionId)
{
dbSkuTransaction.DeleteJournalEntry(skuTransactionId);
var trans = dbSkuTransaction.Read(new List<int> { skuTransactionId }, false).FirstOrDefault();
if (trans == null) { return; }
// test if journal entry needs deleting, or just set to isprocessed = false
if (trans.IsProcessed == true && trans.IsSetStockJournalId)
{
dbSkuTransaction.DeleteJournalEntry(skuTransactionId);
}
else if (trans.IsProcessed == true)
{
new Data.Database.Stock.UpdateSkuTransaction(sqlConnectionString).Update(skuTransactionId, false);
}
}
}
}

View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace bnhtrade.Core.Logic.Stock
{
public class StatusBalance
{
private string sqlConnectionString;
public StatusBalance(string sqlConnectionString)
{
this.sqlConnectionString = sqlConnectionString;
}
/// <summary>
/// Return the avaliable balance of a status. Uses a more efficent sql/code. However, balance requests should
/// generally also involve a date/time (i.e. the system does allow a stock transaction in the future, therefore
/// this method may give an available quantity, but transfers before that date wouod not be possible).
/// </summary>
/// <param name="sku">SKU number</param>
/// <param name="statusId">Status ID</param>
/// <returns>Balance as quantity</returns>
private int GetAvailableBalanceBySku(string sku, int statusId)
{
return new Data.Database.Stock.ReadStatusBalance(sqlConnectionString).BySku(sku, statusId);
}
/// <summary>
/// Return the avaliable balance of a status at a specified date and time. Useful for checking availability before
/// moving stock/sku retrospectivly
/// </summary>
/// <param name="sku">SKU number</param>
/// <param name="statusId">Status ID</param>
/// <param name="atDate">Date and time you would like to know the balance at</param>
/// <returns></returns>
public Model.Stock.StatusBalance GetBySku(string sku, int statusId)
{
if (string.IsNullOrWhiteSpace(sku))
{
throw new Exception("SKU number is null, empty, or whitespace");
}
// get list of transactions for availale stock
var stockTransaction = new Data.Database.Stock.ReadStatusTransaction(sqlConnectionString);
var transList = stockTransaction.BySku(statusId, sku);
// create quantity list
List<int> qtyList = new List<int>();
for (int i = 0; i < transList.TransactionList.Count; i++)
{
qtyList.Add(transList.TransactionList[i].Quantity);
}
// tally list
// loop, in reverse, to find credits to tally with debits
for (int iCr = qtyList.Count - 1; iCr > -1; iCr--)
{
if (qtyList[iCr] < 0)
{
int crStockNumber = transList.TransactionList[iCr].StockNumber;
DateTime crDate = transList.TransactionList[iCr].EntryDate;
// loop, in reverse, to find debits
for (int iDr = qtyList.Count - 1; iDr > -1; iDr--)
{
// find debits, last in first out (filter by date)
if (transList.TransactionList[iDr].StockNumber == crStockNumber
&& transList.TransactionList[iDr].EntryDate <= crDate
&& qtyList[iDr] > 0)
{
// credit fully assigned
if ((qtyList[iCr] + qtyList[iDr]) >= 0)
{
qtyList[iDr] = qtyList[iDr] + qtyList[iCr];
qtyList[iCr] = 0;
break;
}
// credit partially assigned
else
{
qtyList[iCr] = qtyList[iDr] + qtyList[iCr];
qtyList[iDr] = 0;
}
}
}
}
}
// build result list from tally results
var result = new Model.Stock.StatusBalance();
result.Sku = transList.Sku;
result.StockStatusId = transList.StockStatusId;
for (int i = 0; i < qtyList.Count; i++)
{
if (qtyList[i] != 0)
{
result.AddBalanceTransaction(
transList.TransactionList[i].EntryDate,
transList.TransactionList[i].StockNumber,
qtyList[i]);
}
}
return result;
}
}
}

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
namespace bnhtrade.Core.Logic.Stock
{
public class StatusReallocate
{
private string sqlConnectionString;
public StatusReallocate(string sqlConnectionString)
{
this.sqlConnectionString = sqlConnectionString;
}
/// <summary>
/// Reallocates stock between status' by Stock Id
/// </summary>
/// <param name="journalTypeId"></param>
/// <param name="stockId"></param>
/// <param name="quantity"></param>
/// <param name="debitStatusId"></param>
/// <param name="creditStatusId"></param>
/// <param name="entryDate"></param>
/// <returns>Return newly created stock journal Id</returns>
public int ByStockId(DateTime entryDate, int journalTypeId, int stockId, int quantity, int debitStatusId, int creditStatusId)
{
if (entryDate == default(DateTime))
{
entryDate = DateTime.Now;
}
// create the list
var posts = new List<(int statusId, int quantity)>();
posts.Add((debitStatusId, quantity));
posts.Add((creditStatusId, (quantity * -1)));
// execute
return Core.Stock.StockJournal.StockJournalInsert(sqlConnectionString, journalTypeId, stockId, posts, entryDate, false);
}
/// <summary>
/// Feed an SKU number and quantity into function and the stock will be reallocated
/// </summary>
/// <param name="entryDate">Date and time of the transaction</param>
/// <param name="journalTypeId">Journal Type ID</param>
/// <param name="skuNumber">Sku Number to reallocate</param>
/// <param name="quantity">Quantity to reallocate</param>
/// <param name="debitStatusId">Status to move SKU to</param>
/// <param name="creditStatusId">Status to move SKU from</param>
/// <param name="firstInFirstOut">Move stock on first in first out basis</param>
/// <param name="reallocatePartialQuantity">Reallocate patial quantity if the full quantity is not available</param>
/// <returns></returns>
public List<(int StockJournalId, int Quantity)> BySkuNumber(DateTime entryDate, int journalTypeId, string skuNumber, int quantity, int debitStatusId, int creditStatusId,
bool firstInFirstOut = true, bool reallocatePartialQuantity = false)
{
var returnList = new List<(int StockJournalId, int Quantity)>();
// get balance of status and check for avaliable quantity
var statusBalance = new Logic.Stock.StatusBalance(sqlConnectionString).GetBySku(skuNumber, creditStatusId);
if (statusBalance.GetAvaliableQuantity(entryDate) <= 0
|| (statusBalance.CheckAvaliableQuantity(quantity, entryDate) == false && reallocatePartialQuantity == false
))
{
return returnList;
}
// temp code start
// until use of stockId is designed out of application
var getStockId = new Data.Database.Stock.ReadStockId(sqlConnectionString);
var stockIdDictionary = new Dictionary<int, int>();
foreach (var item in statusBalance.ByDateList)
{
if (!stockIdDictionary.ContainsKey(item.StockNumber))
{
stockIdDictionary.Add(item.StockNumber, 0);
}
}
stockIdDictionary = getStockId.ByStockNumber(stockIdDictionary.Keys.ToList());
// temp code finish
//make the changes
using (TransactionScope scope = new TransactionScope())
{
foreach (var item in statusBalance.ByDateList)
{
if (quantity > item.Quantity)
{
int journalId = ByStockId(entryDate, journalTypeId, stockIdDictionary[item.StockNumber], item.Quantity, debitStatusId, creditStatusId);
quantity = quantity - item.Quantity;
returnList.Add((journalId, item.Quantity));
}
else
{
int journalId = ByStockId(entryDate, journalTypeId, stockIdDictionary[item.StockNumber], quantity, debitStatusId, creditStatusId);
returnList.Add((journalId, quantity));
break;
}
}
scope.Complete();
return returnList;
}
}
}
}