mirror of
https://github.com/stokebob/bnhtrade.git
synced 2026-03-19 14:37:16 +00:00
469 lines
22 KiB
C#
469 lines
22 KiB
C#
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 string sqlConnectionString;
|
|
private Data.Database.AmazonShipment.ReadShipmentInfo readShipmentInfo;
|
|
private Logic.Stock.SkuTransactionPersistance dbSkuTransaction;
|
|
private Logic.Stock.SkuTransactionTypePersistance dbSkuTransactionType;
|
|
private Logic.Validate.SkuTransaction validateSkuTrans;
|
|
private Logic.Stock.StatusReallocate stockReallocate;
|
|
private Logic.Log.LogEvent logEvent;
|
|
private string err = "Reconcile Sku Transaction Exception: ";
|
|
|
|
public SkuTransactionReconcile(string sqlConnectionString)
|
|
{
|
|
Innit();
|
|
this.sqlConnectionString = sqlConnectionString;
|
|
dbSkuTransaction = new SkuTransactionPersistance(sqlConnectionString);
|
|
dbSkuTransactionType = new SkuTransactionTypePersistance(sqlConnectionString);
|
|
readShipmentInfo = new Data.Database.AmazonShipment.ReadShipmentInfo();
|
|
validateSkuTrans = new Validate.SkuTransaction();
|
|
stockReallocate = new Logic.Stock.StatusReallocate(sqlConnectionString);
|
|
logEvent = 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 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)
|
|
/// </summary>
|
|
/// <param name="updateTransactions">Download and process Amazon reports before starting process</param>
|
|
/// <returns></returns>
|
|
public void ReconcileStockTransactions(bool updateTransactions)
|
|
{
|
|
Innit();
|
|
string currentMethodName = nameof(ReconcileStockTransactions);
|
|
|
|
// ensure import table have been processed into transaction table without exception
|
|
if (updateTransactions == true)
|
|
{
|
|
try
|
|
{
|
|
var preCheck = new bnhtrade.Core.Stock.StockReconciliation();
|
|
preCheck.ProcessFbaStockImportData(sqlConnectionString);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ProgressMessage = "Precheck failed: " + ex.Message;
|
|
return;
|
|
}
|
|
}
|
|
|
|
logEvent.LogInformation("Starting ReconcileStockTransactions()");
|
|
int recordSkip = 0;
|
|
|
|
ReconcileLostAndFound();
|
|
|
|
// 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;
|
|
|
|
try
|
|
{
|
|
// loop through transaction list
|
|
for (int i = 0; i < transList.Count; i++)
|
|
{
|
|
using (var scope = new TransactionScope())
|
|
{
|
|
|
|
Console.Write("\rProcessing record: {0} ({1} skipped)", (i + 1 + recordSkip), recordSkip);
|
|
|
|
// setup return values
|
|
CurrentSkuTransaction = transList[i];
|
|
CurrentTransactionId = transList[i].SkuTransactionId;
|
|
CurrentTransactionTypeCode = transList[i].SkuTransactionType.TypeCode;
|
|
LastItemDateTime = transList[i].TransactionDate;
|
|
|
|
// load type into variable
|
|
//var transType = dbSkuTransactionType.GetByTypeName(transList[i].SkuTransactionTypeName);
|
|
|
|
// stop if a new transactiontype is encountered
|
|
if (transList[i].SkuTransactionType.IsNewReviewRequired)
|
|
{
|
|
ProgressMessage = "New 'Transaction-Type' encountered";
|
|
//Console.Write("\r");
|
|
//MiscFunction.EventLogInsert(errMessage, 1);
|
|
break;
|
|
}
|
|
|
|
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 == "<AmazonReport><_GET_FBA_FULFILLMENT_INVENTORY_RECEIPTS_DATA_><+ve>"
|
|
|| transList[i].SkuTransactionType.TypeCode == "<AmazonReport><_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 == null
|
|
&& transList[i].SkuTransactionType.DebitStockStatusId > 0)
|
|
{
|
|
transList[i].SkuTransactionType.CreditStockStatusId = shipmentInfo.ShipmentStockStatusId;
|
|
}
|
|
|
|
// -ve shipment receipt
|
|
else if (transList[i].SkuTransactionType.DebitStockStatusId == null
|
|
&& 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 journalList = new List<(int StockJournalId, int Quantity)>();
|
|
if (transList[i].SkuTransactionType.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,
|
|
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,
|
|
true);
|
|
}
|
|
|
|
// insufficient balance available
|
|
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 quantity at status/location to relocate stock";
|
|
recordSkip = recordSkip + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// fail safe
|
|
int qtyAllocated = journalList.Sum(c => c.Quantity);
|
|
if (qtyAllocated > transList[i].Quantity)
|
|
{
|
|
throw new Exception(
|
|
currentMethodName + ": StockReallocateBySkuId() returned greater quantity than passed to function"
|
|
+ transList[i].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
|
|
if (j == 0)
|
|
{
|
|
transList[i].Quantity = (short)journalList[j].Quantity;
|
|
transList[i].StockJournalId = journalList[j].StockJournalId;
|
|
transList[i].IsProcessed = true;
|
|
|
|
dbSkuTransaction.Update(transList[i]);
|
|
}
|
|
// new record
|
|
else
|
|
{
|
|
var newRecord = transList[i].Clone();
|
|
newRecord.Quantity = (short)journalList[j].Quantity;
|
|
newRecord.IsProcessed = true;
|
|
newRecord.StockJournalId = journalList[j].StockJournalId;
|
|
|
|
newRecordList.Add(newRecord);
|
|
}
|
|
|
|
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++)
|
|
{
|
|
dbSkuTransaction.Create(newRecordList[j]);
|
|
}
|
|
}
|
|
|
|
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 ReconcileLostAndFound()
|
|
{
|
|
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++)
|
|
{
|
|
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
|
|
for (int j = 0; j < transList.Count; j++)
|
|
{
|
|
// update the 'lost' transaction
|
|
if (transList[j].SkuNumber == sku
|
|
&& transList[j].IsProcessed == false
|
|
&& transList[j].SkuTransactionTypeCode == lost)
|
|
{
|
|
// split 'lost' transaction
|
|
if (foundQtyUnAllocated - transList[j].Quantity < 0)
|
|
{
|
|
// create 'reconciled' clone
|
|
var clone = transList[j].Clone();
|
|
clone.IsProcessed = true;
|
|
clone.Quantity = (short)foundQtyUnAllocated;
|
|
|
|
// modifiy and validate existing record
|
|
transList[j].IsProcessed = false;
|
|
transList[j].Quantity = (short)(transList[j].Quantity - foundQtyUnAllocated);
|
|
|
|
// 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)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// drop out of the 'find lost' loop
|
|
|
|
// 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);
|
|
|
|
// modifiy existing record
|
|
transList[i].IsProcessed = false;
|
|
transList[i].Quantity = (short)foundQtyUnAllocated;
|
|
|
|
// submitt to database
|
|
dbSkuTransaction.Create(clone);
|
|
dbSkuTransaction.Update(transList[i]);
|
|
}
|
|
// this shouldn't happen
|
|
else
|
|
{
|
|
throw new Exception("Quantity unallocated is negative number");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// drop out of the 'find found' loop
|
|
scope.Complete();
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|