From 3a53350f85ddb28b93673b70f916e5b93a96779a Mon Sep 17 00:00:00 2001 From: Bobbie Hodgetts Date: Fri, 29 May 2020 14:07:46 +0100 Subject: [PATCH] Stock SKU transaction reconciliation --- src/bnhtrade.ComTypeLib/Stock/Stock.cs | 7 + .../Database/Stock/CreateSkuTransaction.cs | 8 +- .../Data/Database/Stock/ReadSkuTransaction.cs | 90 +++++-- .../Database/Stock/ReadSkuTransactionType.cs | 12 +- .../Database/Stock/UpdateSkuTransaction.cs | 10 +- ...fo.cs => ShipmentInfoPersistanceUpdate.cs} | 4 +- .../Logic/Stock/SkuTransactionPersistance.cs | 63 ++++- .../Logic/Stock/SkuTransactionReconcile.cs | 238 +++++++++++------- .../Stock/SkuTransactionTypePersistance.cs | 69 ++++- src/bnhtrade.Core/Model/Stock/JournalEntry.cs | 25 ++ .../Model/Stock/JournalEntryPost.cs | 34 +++ .../Model/Stock/SkuTransaction.cs | 75 ++++-- .../Model/Stock/SkuTransactionType.cs | 6 +- src/bnhtrade.Core/Program.cs | 44 ++-- src/bnhtrade.Core/bnhtrade.Core.csproj | 4 +- src/bnhtrade.ScheduledTasks/Program.cs | 4 +- 16 files changed, 520 insertions(+), 173 deletions(-) rename src/bnhtrade.Core/Logic/AmazonFBAInbound/{UpdateDatabaseShipmentInfo.cs => ShipmentInfoPersistanceUpdate.cs} (97%) create mode 100644 src/bnhtrade.Core/Model/Stock/JournalEntry.cs create mode 100644 src/bnhtrade.Core/Model/Stock/JournalEntryPost.cs diff --git a/src/bnhtrade.ComTypeLib/Stock/Stock.cs b/src/bnhtrade.ComTypeLib/Stock/Stock.cs index 2bfd914..104ff83 100644 --- a/src/bnhtrade.ComTypeLib/Stock/Stock.cs +++ b/src/bnhtrade.ComTypeLib/Stock/Stock.cs @@ -27,6 +27,8 @@ namespace bnhtrade.ComTypeLib object ReconcileStockTransactions(ConnectionCredential sqlConnCred); + void UnReconcileSkuTransaction(ConnectionCredential sqlConnCred, int skuTransactionId); + bool StockJournalConsistencyCheck(ConnectionCredential sqlConnCred, int stockId); } @@ -118,6 +120,11 @@ namespace bnhtrade.ComTypeLib } + public void UnReconcileSkuTransaction(ConnectionCredential sqlConnCred, int skuTransactionId) + { + new Core.Logic.Stock.SkuTransactionReconcile(sqlConnCred.ConnectionString).UnReconcileTransaction(skuTransactionId); + } + public bool StockJournalConsistencyCheck(ConnectionCredential sqlConnCred, int stockId) { return Core.Stock.StockJournal.WIP_StockJournalConsistencyCheck(sqlConnCred.ConnectionString, stockId, null); diff --git a/src/bnhtrade.Core/Data/Database/Stock/CreateSkuTransaction.cs b/src/bnhtrade.Core/Data/Database/Stock/CreateSkuTransaction.cs index 2a7b9e0..420ae0a 100644 --- a/src/bnhtrade.Core/Data/Database/Stock/CreateSkuTransaction.cs +++ b/src/bnhtrade.Core/Data/Database/Stock/CreateSkuTransaction.cs @@ -34,7 +34,11 @@ namespace bnhtrade.Core.Data.Database.Stock OUTPUT INSERTED.StockSkuTransactionID VALUES ( @transactionDate - ,@stockSkuTransactionTypeID + ,( + SELECT StockSkuTransactionTypeID + FROM tblStockSkuTransactionType + WHERE TypeCode = @skuTransactionTypeCode + ) ,@foreignKey ,@reference ,@Detail @@ -50,7 +54,7 @@ namespace bnhtrade.Core.Data.Database.Stock ", conn)) { cmd.Parameters.AddWithValue("@transactionDate", skuTransaction.TransactionDate.ToUniversalTime()); - cmd.Parameters.AddWithValue("@stockSkuTransactionTypeID", skuTransaction.SkuTransactionTypeId); + cmd.Parameters.AddWithValue("@skuTransactionTypeCode", skuTransaction.SkuTransactionTypeCode); if (!skuTransaction.IsSetForeignKey) { cmd.Parameters.AddWithValue("@foreignKey", DBNull.Value); } else { cmd.Parameters.AddWithValue("@foreignKey", skuTransaction.ForeignKey); } if (!skuTransaction.IsSetReference) { cmd.Parameters.AddWithValue("@reference", DBNull.Value); } diff --git a/src/bnhtrade.Core/Data/Database/Stock/ReadSkuTransaction.cs b/src/bnhtrade.Core/Data/Database/Stock/ReadSkuTransaction.cs index 189f776..a1cfbf7 100644 --- a/src/bnhtrade.Core/Data/Database/Stock/ReadSkuTransaction.cs +++ b/src/bnhtrade.Core/Data/Database/Stock/ReadSkuTransaction.cs @@ -10,11 +10,45 @@ namespace bnhtrade.Core.Data.Database.Stock { public class ReadSkuTransaction : Connection { + private Data.Database.WhereBuilder whereBuilder = new WhereBuilder(); + public ReadSkuTransaction(string sqlConnectionString) : base(sqlConnectionString) { } + /// + /// Initialise result filters + /// + public void Init() + { + whereBuilder = new WhereBuilder(); + IsReconciled = null; + StockTransactionTypeName = null; + StockTransactionTypeCode = null; + } + + /// + /// Read result filter + /// + public bool? IsReconciled { get; set; } + + /// + /// Read result filter + /// + public List StockTransactionTypeName { get; set; } + + /// + /// Read result filter + /// + public List StockTransactionTypeCode { get; set; } + + + /// + /// Retrive a 'Stock Journal ID' for a reconciled SKU transaction + /// + /// Stock SKU Transaction ID + /// Stock Journal ID public int? GetJournalId(int skuTransactionId) { using (var conn = new SqlConnection(sqlConnectionString)) @@ -41,24 +75,19 @@ namespace bnhtrade.Core.Data.Database.Stock } } - public List GetUnreconciled() + + /// + /// Populates model class from filtered database results + /// + /// List of SkuTransaction + public List Read() { // order by stocktransId desc = Amazon reports are listed in datetime ASC order, some reports have date only, // however I have reason to believe these are in time order. // Adding this means they at least get processed in the correct order (maybe)! - string sqlWhere = @" - WHERE tblStockSkuTransaction.IsProcessed = 0 - AND ( - tblStockSkuTransactionType.StockJournalEntryEnabled = 1 - OR tblStockSkuTransactionType.IsNewReviewRequired = 1 - ) "; - return Read(sqlWhere, new DynamicParameters()); - } - - private List Read(string sqlWhere, DynamicParameters parameters) - { var resultList = new List(); + var parameters = new DynamicParameters(); string sql = @" SELECT tblStockSkuTransaction.StockSkuTransactionID AS SkuTransactionId @@ -70,14 +99,43 @@ namespace bnhtrade.Core.Data.Database.Stock ,tblStockSkuTransaction.Quantity ,tblStockSkuTransaction.IsProcessed ,tblStockSkuTransaction.StockJournalID AS StockJournalId - ,tblStockSkuTransactionType.TypeTitle AS SkuTransactionTypeName + ,tblStockSkuTransactionType.TypeName AS SkuTransactionTypeName ,tblSku.skuSkuNumber AS SkuNumber ,tblStockSkuTransaction.SkuID FROM tblStockSkuTransaction INNER JOIN tblStockSkuTransactionType ON tblStockSkuTransaction.StockSkuTransactionTypeID = tblStockSkuTransactionType.StockSkuTransactionTypeID - INNER JOIN tblSku ON tblStockSkuTransaction.SkuID = tblSku.skuSkuID "; - - sql += sqlWhere; + INNER JOIN tblSku ON tblStockSkuTransaction.SkuID = tblSku.skuSkuID + WHERE 1=1 "; + + if (IsReconciled != null) + { + sql += @" + AND tblStockSkuTransaction.IsProcessed ="; + if (IsReconciled.GetValueOrDefault()) + { + sql += " 1 "; + } + else + { + sql += " 0 "; + } + } + + if (StockTransactionTypeName != null && StockTransactionTypeName.Any()) + { + parameters.Add("@stockTransactionTypeName", StockTransactionTypeName); + + sql += @" + AND tblStockSkuTransactionType.TypeName IN @stockTransactionTypeName "; + } + + if (StockTransactionTypeCode != null && StockTransactionTypeCode.Any()) + { + parameters.Add("@stockTransactionTypeCode", StockTransactionTypeCode); + + sql += @" + AND tblStockSkuTransactionType.TypeCode IN @stockTransactionTypeCode "; + } sql += @" ORDER BY tblStockSkuTransaction.TransactionDate ASC, tblStockSkuTransaction.StockSkuTransactionID DESC;"; diff --git a/src/bnhtrade.Core/Data/Database/Stock/ReadSkuTransactionType.cs b/src/bnhtrade.Core/Data/Database/Stock/ReadSkuTransactionType.cs index 58d0f43..c4602f9 100644 --- a/src/bnhtrade.Core/Data/Database/Stock/ReadSkuTransactionType.cs +++ b/src/bnhtrade.Core/Data/Database/Stock/ReadSkuTransactionType.cs @@ -54,15 +54,11 @@ namespace bnhtrade.Core.Data.Database.Stock { if (reader.Read()) { - int index01 = reader.GetOrdinal("StockSkuTransactionTypeID"); - int index02 = reader.GetOrdinal("IsNewReviewRequired"); - int index03 = reader.GetOrdinal("TransactionImportEnabled"); + int transactionTypeId = reader.GetInt32(0); + bool isNew = reader.GetBoolean(1); + bool importEnabled = reader.GetBoolean(2); - int transactionTypeId = reader.GetInt32(index01); - bool isNew = reader.GetBoolean(index02); - bool? importEnabled = reader[index03] as bool? ?? null; // column can be null - - if (isNew == true || importEnabled == null) + if (isNew == true) { // return 0 and 'skip' item return 0; diff --git a/src/bnhtrade.Core/Data/Database/Stock/UpdateSkuTransaction.cs b/src/bnhtrade.Core/Data/Database/Stock/UpdateSkuTransaction.cs index ffcade3..2f613c1 100644 --- a/src/bnhtrade.Core/Data/Database/Stock/UpdateSkuTransaction.cs +++ b/src/bnhtrade.Core/Data/Database/Stock/UpdateSkuTransaction.cs @@ -148,14 +148,18 @@ namespace bnhtrade.Core.Data.Database.Stock public void Update(Model.Stock.SkuTransaction skuTransaction) { - using (var conn = new SqlConnection()) + using (var conn = new SqlConnection(sqlConnectionString)) { conn.Open(); using (SqlCommand cmd = new SqlCommand(@" UPDATE tblStockSkuTransaction SET TransactionDate = @transactionDate - ,StockSkuTransactionTypeID = @stockSkuTransactionTypeID + ,StockSkuTransactionTypeID = ( + SELECT StockSkuTransactionTypeID + FROM tblStockSkuTransactionType + WHERE TypeCode = @skuTransactionTypeCode + ) ,ForeignKey = @foreignKey ,Reference = @reference ,Detail = @Detail @@ -171,7 +175,7 @@ namespace bnhtrade.Core.Data.Database.Stock ", conn)) { cmd.Parameters.AddWithValue("@transactionDate", skuTransaction.TransactionDate.ToUniversalTime()); - cmd.Parameters.AddWithValue("@stockSkuTransactionTypeID", skuTransaction.SkuTransactionTypeId); + cmd.Parameters.AddWithValue("@skuTransactionTypeCode", skuTransaction.SkuTransactionTypeCode); if (!skuTransaction.IsSetForeignKey) { cmd.Parameters.AddWithValue("@foreignKey", DBNull.Value); } else { cmd.Parameters.AddWithValue("@foreignKey", skuTransaction.ForeignKey); } if (!skuTransaction.IsSetReference) { cmd.Parameters.AddWithValue("@reference", DBNull.Value); } diff --git a/src/bnhtrade.Core/Logic/AmazonFBAInbound/UpdateDatabaseShipmentInfo.cs b/src/bnhtrade.Core/Logic/AmazonFBAInbound/ShipmentInfoPersistanceUpdate.cs similarity index 97% rename from src/bnhtrade.Core/Logic/AmazonFBAInbound/UpdateDatabaseShipmentInfo.cs rename to src/bnhtrade.Core/Logic/AmazonFBAInbound/ShipmentInfoPersistanceUpdate.cs index b353629..2429064 100644 --- a/src/bnhtrade.Core/Logic/AmazonFBAInbound/UpdateDatabaseShipmentInfo.cs +++ b/src/bnhtrade.Core/Logic/AmazonFBAInbound/ShipmentInfoPersistanceUpdate.cs @@ -8,12 +8,12 @@ using bnhtrade.Core.Data; namespace bnhtrade.Core.Logic.AmazonFBAInbound { - public class UpdateDatabaseShipmentInfo + public class ShipmentInfoPersistanceUpdate { private string sqlConnectionString; private readonly string logDateTimeId = "FbaInboundShipmentNewCheck"; public int TotalUpdated { get; private set; } = 0; - public UpdateDatabaseShipmentInfo(string sqlConnectionString) + public ShipmentInfoPersistanceUpdate(string sqlConnectionString) { this.sqlConnectionString = sqlConnectionString; } diff --git a/src/bnhtrade.Core/Logic/Stock/SkuTransactionPersistance.cs b/src/bnhtrade.Core/Logic/Stock/SkuTransactionPersistance.cs index 010f596..bd3ad9b 100644 --- a/src/bnhtrade.Core/Logic/Stock/SkuTransactionPersistance.cs +++ b/src/bnhtrade.Core/Logic/Stock/SkuTransactionPersistance.cs @@ -24,6 +24,31 @@ namespace bnhtrade.Core.Logic.Stock log = new Log.LogEvent(); } + /// + /// Initialise result filters + /// + public void Init() + { + IsReconciled = null; + StockTransactionTypeName = null; + StockTransactionTypeCode = null; + } + + /// + /// Read result filter + /// + public bool? IsReconciled { get; set; } + + /// + /// Read result filter + /// + public List StockTransactionTypeName { get; set; } + + /// + /// Read result filter + /// + public List StockTransactionTypeCode { get; set; } + private Data.Database.Stock.DeleteSkuTransaction DatabaseSkuTransDelete(bool forceNew = false) { if (dbSkuTransDelete == null || forceNew) @@ -93,13 +118,18 @@ namespace bnhtrade.Core.Logic.Stock } } + + /// + /// Deletes an attached journal entry, if one is present. + /// + /// Stock SKU Transaction ID public void DeleteJournalEntry(int skuTransactionId) { using (var scope = new TransactionScope()) { try { - // is there a journal entry attached? + // comfirm there is a journal entry attached int? journalId = DatabaseSkuTransRead().GetJournalId(skuTransactionId); if (journalId != null) { @@ -116,6 +146,10 @@ namespace bnhtrade.Core.Logic.Stock } } + /// + /// Validates and then creates a Stock SKU Tranaction + /// + /// 'Stock SKU Transaction' model public void Create(Model.Stock.SkuTransaction skuTransaction) { if (skuTransaction == null) @@ -134,6 +168,31 @@ namespace bnhtrade.Core.Logic.Stock DatabaseSkuTransInsert().Create(skuTransaction); } + /// + /// + /// + /// Retrive and include transaction type model class + public List Read(bool retriveTransactionTypeInfo = true) + { + var dbRead = new Data.Database.Stock.ReadSkuTransaction(sqlConnectionString); + dbRead.IsReconciled = IsReconciled; + dbRead.StockTransactionTypeCode = StockTransactionTypeCode; + dbRead.StockTransactionTypeName = StockTransactionTypeName; + var resultList = dbRead.Read(); + + if (retriveTransactionTypeInfo) + { + var dbReadType = new Logic.Stock.SkuTransactionTypePersistance(sqlConnectionString); + dbReadType.GetBySkuTransaction(resultList); + } + + return resultList; + } + + /// + /// Validates and then updates a Stock SKU Tranaction + /// + /// 'Stock SKU Transaction' model public void Update(Model.Stock.SkuTransaction skuTransaction) { if (skuTransaction == null) @@ -159,7 +218,7 @@ namespace bnhtrade.Core.Logic.Stock DeleteJournalEntry(skuTransaction.SkuTransactionId); } } - dbSkuTransUpdate.Update(skuTransaction); + DatabaseSkuTransUpdate().Update(skuTransaction); scope.Complete(); } } diff --git a/src/bnhtrade.Core/Logic/Stock/SkuTransactionReconcile.cs b/src/bnhtrade.Core/Logic/Stock/SkuTransactionReconcile.cs index 350dc80..9f85d67 100644 --- a/src/bnhtrade.Core/Logic/Stock/SkuTransactionReconcile.cs +++ b/src/bnhtrade.Core/Logic/Stock/SkuTransactionReconcile.cs @@ -86,10 +86,13 @@ namespace bnhtrade.Core.Logic.Stock logEvent.LogInformation("Starting ReconcileStockTransactions()"); int recordSkip = 0; - ProcessLostAndFound(); + ReconcileLostAndFound(); // get list of sku transactions to reconcile - var transList = new Data.Database.Stock.ReadSkuTransaction(sqlConnectionString).GetUnreconciled(); + dbSkuTransaction.Init(); + dbSkuTransaction.IsReconciled = false; + var transList = dbSkuTransaction.Read(); + var shipmentInfoDic = new Dictionary(); ItemsRemaining = transList.Count; ItemsCompleted = 0; @@ -106,88 +109,113 @@ namespace bnhtrade.Core.Logic.Stock // setup return values CurrentSkuTransaction = transList[i]; CurrentTransactionId = transList[i].SkuTransactionId; - CurrentTransactionTypeId = transList[i].SkuTransactionTypeId; + CurrentTransactionTypeId = transList[i].SkuTransactionType.TypeId; LastItemDateTime = transList[i].TransactionDate; // load type into variable - var transType = dbSkuTransactionType.GetByTypeName(transList[i].SkuTransactionTypeName); + //var transType = dbSkuTransactionType.GetByTypeName(transList[i].SkuTransactionTypeName); // stop if a new transactiontype is encountered - if (transType.IsNewReviewRequired) + if (transList[i].SkuTransactionType.IsNewReviewRequired) { ProgressMessage = "New 'Transaction-Type' encountered"; //Console.Write("\r"); //MiscFunction.EventLogInsert(errMessage, 1); - goto Stop; + break; } - // set debit/credit status' for special cases (i.e. NULL or 0 debit/credit ids in stockTransactionType table) - if (transType.StockJournalEntryEnabled == true && (transType.DebitStockStatusId == 0 || transType.CreditStockStatusId == 0)) - { - // FBA Shipment Receipt +ve - if (transType.TypeCode == "<_GET_FBA_FULFILLMENT_INVENTORY_RECEIPTS_DATA_><+ve>" - || transType.TypeCode == "<_GET_FBA_FULFILLMENT_INVENTORY_RECEIPTS_DATA_><-ve>") - { - var shipmentInfo = readShipmentInfo.HeaderByFbaShipmentId(transList[i].Reference); - - if (shipmentInfo.IsSetShipmentStockStatusId()) - { - // +ve shipment receipt - if (transType.CreditStockStatusId == 0 && transType.DebitStockStatusId > 0) - { transType.CreditStockStatusId = shipmentInfo.ShipmentStockStatusId; } - - // -ve shipment receipt - else if (transType.DebitStockStatusId == 0 && transType.CreditStockStatusId > 0) - { transType.DebitStockStatusId = shipmentInfo.ShipmentStockStatusId; } - - // something went wrong, raise error - else - { - ProgressMessage = "Unable to retrive FBA shipment location/status from tblAmazonShipment for Amazon shipment '" + shipmentInfo.FbaShipmentId + "'."; - recordSkip = recordSkip + 1; - goto Stop; - } - } - } - // something went wrong, raise error - else - { - ProgressMessage = "Coding required. Unhandled special case Transaction-Type encountered (Transaction-Type debit or credit is set to 0)."; - recordSkip = recordSkip + 1; - goto Stop; - } - } - - // make the changes - if (transType.StockJournalEntryEnabled == false) + else if (transList[i].SkuTransactionType.StockJournalEntryEnabled == false) { transList[i].IsProcessed = true; dbSkuTransaction.Update(transList[i]); } + + // stock journal entry is enabled else { + // check debit/credits + if (transList[i].SkuTransactionType.DebitStockStatusId.GetValueOrDefault() == 0 + || transList[i].SkuTransactionType.CreditStockStatusId.GetValueOrDefault() == 0) + { + // special case FBA Shipment Receipt +ve + if (transList[i].SkuTransactionType.TypeCode == "<_GET_FBA_FULFILLMENT_INVENTORY_RECEIPTS_DATA_><+ve>" + || transList[i].SkuTransactionType.TypeCode == "<_GET_FBA_FULFILLMENT_INVENTORY_RECEIPTS_DATA_><-ve>") + { + Model.AmazonFba.ShipmentInfo shipmentInfo = null; + if (!shipmentInfoDic.ContainsKey(transList[i].Reference)) + { + shipmentInfo = readShipmentInfo.HeaderByFbaShipmentId(transList[i].Reference); + if (shipmentInfo == null) + { + throw new Exception("Unable to retrive shipment info for reference '" + transList[i].Reference + "'."); + } + else + { + shipmentInfoDic.Add(transList[i].Reference, shipmentInfo); + } + } + + if (shipmentInfo.IsSetShipmentStockStatusId()) + { + // +ve shipment receipt + if (transList[i].SkuTransactionType.CreditStockStatusId == 0 + && transList[i].SkuTransactionType.DebitStockStatusId > 0) + { + transList[i].SkuTransactionType.CreditStockStatusId = shipmentInfo.ShipmentStockStatusId; + } + + // -ve shipment receipt + else if (transList[i].SkuTransactionType.DebitStockStatusId == 0 + && transList[i].SkuTransactionType.CreditStockStatusId > 0) + { + transList[i].SkuTransactionType.DebitStockStatusId = shipmentInfo.ShipmentStockStatusId; + } + + // something went wrong, raise error + else + { + ProgressMessage = "Unable to retrive FBA shipment location/status from tblAmazonShipment for Amazon shipment '" + shipmentInfo.FbaShipmentId + "'."; + recordSkip = recordSkip + 1; + break; + } + } + else + { + throw new Exception("Unable to retrive shipment info."); + } + } + // manual entry + else + { + ProgressMessage = "Transaction-Type debit or credit is not set, is this a manual entry?"; + recordSkip = recordSkip + 1; + break; + } + } + + // make the journal entries var list = new List<(int StockJournalId, int Quantity)>(); - if (transType.FilterStockOnDateTime) + if (transList[i].SkuTransactionType.FilterStockOnDateTime) { list = stockReallocate.StockReallocateBySkuNumber( - transType.StockJournalTypeId, + transList[i].SkuTransactionType.StockJournalTypeId, transList[i].SkuNumber, transList[i].Quantity, - transType.DebitStockStatusId, - transType.CreditStockStatusId, - transType.FirstInFirstOut, + transList[i].SkuTransactionType.DebitStockStatusId.GetValueOrDefault(), + transList[i].SkuTransactionType.CreditStockStatusId.GetValueOrDefault(), + transList[i].SkuTransactionType.FirstInFirstOut, transList[i].TransactionDate, false); } else { list = stockReallocate.StockReallocateBySkuNumber( - transType.StockJournalTypeId, + transList[i].SkuTransactionType.StockJournalTypeId, transList[i].SkuNumber, transList[i].Quantity, - transType.DebitStockStatusId, - transType.CreditStockStatusId, - transType.FirstInFirstOut, + transList[i].SkuTransactionType.DebitStockStatusId.GetValueOrDefault(), + transList[i].SkuTransactionType.CreditStockStatusId.GetValueOrDefault(), + transList[i].SkuTransactionType.FirstInFirstOut, DateTime.UtcNow, false); } @@ -196,15 +224,15 @@ namespace bnhtrade.Core.Logic.Stock if (list == null || !list.Any()) { // in special case (found inventory), continue - if (transType.TypeCode.Contains("<_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_>")) + if (transList[i].SkuTransactionType.TypeCode.Contains("<_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_>")) { - continue;// <--------------------------------------------------------------------------------------------------- is this the soruce of the bug? + continue; } else { ProgressMessage = "Insurficent status/location balance to relocate stock"; recordSkip = recordSkip + 1; - goto Stop; + break; } } @@ -260,8 +288,9 @@ namespace bnhtrade.Core.Logic.Stock dbSkuTransaction.Create(newRecordList[j]); } } + ItemsCompleted++; - ItemsRemaining++; + ItemsRemaining--; scope.Complete(); } // end of scope @@ -275,64 +304,87 @@ namespace bnhtrade.Core.Logic.Stock return; } - Stop: + if (ItemsRemaining == 0) + { + ReconciliationComplete = true; + ProgressMessage = "Operation complete."; + } + + //Stop: Console.Write("\r"); - MiscFunction.EventLogInsert("ProcessStockTransactions() compete. " + ItemsCompleted + " total records processed, " + recordSkip + " rows uncompllete due to insurficent stock."); - MiscFunction.EventLogInsert("ProcessStockTransactions(), " + recordSkip + " rows skipped due to insurficent stock.", 2); + MiscFunction.ConsoleUpdate("ProcessStockTransactions() compete. " + ItemsCompleted + " total records processed, " + recordSkip + " rows uncompllete due to insurficent stock."); + MiscFunction.ConsoleUpdate("ProcessStockTransactions(), " + recordSkip + " rows skipped due to insurficent stock."); - ReconciliationComplete = true; - ProgressMessage = "Operation complete."; return; } - /// /// /// - public void ProcessLostAndFound() + public void ReconcileLostAndFound() { using (var scope = new TransactionScope()) { - - // get list of sku transactions to reconcile - var transList = new Data.Database.Stock.ReadSkuTransaction(sqlConnectionString).GetUnreconciled(); - ItemsRemaining = transList.Count; - ItemsCompleted = 0; - // need to loop though table and cancel out any found before they are lost (in reality they were never // lost, therefore should not be entered into journal as lost) string lost = "<_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_><-ve>"; string found = "<_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_><+ve>"; + var codeList = new List(); + codeList.Add(lost); + codeList.Add(found); + + // get list of sku transactions to reconcile + dbSkuTransaction.Init(); + dbSkuTransaction.IsReconciled = false; + dbSkuTransaction.StockTransactionTypeCode = codeList; + var transList = dbSkuTransaction.Read(); + + ItemsRemaining = transList.Count; + ItemsCompleted = 0; + for (int i = 0; i < transList.Count; i++) { - var transType = dbSkuTransactionType.GetByTypeName(transList[i].SkuTransactionTypeName); - - if (transType.TypeCode == found && !transList[i].IsProcessed) + if (transList[i].SkuTransactionTypeCode == found && !transList[i].IsProcessed) { string sku = transList[i].SkuNumber; int foundQty = transList[i].Quantity; int foundQtyUnAllocated = foundQty; - //loop though list and find matching missing + // loop though list and find matching missing for (int j = 0; j < transList.Count; j++) { - // we have a match - if (transList[j].SkuNumber == sku && !transList[j].IsProcessed && transType.TypeCode == lost) + // update the 'lost' transaction + if (transList[j].SkuNumber == sku + && transList[j].IsProcessed == false + && transList[j].SkuTransactionTypeCode == lost) { - // split transaction and break + // split 'lost' transaction if (foundQtyUnAllocated - transList[j].Quantity < 0) { - // create and validate clone + // create 'reconciled' clone var clone = transList[j].Clone(); - clone.Quantity = (short)foundQtyUnAllocated; clone.IsProcessed = true; + clone.Quantity = (short)foundQtyUnAllocated; // modifiy and validate existing record transList[j].IsProcessed = false; transList[j].Quantity = (short)(transList[j].Quantity - foundQtyUnAllocated); - foundQtyUnAllocated = 0; + + // fail safe check + if (clone.IsProcessed) + { + foundQtyUnAllocated -= clone.Quantity; + } + if (transList[j].IsProcessed) + { + foundQtyUnAllocated -= transList[j].Quantity; + } + if (foundQtyUnAllocated != 0) + { + throw new Exception("Unallocated quantity should equal zero."); + } // submitt to database dbSkuTransaction.Create(clone); @@ -353,9 +405,10 @@ namespace bnhtrade.Core.Logic.Stock break; } } - } + } + // drop out of the 'find lost' loop - // update the found record + // update the 'found' record if (foundQty != foundQtyUnAllocated) { // set isprocess = true @@ -367,17 +420,14 @@ namespace bnhtrade.Core.Logic.Stock // split record else if (foundQtyUnAllocated > 0) { - throw new NotImplementedException(); - - // create clone + // create 'reconciled' clone var clone = transList[i].Clone(); - clone.Quantity -= (short)foundQtyUnAllocated; clone.IsProcessed = true; + clone.Quantity = (short)(clone.Quantity - foundQtyUnAllocated); - // modifiy and validate existing record + // modifiy existing record transList[i].IsProcessed = false; - transList[i].Quantity -= (short)foundQtyUnAllocated; - foundQtyUnAllocated = 0; + transList[i].Quantity = (short)foundQtyUnAllocated; // submitt to database dbSkuTransaction.Create(clone); @@ -390,9 +440,15 @@ namespace bnhtrade.Core.Logic.Stock } } } - } + } + // drop out of the 'find found' loop scope.Complete(); } } + + public void UnReconcileTransaction(int skuTransactionId) + { + dbSkuTransaction.DeleteJournalEntry(skuTransactionId); + } } } diff --git a/src/bnhtrade.Core/Logic/Stock/SkuTransactionTypePersistance.cs b/src/bnhtrade.Core/Logic/Stock/SkuTransactionTypePersistance.cs index e93bc41..5fe313c 100644 --- a/src/bnhtrade.Core/Logic/Stock/SkuTransactionTypePersistance.cs +++ b/src/bnhtrade.Core/Logic/Stock/SkuTransactionTypePersistance.cs @@ -16,14 +16,75 @@ namespace bnhtrade.Core.Logic.Stock { this.sqlConnectionString = sqlConnectionString; dbRead = new Data.Database.Stock.ReadSkuTransactionType(sqlConnectionString); - InnitCache(); + CacheInnit(); } - public void InnitCache() + public void CacheInnit() { cache = new List(); } + public void CacheFillByTypeName(List typeNameList) + { + CacheInnit(); + + if (typeNameList == null || !typeNameList.Any()) + { + return; + } + + //fill cache + cache = dbRead.ByTypeName(typeNameList.Distinct().ToList()); + } + + public void CacheFillByTypeCode(List typeCodeList) + { + CacheInnit(); + + if (typeCodeList == null || !typeCodeList.Any()) + { + return; + } + + //fill cache + cache = dbRead.ByTypeCode(typeCodeList.Distinct().ToList()); + } + + /// + /// Using 'SKU Transaction Type Code', adds Sku Transaction Type info to a list of Stock SKU Transactions. + /// + /// Stock SKU Transaction list + /// Force database read + /// Returns false if a 'Type Code' is not found, otherwise true + public bool GetBySkuTransaction(List skuTransactionList) + { + bool allCodesFound = true; + + if (skuTransactionList == null || !skuTransactionList.Any()) + { + return allCodesFound; + } + + CacheFillByTypeCode(skuTransactionList.Select(x => x.SkuTransactionTypeCode).Distinct().ToList()); + for (int i = 0; i < skuTransactionList.Count(); i++) + { + if (skuTransactionList[i].IsSetSkuTransactionTypeCode) + { + var transType = GetByTypeCode(skuTransactionList[i].SkuTransactionTypeCode); + if (transType == null) + { + allCodesFound = false; + } + else + { + skuTransactionList[i].SkuTransactionType = transType; + } + } + } + + return allCodesFound; + } + public Model.Stock.SkuTransactionType GetByTypeCode(string typeCode, bool clearCache = false) { if (string.IsNullOrWhiteSpace(typeCode)) @@ -33,7 +94,7 @@ namespace bnhtrade.Core.Logic.Stock if (clearCache) { - InnitCache(); + CacheInnit(); } else { @@ -68,7 +129,7 @@ namespace bnhtrade.Core.Logic.Stock if (clearCache) { - InnitCache(); + CacheInnit(); } else { diff --git a/src/bnhtrade.Core/Model/Stock/JournalEntry.cs b/src/bnhtrade.Core/Model/Stock/JournalEntry.cs new file mode 100644 index 0000000..d4cb893 --- /dev/null +++ b/src/bnhtrade.Core/Model/Stock/JournalEntry.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace bnhtrade.Core.Model.Stock +{ + public class JournalEntry + { + public string TypeTitle { get; set; } + + public int StockId { get; set; } + + public int StockNumber { get; set; } + + public DateTime EntryDate { get; set; } + + public DateTime PostDate { get; set; } + + public DateTime LastModified { get; set; } + + public bool IsLocked { get; set; } + } +} diff --git a/src/bnhtrade.Core/Model/Stock/JournalEntryPost.cs b/src/bnhtrade.Core/Model/Stock/JournalEntryPost.cs new file mode 100644 index 0000000..c2c9a8d --- /dev/null +++ b/src/bnhtrade.Core/Model/Stock/JournalEntryPost.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace bnhtrade.Core.Model.Stock +{ + public class JournalEntryPost : IValidatableObject + { + public int JournalPostId { get; set; } + + public int StockStatusId { get; set; } + + [Required()] + public string StockStatus { get; set; } + + [Required()] + public int Quantity { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + throw new NotImplementedException(); + + var resultList = new List(); + + if (Quantity == 0) + { + resultList.Add(new ValidationResult("Quantity must be greater than, or less than, zero")); + } + } + } +} diff --git a/src/bnhtrade.Core/Model/Stock/SkuTransaction.cs b/src/bnhtrade.Core/Model/Stock/SkuTransaction.cs index 483d163..0bab62f 100644 --- a/src/bnhtrade.Core/Model/Stock/SkuTransaction.cs +++ b/src/bnhtrade.Core/Model/Stock/SkuTransaction.cs @@ -16,6 +16,8 @@ namespace bnhtrade.Core.Model.Stock private int? skuTransactionId = null; private int? skuTransactionTypeId = null; private int? foreignKey = null; + private string skuTransactionTypeCode; + private Model.Stock.SkuTransactionType skuTransactionType; public int SkuTransactionId { @@ -41,22 +43,60 @@ namespace bnhtrade.Core.Model.Stock } [Required()] - public int SkuTransactionTypeId + public string SkuTransactionTypeCode { - get { return skuTransactionTypeId.GetValueOrDefault(); } - private set { skuTransactionTypeId = value; } + get + { + if (IsSetSkuTransactionType) + { + return SkuTransactionType.TypeCode; + } + else + { + return skuTransactionTypeCode; + } + } + set + { + if (IsSetSkuTransactionType) + { + if (SkuTransactionType.TypeCode != value) + { + SkuTransactionType = null; + skuTransactionTypeCode = value; + } + } + else + { + skuTransactionTypeCode = value; + } + } } - public bool IsSetSkuTransactionTypeId + public bool IsSetSkuTransactionTypeCode { - get { return skuTransactionTypeId != null; } + get { return SkuTransactionTypeCode != null; } } - public string SkuTransactionTypeName { get; private set; } - - public bool IsSetReconcileSkuTypeName + public Model.Stock.SkuTransactionType SkuTransactionType { - get { return SkuTransactionTypeName != null; } + get + { + return skuTransactionType; + } + set + { + if (IsSetSkuTransactionTypeCode) + { + skuTransactionTypeCode = null; + } + skuTransactionType = value; + } + } + + public bool IsSetSkuTransactionType + { + get { return SkuTransactionType != null; } } public int ForeignKey @@ -126,15 +166,6 @@ namespace bnhtrade.Core.Model.Stock get { return stockjournalId != null; } } - public void SetReconcileSkuType(int reconcileSkuTypeId, string reconcileSkuTypeName) - { - if (!string.IsNullOrWhiteSpace(reconcileSkuTypeName) || reconcileSkuTypeId == 0) - { - SkuTransactionTypeId = reconcileSkuTypeId; - SkuTransactionTypeName = reconcileSkuTypeName; - } - } - public SkuTransaction Clone() { var clone = new SkuTransaction(); @@ -145,11 +176,11 @@ namespace bnhtrade.Core.Model.Stock if (IsSetQuantity) { clone.Quantity = this.Quantity; } if (IsSetReference) { clone.Reference = string.Copy(this.Reference); } if (IsSetSkuNumber) { clone.SkuNumber = string.Copy(this.SkuNumber); } - if (IsSetReconcileSkuTypeName) { clone.SkuTransactionTypeName = string.Copy(this.SkuTransactionTypeName); } if (IsSetStockJournalId) { clone.StockJournalId = this.StockJournalId; } if (IsSetSkuTransactionId) { clone.SkuTransactionId = this.SkuTransactionId; } - if (IsSetSkuTransactionTypeId) { clone.SkuTransactionTypeId = this.SkuTransactionTypeId; } if (IsSetTransactionDate) { clone.TransactionDate = this.TransactionDate; } + if (IsSetSkuTransactionType) { clone.SkuTransactionType = this.SkuTransactionType; } + else if (IsSetSkuTransactionTypeCode) { clone.SkuTransactionTypeCode = string.Copy(this.SkuTransactionTypeCode); } return clone; } @@ -174,10 +205,6 @@ namespace bnhtrade.Core.Model.Stock { result.Add(new ValidationResult("Stock Transaction Id is not set")); } - if (!IsSetSkuTransactionTypeId) - { - result.Add(new ValidationResult("Stock Transaction TypeId is not set")); - } if (IsSetStockJournalId && (!IsSetIsProcessed || IsProcessed == false)) { result.Add(new ValidationResult("Stock Journal Id is set, IsProcessed must be set to true")); diff --git a/src/bnhtrade.Core/Model/Stock/SkuTransactionType.cs b/src/bnhtrade.Core/Model/Stock/SkuTransactionType.cs index b9eac33..1e51fea 100644 --- a/src/bnhtrade.Core/Model/Stock/SkuTransactionType.cs +++ b/src/bnhtrade.Core/Model/Stock/SkuTransactionType.cs @@ -46,11 +46,11 @@ namespace bnhtrade.Core.Model.Stock public bool StockJournalEntryEnabled { get; set; } - public int DebitStockStatusId { get; set; } + public int? DebitStockStatusId { get; set; } public string DebitStockStatus { get; set; } - public int CreditStockStatusId { get; set; } + public int? CreditStockStatusId { get; set; } public string CreditStockStatus { get; set; } @@ -58,6 +58,6 @@ namespace bnhtrade.Core.Model.Stock public bool FilterStockOnDateTime { get; set; } - public bool FirstInFirstOut { get; set; } + public bool FirstInFirstOut { get; set; } // TODO: move FIFO rule to 'Stock Status' } } diff --git a/src/bnhtrade.Core/Program.cs b/src/bnhtrade.Core/Program.cs index 82321dd..2e8c133 100644 --- a/src/bnhtrade.Core/Program.cs +++ b/src/bnhtrade.Core/Program.cs @@ -23,6 +23,7 @@ using bnhtrade.Core.AmazonAPI; using bnhtrade.Core.Order; using bnhtrade.Core.Data.AmazonMWS; using bnhtrade.Core.Logic.Sku; +using bnhtrade.Core.Data.Database.Log; namespace bnhtrade.Core { @@ -2109,9 +2110,9 @@ namespace bnhtrade.Core * level transaction scope, this nested dispose() also rolls back the all transacopes scopes it is a child of. * * Therefore, a consistancy check needs to be simulated in code to negate the need to rollback/dispose of a db transaction. - * This would also have ome slight performance benefits. + * This would also have some slight performance benefits. * - * Once you've done this, fix the SkuTransactionReconcile class: Currently it's transactionscope onyl covers updates. + * Once you've done this, fix the SkuTransactionReconcile class: Currently it's transactionscope only covers updates. * Need to set the scope to cover the intial table read (to lock the records). The issue above restricts this. */ @@ -2846,7 +2847,7 @@ namespace bnhtrade.Core } } - public static int StockTransactionTypeIdInsert(string sqlConnectionString, int stockJournalTypeId, string matchString, bool transScopeSuppress = true) + public static int StockTransactionTypeIdInsert(string sqlConnectionString, int stockJournalTypeId, string typeCode, bool transScopeSuppress = true) { int transactionTypeId; var scopeOption = new TransactionScopeOption(); @@ -2871,10 +2872,10 @@ namespace bnhtrade.Core tblStockSkuTransactionType WHERE tblStockSkuTransactionType.StockJournalTypeID=@stockJournalTypeId - AND tblStockSkuTransactionType.MatchString=@matchString; + AND tblStockSkuTransactionType.TypeCode=@typeCode; ", conn)) { - cmd.Parameters.AddWithValue("@matchString", matchString); + cmd.Parameters.AddWithValue("@typeCode", typeCode); cmd.Parameters.AddWithValue("@stockJournalTypeId", stockJournalTypeId); object obj = cmd.ExecuteScalar(); @@ -2888,14 +2889,15 @@ namespace bnhtrade.Core // insert new and retrive new value using (SqlCommand cmd = new SqlCommand(@" INSERT INTO tblStockSkuTransactionType - ( StockJournalTypeID, MatchString ) + ( TypeName, StockJournalTypeID, TypeCode ) OUTPUT INSERTED.StockSkuTransactionTypeID VALUES - ( @stockJournalTypeId, @matchString ); + ( @typeName, @stockJournalTypeId, @typeCode ); ", conn)) { - cmd.Parameters.AddWithValue("@matchString", matchString); + cmd.Parameters.AddWithValue("@typeName", typeCode); + cmd.Parameters.AddWithValue("@typeCode", typeCode); cmd.Parameters.AddWithValue("@stockJournalTypeId", stockJournalTypeId); transactionTypeId = (int)cmd.ExecuteScalar(); @@ -6914,6 +6916,11 @@ namespace bnhtrade.Core int columnCount = headers.Length; + if (columnCount != 38) + { + throw new Exception("Chnages found to 'Fba Inventory Age Data' flatfile structure"); + } + int index01 = Array.IndexOf(headers, "snapshot-date"); int index02 = Array.IndexOf(headers, "marketplace"); int index03 = Array.IndexOf(headers, "sku"); @@ -6947,7 +6954,8 @@ namespace bnhtrade.Core int index31 = Array.IndexOf(headers, "Recommended sales price"); int index32 = Array.IndexOf(headers, "Recommended sale duration (days)"); int index33 = Array.IndexOf(headers, "Recommended Removal Quantity"); - int index34 = Array.IndexOf(headers, "Estimated cost savings of removal"); + int index34 = Array.IndexOf(headers, "estimated-cost-savings-of-recommended-actions"); + int index35 = Array.IndexOf(headers, "sell-through"); string fileRow; while ((fileRow = reader.ReadLine()) != null) @@ -7002,7 +7010,8 @@ namespace bnhtrade.Core string recommendedSalesPrice = Regex.Replace(items[index31], "[^.0-9]", ""); // strip currency code prefix string recommendedSaleDuration = items[index32]; string recommendedRemovalQuantity = items[index33]; - string estimatedCostSavingsOfRemoval = items[index34]; + string estimatedCostSavingsOfRecommendedActions = items[index34]; + string sellThrough = items[index35]; using (SqlCommand cmd = new SqlCommand(@" INSERT INTO tblImportFbaInventoryAgeReport( @@ -7012,13 +7021,15 @@ namespace bnhtrade.Core [qty-to-be-charged-ltsf-12-mo], [projected-ltsf-12-mo], [units-shipped-last-7-days], [units-shipped-last-30-days], [units-shipped-last-60-days], [units-shipped-last-90-days], alert, [your-price], sales_price, lowest_price_new, lowest_price_used, [Recommended action], [Healthy Inventory Level], [Recommended sales price], - [Recommended sale duration (days)], [Recommended Removal Quantity], [Estimated cost savings of removal] ) + [Recommended sale duration (days)], [Recommended Removal Quantity], [estimated-cost-savings-of-recommended-actions], + [sell-through] ) VALUES ( @snapshotDate, @marketplace, @sku, @fnsku, @asin, @productName, @condition, @avaliableQuantity, @qtyWithRemovalsInProgress, @invAge0To90Days, @invAge91To180Days, @invAge181To270Days, @invAge271To365Days, @invAge365PlusDays, @currency, @qtyToBeChargedLtsf6Mo, @projectedLtsf6Mo, @qtyToBeChargedLtsf12Mo, @projectedLtsf12Mo, @unitsShippedLast7Days, @unitsShippedLast30Days, @unitsShippedLast60Days, @unitsShippedLast90Days, @alert, - @yourPrice, @salesPrice, @lowestPriceNew, @lowestPriceUsed, @recommendedAction, @healthyInventoryLevel, @recommendedSalesPrice, @recommendedSaleDuration, - @recommendedRemovalQuantity, @estimatedCostSavingsOfRemoval ); + @yourPrice, @salesPrice, @lowestPriceNew, @lowestPriceUsed, @recommendedAction, @healthyInventoryLevel, @recommendedSalesPrice, + @recommendedSaleDuration, @recommendedRemovalQuantity, @estimatedCostSavingsOfRecommendedActions, + @sellThrough ); ", sqlConn)) { // add parameters @@ -7121,8 +7132,11 @@ namespace bnhtrade.Core if (recommendedRemovalQuantity.Length == 0) { cmd.Parameters.AddWithValue("@recommendedRemovalQuantity", DBNull.Value); } else { cmd.Parameters.AddWithValue("@recommendedRemovalQuantity", int.Parse(recommendedRemovalQuantity)); } - if (estimatedCostSavingsOfRemoval.Length == 0) { cmd.Parameters.AddWithValue("@estimatedCostSavingsOfRemoval", DBNull.Value); } - else { cmd.Parameters.AddWithValue("@estimatedCostSavingsOfRemoval", decimal.Parse(estimatedCostSavingsOfRemoval)); } + if (estimatedCostSavingsOfRecommendedActions.Length == 0) { cmd.Parameters.AddWithValue("@estimatedCostSavingsOfRemoval", DBNull.Value); } + else { cmd.Parameters.AddWithValue("@estimatedCostSavingsOfRecommendedActions", decimal.Parse(estimatedCostSavingsOfRecommendedActions)); } + + if (sellThrough.Length == 0) { cmd.Parameters.AddWithValue("@sellThrough", DBNull.Value); } + else { cmd.Parameters.AddWithValue("@sellThrough", decimal.Parse(sellThrough)); } // execute the query cmd.ExecuteNonQuery(); diff --git a/src/bnhtrade.Core/bnhtrade.Core.csproj b/src/bnhtrade.Core/bnhtrade.Core.csproj index 310cfd0..1db28b1 100644 --- a/src/bnhtrade.Core/bnhtrade.Core.csproj +++ b/src/bnhtrade.Core/bnhtrade.Core.csproj @@ -115,7 +115,7 @@ - + @@ -160,6 +160,8 @@ + + diff --git a/src/bnhtrade.ScheduledTasks/Program.cs b/src/bnhtrade.ScheduledTasks/Program.cs index 95ee4bb..7b29c21 100644 --- a/src/bnhtrade.ScheduledTasks/Program.cs +++ b/src/bnhtrade.ScheduledTasks/Program.cs @@ -291,7 +291,7 @@ namespace bnhtradeScheduledTasks Console.WriteLine("<6> Test SKU"); Console.WriteLine("<7> Test xxxxxxx"); Console.WriteLine("<8> Test xxxxxxx"); - Console.WriteLine("<9> Detele Sku Transaction"); + Console.WriteLine("<9> Detele Sku Transaction 'n'"); Console.WriteLine(); Console.WriteLine("<0> Back"); Console.WriteLine(""); @@ -386,7 +386,7 @@ namespace bnhtradeScheduledTasks { Console.Clear(); - new bnhtrade.Core.Logic.Stock.SkuTransactionPersistance(sqlConnectionString).DeleteJournalEntry(80774); + new bnhtrade.Core.Logic.Stock.SkuTransactionPersistance(sqlConnectionString).DeleteJournalEntry(32731); Console.WriteLine("Done"); Console.WriteLine("Complete, press any key to continue...");