SP-API stock reconciliation

Amazon had depreciated a number of reports that were used for stock reconciliation. Application now uses the new fba ledger report to reconcile. It is currently untested, as this requires data from Amazon. Methods that require testing will return a 'NotImplementedException'.

Also, removed the depreciated ILMerge and replaced with ILRepack.

Plus much more tidying up, and improvements.
This commit is contained in:
Bobbie Hodgetts
2024-05-07 08:24:00 +01:00
committed by GitHub
parent 2f919d7b5a
commit 91ef9acc78
1272 changed files with 4944 additions and 2773311 deletions

View File

@@ -1,33 +1,31 @@
using System;
using bnhtrade.Core.Data.Database;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
using System.Web.UI;
namespace bnhtrade.Core.Logic.Stock
{
public class SkuTransactionReconcile
{
private string sqlConnectionString;
private Data.Database.AmazonShipment.ReadShipmentInfo readShipmentInfo;
private Logic.Stock.SkuTransactionPersistance dbSkuTransaction;
private Logic.Stock.SkuTransactionTypePersistance dbSkuTransactionType;
private Logic.Stock.SkuTransactionCrud dbSkuTransaction;
private Logic.Validate.SkuTransaction validateSkuTrans;
private Logic.Stock.StatusReallocate stockReallocate;
private Logic.Log.LogEvent logEvent;
private Logic.Log.LogEvent log;
private string err = "Reconcile Sku Transaction Exception: ";
public SkuTransactionReconcile(string sqlConnectionString)
public SkuTransactionReconcile()
{
Innit();
this.sqlConnectionString = sqlConnectionString;
dbSkuTransaction = new SkuTransactionPersistance(sqlConnectionString);
dbSkuTransactionType = new SkuTransactionTypePersistance(sqlConnectionString);
dbSkuTransaction = new SkuTransactionCrud();
readShipmentInfo = new Data.Database.AmazonShipment.ReadShipmentInfo();
validateSkuTrans = new Validate.SkuTransaction();
stockReallocate = new Logic.Stock.StatusReallocate(sqlConnectionString);
logEvent = new Log.LogEvent();
stockReallocate = new Logic.Stock.StatusReallocate();
log = new Log.LogEvent();
}
public int ItemsCompleted { get; private set; }
@@ -58,12 +56,14 @@ namespace bnhtrade.Core.Logic.Stock
}
/// <summary>
/// Iterates through the stock transaction table and inserts stock journal entries, where applicable
/// N.B. This function does not make allowances for status' that can create stock (i.e. if a status does not have stock available, this function will halt processing rows)
/// Iterates through the stock transaction table and creates stock journal entries, if required
/// N.B. This function does not make allowances for status' that can create stock (i.e. if a status does not have stock available,
/// this function will halt processing rows)
/// </summary>
/// <param name="updateTransactions">Download and process Amazon reports before starting process</param>
/// <param name="updateTransactions">Process Amazon reports before starting process</param>
/// <param name="downloadAmaozn">Download reports from Amazon before starting process</param>
/// <returns></returns>
public void ReconcileStockTransactions(bool updateTransactions)
public void ReconcileStockTransactions(bool updateTransactions, bool downloadAmazon = false)
{
Innit();
string currentMethodName = nameof(ReconcileStockTransactions);
@@ -73,8 +73,12 @@ namespace bnhtrade.Core.Logic.Stock
{
try
{
var preCheck = new bnhtrade.Core.Stock.StockReconciliation();
preCheck.ProcessFbaStockImportData(sqlConnectionString);
if (downloadAmazon)
{
new bnhtrade.Core.Logic.Import.AmazonFbaReimbursement().SyncDatabaseWithAmazon();
new bnhtrade.Core.Logic.Import.AmazonFbaInventoryLedgerDetail().SyncDatabaseWithAmazon();
}
new bnhtrade.Core.Logic.Stock.SkuTransactionImport().ImportAll();
}
catch (Exception ex)
{
@@ -83,10 +87,10 @@ namespace bnhtrade.Core.Logic.Stock
}
}
logEvent.LogInformation("Starting ReconcileStockTransactions()");
log.LogInformation("Starting ReconcileStockTransactions()");
int recordSkip = 0;
ReconcileLostAndFound();
ReconcileFoundAndLost();
// get list of sku transactions to reconcile
dbSkuTransaction.Init();
@@ -96,27 +100,37 @@ namespace bnhtrade.Core.Logic.Stock
ItemsRemaining = transList.Count;
ItemsCompleted = 0;
// get list of sku transaction types
var codeList = new List<string>();
foreach(var item in transList)
{
codeList.Add(item.SkuTransactionTypeCode);
}
var transTypeDict = new Logic.Stock.SkuTransactionTypeCrud().GetByTypeCode(codeList);
try
{
// loop through transaction list
for (int i = 0; i < transList.Count; i++)
//for (int i = 0; i < transList.Count; i++)
foreach (var transaction in transList)
{
using (var scope = new TransactionScope())
{
Console.Write("\rProcessing record: {0} ({1} skipped)", (i + 1 + recordSkip), recordSkip);
Console.Write("\rProcessing record: {0} ({1} skipped)", (ItemsCompleted + 1 + recordSkip), recordSkip);
// setup return values
CurrentSkuTransaction = transList[i];
CurrentTransactionId = transList[i].SkuTransactionId;
CurrentTransactionTypeCode = transList[i].SkuTransactionType.TypeCode;
LastItemDateTime = transList[i].TransactionDate;
CurrentSkuTransaction = transaction;
CurrentTransactionId = transaction.SkuTransactionId;
CurrentTransactionTypeCode = transaction.SkuTransactionTypeCode;
LastItemDateTime = transaction.TransactionDate;
// load type into variable
//var transType = dbSkuTransactionType.GetByTypeName(transList[i].SkuTransactionTypeName);
// setup variables
var transactionType = transTypeDict[transaction.SkuTransactionTypeCode].Clone();
// stop if a new transactiontype is encountered
if (transList[i].SkuTransactionType.IsNewReviewRequired)
if (transactionType.IsNewReviewRequired)
{
ProgressMessage = "New 'Transaction-Type' encountered";
//Console.Write("\r");
@@ -124,51 +138,52 @@ namespace bnhtrade.Core.Logic.Stock
break;
}
else if (transList[i].SkuTransactionType.StockJournalEntryEnabled == false)
else if (transactionType.StockJournalEntryEnabled == false)
{
transList[i].IsProcessed = true;
dbSkuTransaction.Update(transList[i]);
//transList[i].IsProcessed = true;
ReconcileTransaction(transaction.SkuTransactionId);
}
// stock journal entry is enabled
else
{
// check debit/credits
if (transList[i].SkuTransactionType.DebitStockStatusId.GetValueOrDefault() == 0
|| transList[i].SkuTransactionType.CreditStockStatusId.GetValueOrDefault() == 0)
if (transactionType.DebitStockStatusId.GetValueOrDefault() == 0
|| transactionType.CreditStockStatusId.GetValueOrDefault() == 0)
{
// special case FBA Shipment Receipt +ve
if (transList[i].SkuTransactionType.TypeCode == "<AmazonReport><_GET_FBA_FULFILLMENT_INVENTORY_RECEIPTS_DATA_><+ve>"
|| transList[i].SkuTransactionType.TypeCode == "<AmazonReport><_GET_FBA_FULFILLMENT_INVENTORY_RECEIPTS_DATA_><-ve>")
if (transactionType.StockJournalTypeId == (int)Constants.StockJournalType.SkuReconciliationFbaReceipt)
//transactionType.TypeCode == "<AmazonReport><GET_LEDGER_DETAIL_VIEW_DATA><Receipt><+ve>"
//|| transactionType.TypeCode == "<AmazonReport><GET_LEDGER_DETAIL_VIEW_DATA><Receipt><-ve>")
{
Model.AmazonFba.ShipmentInfo shipmentInfo = null;
if (!shipmentInfoDic.ContainsKey(transList[i].Reference))
if (!shipmentInfoDic.ContainsKey(transaction.Reference))
{
shipmentInfo = readShipmentInfo.HeaderByFbaShipmentId(transList[i].Reference);
shipmentInfo = readShipmentInfo.HeaderByFbaShipmentId(transaction.Reference);
if (shipmentInfo == null)
{
throw new Exception("Unable to retrive shipment info for reference '" + transList[i].Reference + "'.");
throw new Exception("Unable to retrive shipment info for reference '" + transaction.Reference + "'.");
}
else
{
shipmentInfoDic.Add(transList[i].Reference, shipmentInfo);
shipmentInfoDic.Add(transaction.Reference, shipmentInfo);
}
}
if (shipmentInfo.IsSetShipmentStockStatusId())
{
// +ve shipment receipt
if (transList[i].SkuTransactionType.CreditStockStatusId == null
&& transList[i].SkuTransactionType.DebitStockStatusId > 0)
if (transactionType.CreditStockStatusId == null
&& transactionType.DebitStockStatusId > 0)
{
transList[i].SkuTransactionType.CreditStockStatusId = shipmentInfo.ShipmentStockStatusId;
transactionType.CreditStockStatusId = shipmentInfo.ShipmentStockStatusId;
}
// -ve shipment receipt
else if (transList[i].SkuTransactionType.DebitStockStatusId == null
&& transList[i].SkuTransactionType.CreditStockStatusId > 0)
else if (transactionType.DebitStockStatusId == null
&& transactionType.CreditStockStatusId > 0)
{
transList[i].SkuTransactionType.DebitStockStatusId = shipmentInfo.ShipmentStockStatusId;
transactionType.DebitStockStatusId = shipmentInfo.ShipmentStockStatusId;
}
// something went wrong, raise error
@@ -195,28 +210,28 @@ namespace bnhtrade.Core.Logic.Stock
// make the journal entries
var journalList = new List<(int StockJournalId, int Quantity)>();
if (transList[i].SkuTransactionType.FilterStockOnDateTime)
if (transactionType.FilterStockOnDateTime)
{
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,
transaction.TransactionDate,
transactionType.StockJournalTypeId,
transaction.SkuNumber,
transaction.Quantity,
transactionType.DebitStockStatusId.GetValueOrDefault(),
transactionType.CreditStockStatusId.GetValueOrDefault(),
transactionType.FirstInFirstOut,
true);
}
else
{
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,
transactionType.StockJournalTypeId,
transaction.SkuNumber,
transaction.Quantity,
transactionType.DebitStockStatusId.GetValueOrDefault(),
transactionType.CreditStockStatusId.GetValueOrDefault(),
transactionType.FirstInFirstOut,
true);
}
@@ -224,7 +239,8 @@ namespace bnhtrade.Core.Logic.Stock
if (!journalList.Any())
{
// in special case (found inventory), continue
if (transList[i].SkuTransactionType.TypeCode.Contains("<AmazonReport><_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_><F>"))
if (transactionType.TypeCode.Contains("<AmazonReport><GET_LEDGER_DETAIL_VIEW_DATA><Adjustment><F>")
|| transactionType.TypeCode.Contains("<AmazonReport><GET_LEDGER_DETAIL_VIEW_DATA><Adjustment><N>"))
{
ItemsCompleted++;
ItemsRemaining--;
@@ -239,56 +255,61 @@ namespace bnhtrade.Core.Logic.Stock
}
// fail safe
int qtyAllocated = journalList.Sum(c => c.Quantity);
if (qtyAllocated > transList[i].Quantity)
int qtyUnallocated = journalList.Sum(c => c.Quantity);
if (qtyUnallocated > transaction.Quantity)
{
throw new Exception(
currentMethodName + ": StockReallocateBySkuId() returned greater quantity than passed to function"
+ transList[i].SkuTransactionId);
+ transaction.SkuTransactionId);
}
// update sku transaction table
int qtyRemain = qtyAllocated;
var newRecordList = new List<Model.Stock.SkuTransaction>();
for (int j = 0; j < journalList.Count; j++)
{
// update existing record
// update existing record to reconciled
if (j == 0)
{
transList[i].Quantity = (short)journalList[j].Quantity;
transList[i].StockJournalId = journalList[j].StockJournalId;
transList[i].IsProcessed = true;
dbSkuTransaction.UpdateQuanitity(transaction.SkuTransactionId, journalList[j].Quantity);
dbSkuTransaction.UpdateIsProcessed(transaction.SkuTransactionId, true, journalList[j].StockJournalId);
dbSkuTransaction.Update(transList[i]);
}
// new record
// create new reconciled record
else
{
var newRecord = transList[i].Clone();
newRecord.Quantity = (short)journalList[j].Quantity;
newRecord.IsProcessed = true;
newRecord.StockJournalId = journalList[j].StockJournalId;
newRecordList.Add(newRecord);
var newTransaction = new Model.Stock.SkuTransactionCreate(
transaction.TransactionDate
, transaction.SkuTransactionTypeCode
, transaction.ForeignKey
, transaction.Reference
, transaction.Detail
, transaction.SkuNumber
, journalList[j].Quantity
);
int newId = dbSkuTransaction.Create(newTransaction);
dbSkuTransaction.UpdateIsProcessed(newId, true, journalList[j].StockJournalId);
}
qtyRemain = qtyRemain - journalList[j].Quantity;
qtyUnallocated = qtyUnallocated - journalList[j].Quantity;
}
// new record for unallocated quantity
if (qtyRemain > 0)
// create new record for unreconciled quantity
if (qtyUnallocated > 0)
{
var newRecord = transList[i].Clone();
newRecord.Quantity = (short)qtyRemain;
newRecord.IsProcessed = false;
var newTransaction = new Model.Stock.SkuTransactionCreate(
transaction.TransactionDate
, transaction.SkuTransactionTypeCode
, transaction.ForeignKey
, transaction.Reference
, transaction.Detail
, transaction.SkuNumber
, qtyUnallocated
);
int newId = dbSkuTransaction.Create(newTransaction);
newRecordList.Add(newRecord);
}
// add new transactions to table
for (int j = 0; j < newRecordList.Count; j++)
{
dbSkuTransaction.Create(newRecordList[j]);
ProgressMessage = "Transaction could not be fully reconcoiled. Unallocated quanity remaing:" + qtyUnallocated;
recordSkip = recordSkip + 1;
break;
}
}
@@ -325,144 +346,146 @@ namespace bnhtrade.Core.Logic.Stock
/// <summary>
///
/// </summary>
public void ReconcileLostAndFound()
public void ReconcileFoundAndLost()
{
/* Amazon can find a item before they lost it. In this senario the found item will not reconcile. Even when the lost transaction comes
* though, it will be dated after the found transaction, so it also will not reconcile.
*
* This method tackles this. Instead of journal entries for found before lost, just cancel then out (set IsProcessed=true) for
* both transactions.
*
* In the main reconcile method, if the porcdure hits an unreconciable found transaction, it will ignore it and carry on. Everything will still
* tally at my end, even if Amazon thinks i've got more than I actually have
*/
using (var scope = new TransactionScope())
{
// 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 = "<AmazonReport><_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_><M><-ve><InventoryMisplaced><SELLABLE>";
string found = "<AmazonReport><_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_><F><+ve><InventoryFound><SELLABLE>";
var codeList = new List<string>();
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++)
try
{
if (transList[i].SkuTransactionTypeCode == found && !transList[i].IsProcessed)
var lostQueryString = new List<string>();
lostQueryString.Add("<AmazonReport><GET_LEDGER_DETAIL_VIEW_DATA><Adjustments><M><-ve><SELLABLE>");
var foundQueryString = new List<string>();
foundQueryString.Add("<AmazonReport><GET_LEDGER_DETAIL_VIEW_DATA><Adjustments><F><+ve><SELLABLE>");
dbSkuTransaction.Init();
dbSkuTransaction.IsReconciled = false;
dbSkuTransaction.StockTransactionTypeCode = lostQueryString;
var lostList = dbSkuTransaction.Read();
dbSkuTransaction.Init();
dbSkuTransaction.IsReconciled = false;
dbSkuTransaction.StockTransactionTypeCode = foundQueryString;
var foundList = dbSkuTransaction.Read();
ItemsRemaining = foundList.Count;
ItemsCompleted = 0;
for (int i = 0; i < foundList.Count; i++)
{
string sku = transList[i].SkuNumber;
int foundQty = transList[i].Quantity;
int foundQtyUnAllocated = foundQty;
// loop though list and find matching missing
for (int j = 0; j < transList.Count; j++)
for (int j = 0; j < lostList.Count; j++)
{
// update the 'lost' transaction
if (transList[j].SkuNumber == sku
&& transList[j].IsProcessed == false
&& transList[j].SkuTransactionTypeCode == lost)
if (foundList[i].SkuNumber == lostList[j].SkuNumber)
{
// split 'lost' transaction
if (foundQtyUnAllocated - transList[j].Quantity < 0)
// notes. isProcessed items need to be removed from the 'lost' list as we'll be looping over this multiple times, the
// 'found' list we only loop though once
//
// when an sku match is found there is only 3 possible routes
// 1. Lost == Found
if (foundList[i].Quantity == lostList[j].Quantity)
{
// create 'reconciled' clone
var clone = transList[j].Clone();
clone.IsProcessed = true;
clone.Quantity = (short)foundQtyUnAllocated;
// mark reconciled transaction as isProcessed
dbSkuTransaction.UpdateIsProcessed(foundList[i].SkuTransactionId, true, null);
dbSkuTransaction.UpdateIsProcessed(lostList[j].SkuTransactionId, true, null);
// modifiy and validate existing record
transList[j].IsProcessed = false;
transList[j].Quantity = (short)(transList[j].Quantity - foundQtyUnAllocated);
// delete reconciled from 'lost' list
lostList.RemoveAt(j);
// 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);
dbSkuTransaction.Update(transList[j]);
}
// set as isprocessed and continue
else
{
foundQtyUnAllocated = foundQtyUnAllocated - transList[j].Quantity;
transList[j].IsProcessed = true;
dbSkuTransaction.Update(transList[j]);
}
// break?
if (foundQtyUnAllocated == 0)
{
ItemsCompleted++;
break;
}
}
}
// drop out of the 'find lost' loop
// 2. Lost < Found
else if (foundList[i].Quantity < lostList[j].Quantity)
{
// split the lost transaction
int newTransId = dbSkuTransaction.SplitTransaction(lostList[j].SkuTransactionId, foundList[i].Quantity);
// update the 'found' record
if (foundQty != foundQtyUnAllocated)
{
// set isprocess = true
if (foundQtyUnAllocated == 0)
{
transList[i].IsProcessed = true;
dbSkuTransaction.Update(transList[i]);
}
// split record
else if (foundQtyUnAllocated > 0)
{
// create 'reconciled' clone
var clone = transList[i].Clone();
clone.IsProcessed = true;
clone.Quantity = (short)(clone.Quantity - foundQtyUnAllocated);
// mark reconciled transaction as isProcessed
dbSkuTransaction.UpdateIsProcessed(foundList[i].SkuTransactionId, true, null);
dbSkuTransaction.UpdateIsProcessed(lostList[j].SkuTransactionId, true, null);
// modifiy existing record
transList[i].IsProcessed = false;
transList[i].Quantity = (short)foundQtyUnAllocated;
// delete reconciled from 'lost' list
lostList.RemoveAt(j);
// submitt to database
dbSkuTransaction.Create(clone);
dbSkuTransaction.Update(transList[i]);
}
// this shouldn't happen
else
{
throw new Exception("Quantity unallocated is negative number");
// retrieve the new split transaction
var newTransactionList = dbSkuTransaction.Read(new List<int> { newTransId });
if (!newTransactionList.Any())
throw new Exception("Something went wrong, this should not happen.");
var newTransaction = newTransactionList[0];
// insert new split 'lost' record into the list
lostList.Insert(j, newTransaction);
ItemsCompleted++;
break;
}
// 3. Lost > Found
else if (foundList[i].Quantity > lostList[j].Quantity)
{
// split the found transaction
int newTransId = dbSkuTransaction.SplitTransaction(lostList[i].SkuTransactionId, foundList[j].Quantity);
// mark reconciled transaction as isProcessed
dbSkuTransaction.UpdateIsProcessed(foundList[i].SkuTransactionId, true, null);
dbSkuTransaction.UpdateIsProcessed(lostList[j].SkuTransactionId, true, null);
// delete reconciled from 'lost' list
lostList.RemoveAt(j);
// retrive the new split transaction
var newTransactionList = dbSkuTransaction.Read(new List<int> { newTransId });
if (!newTransactionList.Any())
throw new Exception("Something went wrong, this should not happen.");
var newTransaction = newTransactionList[0];
// // insert new split 'found' record into the list, in the next position
foundList.Insert((i + 1), newTransaction);
ItemsCompleted++;
ItemsRemaining++;
break;
}
else
{
throw new Exception("Something went wrong, this should not happen.");
}
}
}
}
}
// drop out of the 'find found' loop
scope.Complete();
scope.Complete();
}
catch (Exception ex)
{
scope.Dispose();
Console.Write("\r");
ProgressMessage = ex.Message;
return;
}
}
}
/// <summary>
/// Marks the transaction as reconciled/isprocessed with no stock journal entry required
/// </summary>
/// <param name="skuTransactionId"></param>
public void ReconcileTransaction(int skuTransactionId)
{
new Data.Database.Stock.UpdateSkuTransaction().UpdateIsProcessed(skuTransactionId, true, null);
}
public void UnReconcileTransaction(int 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().Update(skuTransactionId, false);
}
new Data.Database.Stock.UpdateSkuTransaction().UpdateIsProcessed(skuTransactionId, false, null, true);
}
}
}