mirror of
https://github.com/stokebob/bnhtrade.git
synced 2026-03-19 06:27:15 +00:00
491 lines
24 KiB
C#
491 lines
24 KiB
C#
using bnhtrade.Core.Data.Database;
|
|
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 SkuTransactionReconcile
|
|
{
|
|
private Data.Database.AmazonShipment.ReadShipmentInfo readShipmentInfo;
|
|
private Logic.Stock.SkuTransactionCrud dbSkuTransaction;
|
|
private Logic.Validate.SkuTransaction validateSkuTrans;
|
|
private Logic.Stock.StatusReallocate stockReallocate;
|
|
private Logic.Log.LogEvent log;
|
|
private string err = "Reconcile Sku Transaction Exception: ";
|
|
|
|
public SkuTransactionReconcile()
|
|
{
|
|
Innit();
|
|
dbSkuTransaction = new SkuTransactionCrud();
|
|
readShipmentInfo = new Data.Database.AmazonShipment.ReadShipmentInfo();
|
|
validateSkuTrans = new Validate.SkuTransaction();
|
|
stockReallocate = new Logic.Stock.StatusReallocate();
|
|
log = new Log.LogEvent();
|
|
}
|
|
|
|
public int ItemsCompleted { get; private set; }
|
|
|
|
public int ItemsRemaining { get; private set; }
|
|
|
|
public DateTime LastItemDateTime { get; private set; }
|
|
|
|
public string ProgressMessage { get; private set; }
|
|
|
|
public bool ReconciliationComplete { get; private set; }
|
|
|
|
public int CurrentTransactionId { get; private set; }
|
|
|
|
public string CurrentTransactionTypeCode { get; private set; }
|
|
|
|
public Model.Stock.SkuTransaction CurrentSkuTransaction { get; private set; }
|
|
|
|
private void Innit()
|
|
{
|
|
ItemsCompleted = 0;
|
|
ItemsRemaining = 0;
|
|
LastItemDateTime = default(DateTime);
|
|
ProgressMessage = null;
|
|
ReconciliationComplete = false;
|
|
CurrentTransactionId = 0;
|
|
CurrentTransactionTypeCode = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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">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, bool downloadAmazon = false)
|
|
{
|
|
Innit();
|
|
string currentMethodName = nameof(ReconcileStockTransactions);
|
|
|
|
// ensure import table have been processed into transaction table without exception
|
|
if (updateTransactions == true)
|
|
{
|
|
try
|
|
{
|
|
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)
|
|
{
|
|
ProgressMessage = "Precheck failed: " + ex.Message;
|
|
return;
|
|
}
|
|
}
|
|
|
|
log.LogInformation("Starting ReconcileStockTransactions()");
|
|
int recordSkip = 0;
|
|
|
|
ReconcileFoundAndLost();
|
|
|
|
// get list of sku transactions to reconcile
|
|
dbSkuTransaction.Init();
|
|
dbSkuTransaction.IsReconciled = false;
|
|
var transList = dbSkuTransaction.Read();
|
|
var shipmentInfoDic = new Dictionary<string, Model.AmazonFba.ShipmentInfo>();
|
|
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++)
|
|
foreach (var transaction in transList)
|
|
{
|
|
using (var scope = new TransactionScope())
|
|
{
|
|
|
|
Console.Write("\rProcessing record: {0} ({1} skipped)", (ItemsCompleted + 1 + recordSkip), recordSkip);
|
|
|
|
// setup return values
|
|
CurrentSkuTransaction = transaction;
|
|
CurrentTransactionId = transaction.SkuTransactionId;
|
|
CurrentTransactionTypeCode = transaction.SkuTransactionTypeCode;
|
|
LastItemDateTime = transaction.TransactionDate;
|
|
|
|
// setup variables
|
|
var transactionType = transTypeDict[transaction.SkuTransactionTypeCode].Clone();
|
|
|
|
// stop if a new transactiontype is encountered
|
|
if (transactionType.IsNewReviewRequired)
|
|
{
|
|
ProgressMessage = "New 'Transaction-Type' encountered";
|
|
//Console.Write("\r");
|
|
//new Logic.Log.LogEvent().EventLogInsert(errMessage, 1);
|
|
break;
|
|
}
|
|
|
|
else if (transactionType.StockJournalEntryEnabled == false)
|
|
{
|
|
//transList[i].IsProcessed = true;
|
|
ReconcileTransaction(transaction.SkuTransactionId);
|
|
}
|
|
|
|
// stock journal entry is enabled
|
|
else
|
|
{
|
|
// check debit/credits
|
|
if (transactionType.DebitStockStatusId.GetValueOrDefault() == 0
|
|
|| transactionType.CreditStockStatusId.GetValueOrDefault() == 0)
|
|
{
|
|
// special case FBA Shipment Receipt +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(transaction.Reference))
|
|
{
|
|
shipmentInfo = readShipmentInfo.HeaderByFbaShipmentId(transaction.Reference);
|
|
if (shipmentInfo == null)
|
|
{
|
|
throw new Exception("Unable to retrive shipment info for reference '" + transaction.Reference + "'.");
|
|
}
|
|
else
|
|
{
|
|
shipmentInfoDic.Add(transaction.Reference, shipmentInfo);
|
|
}
|
|
}
|
|
|
|
if (shipmentInfo.IsSetShipmentStockStatusId())
|
|
{
|
|
// +ve shipment receipt
|
|
if (transactionType.CreditStockStatusId == null
|
|
&& transactionType.DebitStockStatusId > 0)
|
|
{
|
|
transactionType.CreditStockStatusId = shipmentInfo.ShipmentStockStatusId;
|
|
}
|
|
|
|
// -ve shipment receipt
|
|
else if (transactionType.DebitStockStatusId == null
|
|
&& transactionType.CreditStockStatusId > 0)
|
|
{
|
|
transactionType.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 journalList = new List<(int StockJournalId, int Quantity)>();
|
|
if (transactionType.FilterStockOnDateTime)
|
|
{
|
|
journalList = stockReallocate.BySkuNumber(
|
|
transaction.TransactionDate,
|
|
transactionType.StockJournalTypeId,
|
|
transaction.SkuNumber,
|
|
transaction.Quantity,
|
|
transactionType.DebitStockStatusId.GetValueOrDefault(),
|
|
transactionType.CreditStockStatusId.GetValueOrDefault(),
|
|
transactionType.FirstInFirstOut,
|
|
true);
|
|
}
|
|
else
|
|
{
|
|
journalList = stockReallocate.BySkuNumber(
|
|
DateTime.UtcNow,
|
|
transactionType.StockJournalTypeId,
|
|
transaction.SkuNumber,
|
|
transaction.Quantity,
|
|
transactionType.DebitStockStatusId.GetValueOrDefault(),
|
|
transactionType.CreditStockStatusId.GetValueOrDefault(),
|
|
transactionType.FirstInFirstOut,
|
|
true);
|
|
}
|
|
|
|
// insufficient balance available
|
|
if (!journalList.Any())
|
|
{
|
|
// in special case (found inventory), continue
|
|
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--;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
ProgressMessage = "Insurficent quantity at status/location to relocate stock";
|
|
recordSkip = recordSkip + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// fail safe
|
|
int qtyUnallocated = journalList.Sum(c => c.Quantity);
|
|
if (qtyUnallocated > transaction.Quantity)
|
|
{
|
|
throw new Exception(
|
|
currentMethodName + ": StockReallocateBySkuId() returned greater quantity than passed to function"
|
|
+ transaction.SkuTransactionId);
|
|
}
|
|
|
|
// update sku transaction table
|
|
var newRecordList = new List<Model.Stock.SkuTransaction>();
|
|
for (int j = 0; j < journalList.Count; j++)
|
|
{
|
|
// update existing record to reconciled
|
|
if (j == 0)
|
|
{
|
|
dbSkuTransaction.UpdateQuanitity(transaction.SkuTransactionId, journalList[j].Quantity);
|
|
dbSkuTransaction.UpdateIsProcessed(transaction.SkuTransactionId, true, journalList[j].StockJournalId);
|
|
|
|
}
|
|
// create new reconciled record
|
|
else
|
|
{
|
|
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);
|
|
}
|
|
|
|
qtyUnallocated = qtyUnallocated - journalList[j].Quantity;
|
|
}
|
|
|
|
// create new record for unreconciled quantity
|
|
if (qtyUnallocated > 0)
|
|
{
|
|
var newTransaction = new Model.Stock.SkuTransactionCreate(
|
|
transaction.TransactionDate
|
|
, transaction.SkuTransactionTypeCode
|
|
, transaction.ForeignKey
|
|
, transaction.Reference
|
|
, transaction.Detail
|
|
, transaction.SkuNumber
|
|
, qtyUnallocated
|
|
);
|
|
int newId = dbSkuTransaction.Create(newTransaction);
|
|
|
|
ProgressMessage = "Transaction could not be fully reconcoiled. Unallocated quanity remaing:" + qtyUnallocated;
|
|
recordSkip = recordSkip + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ItemsCompleted++;
|
|
ItemsRemaining--;
|
|
scope.Complete();
|
|
}
|
|
// end of scope
|
|
}
|
|
// end of loop
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Write("\r");
|
|
ProgressMessage = ex.Message;
|
|
return;
|
|
}
|
|
|
|
if (ItemsRemaining == 0)
|
|
{
|
|
ReconciliationComplete = true;
|
|
ProgressMessage = "Operation complete.";
|
|
}
|
|
|
|
//Stop:
|
|
Console.Write("\r");
|
|
|
|
UI.Console.WriteLine("ProcessStockTransactions() compete. " + ItemsCompleted + " total records processed, " + recordSkip + " rows uncompllete due to insurficent stock.");
|
|
UI.Console.WriteLine("ProcessStockTransactions(), " + recordSkip + " rows skipped due to insurficent stock.");
|
|
|
|
return;
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
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())
|
|
{
|
|
try
|
|
{
|
|
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++)
|
|
{
|
|
for (int j = 0; j < lostList.Count; j++)
|
|
{
|
|
if (foundList[i].SkuNumber == lostList[j].SkuNumber)
|
|
{
|
|
// 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)
|
|
{
|
|
// 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);
|
|
|
|
ItemsCompleted++;
|
|
break;
|
|
}
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// 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.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
new Data.Database.Stock.UpdateSkuTransaction().UpdateIsProcessed(skuTransactionId, false, null, true);
|
|
}
|
|
}
|
|
}
|