wip, this turned into a maaaaaahooosive job

This commit is contained in:
2025-07-09 13:28:17 +01:00
parent 8c3b00c75c
commit c0f7f1a476
25 changed files with 1888 additions and 1188 deletions

View File

@@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
namespace bnhtrade.Core.Logic.Account
{
public class Journal : UnitOfWorkBase
{
public int AccountJournalInsert(int journalTypeId, DateTime entryDate, string currencyCode,
decimal amount, int debitAccountId = 0, int creditAccountId = 0, bool lockEntry = false)
{
return new Data.Database.Account.CreateJournal().AccountJournalInsert(journalTypeId, entryDate, currencyCode,
amount, debitAccountId, creditAccountId, lockEntry);
}
public bool AccountJournalDelete(int accountJournalId)
{
return WithUnitOfWork(uow =>
{
bool result = uow.JournalRepository.DeleteJournal(accountJournalId);
CommitIfOwned(uow);
return result;
});
}
}
}

View File

@@ -0,0 +1,44 @@
using bnhtrade.Core.Data.Database.UnitOfWork;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
namespace bnhtrade.Core.Logic.Account
{
public class JournalService : UnitOfWorkBase
{
public JournalService()
{
}
internal JournalService(IUnitOfWork unitOfWork) : base(unitOfWork)
{
}
public int JournalInsert(int journalTypeId, DateTime entryDate, string currencyCode,
decimal amount, int debitAccountId = 0, int creditAccountId = 0, bool lockEntry = false)
{
return WithUnitOfWork(uow =>
{
int journalId = uow.JournalRepository.AccountJournalInsert(journalTypeId, entryDate, currencyCode,
amount, debitAccountId, creditAccountId, lockEntry);
CommitIfOwned(uow);
return journalId;
});
}
public bool JournalDelete(int accountJournalId)
{
return WithUnitOfWork(uow =>
{
bool result = uow.JournalRepository.DeleteJournal(accountJournalId);
CommitIfOwned(uow);
return result;
});
}
}
}

View File

@@ -1,13 +1,18 @@
using System;
using bnhtrade.Core.Data.Database.UnitOfWork;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Transactions;
using System.Windows.Forms;
namespace bnhtrade.Core.Logic.Sku
namespace bnhtrade.Core.Logic.Inventory
{
public class SkuService : UnitOfWorkBase
{
public SkuService() : base() { }
internal SkuService(IUnitOfWork unitOfWork) : base(unitOfWork) { }
/// <summary>
/// Used for retriving an SKU ID by parameters. If no match, can create a new SKU if required.
/// </summary>

View File

@@ -0,0 +1,45 @@
using bnhtrade.Core.Data.Database.UnitOfWork;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace bnhtrade.Core.Logic.Inventory
{
public class StockJournalService : UnitOfWorkBase
{
public StockJournalService() { }
internal StockJournalService(IUnitOfWork unitOfWork) : base(unitOfWork) { }
public void StockJournalDelete(int stockJournalId)
{
WithUnitOfWork(uow =>
{
if (stockJournalId <= 0)
{
throw new ArgumentException("Stock journal ID must be greater than zero", nameof(stockJournalId));
}
bool result = uow.StockJournalRepository.DeleteStockJournal(stockJournalId);
CommitIfOwned(uow);
if (!result)
{
throw new InvalidOperationException($"Failed to delete stock journal with ID {stockJournalId}");
}
});
}
public bool WIP_StockJournalConsistencyCheck(int stockId, List<int> statusIdEffected = null)
{
return WithUnitOfWork(uow =>
{
if (stockId <= 0)
{
throw new ArgumentException("Stock ID must be greater than zero", nameof(stockId));
}
return uow.StockJournalRepository.WIP_StockJournalConsistencyCheck(stockId, statusIdEffected);
});
}
}
}

View File

@@ -0,0 +1,443 @@
using bnhtrade.Core.Data.Database.UnitOfWork;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
using static System.Formats.Asn1.AsnWriter;
namespace bnhtrade.Core.Logic.Inventory
{
public class StockService : UnitOfWorkBase
{
public StockService() : base() { }
internal StockService(IUnitOfWork unitOfWork) : base(unitOfWork) { }
private int WIP_StockInsert(int accountJournalType, int stockJournalType, string currencyCode, decimal amount,
int quantity, int productId, int conditionId, int accountTaxCodeId, DateTime entryDate, int debitStatusId)
{
using (TransactionScope scope = new TransactionScope())
using (SqlConnection conn = new SqlConnection(sqlConnectionString))
{
conn.Open();
// add account journal entry
int accountJournalId = new Logic.Account.JournalService().JournalInsert(accountJournalType, entryDate, currencyCode, amount);
// make the stock insert
int stockId = WIP_StockInsertSub(sqlConnectionString, productId, conditionId, accountTaxCodeId,
accountJournalId, stockJournalType, entryDate, quantity, debitStatusId);
scope.Complete();
return stockId;
}
}
private int WIP_StockInsertSub(int productId, int conditionId, int accountTaxCodeId,
int accountJournalId, int stockJournalTypeId, DateTime stockJournalEntryDate, int quantity, int statusDebitId)
{
stockJournalEntryDate = DateTime.SpecifyKind(stockJournalEntryDate, DateTimeKind.Utc);
// ensure account journal id hasn't already been added to stock table
int count = 0;
using (SqlCommand cmd = new SqlCommand(@"
SELECT Count(tblStock.StockID) AS CountOfID
FROM tblStock
WHERE (((tblStock.AccountJournalID)=@accountJouranlId));
", conn))
{
cmd.Parameters.AddWithValue("@accountJouranlId", accountJournalId);
count = (int)cmd.ExecuteScalar();
}
if (count == 1)
{
throw new Exception("Add account journal entry already assigned to stock line.");
}
else if (count > 1)
{
throw new Exception("Houston we have a problem! An account journal entry is assigned to " + count + " stock lines.");
}
// ensure the debit for the account journal transaction is to an 'Asset' account type
using (SqlCommand cmd = new SqlCommand(@"
SELECT
Count(tblAccountJournalPost.AccountJournalPostID) AS CountOfAccountJournalPostID
FROM
(tblAccountJournalPost
INNER JOIN tblAccountChartOf
ON tblAccountJournalPost.AccountChartOfID = tblAccountChartOf.AccountChartOfID)
INNER JOIN tblAccountChartOfType
ON tblAccountChartOf.AccountChartOfTypeID = tblAccountChartOfType.AccountChartOfTypeID
WHERE
tblAccountJournalPost.AmountGbp>=0
AND tblAccountChartOfType.BasicType='Asset'
AND tblAccountJournalPost.AccountJournalID=@accountJournalId;
", conn))
{
cmd.Parameters.AddWithValue("@accountJournalId", accountJournalId);
if ((int)cmd.ExecuteScalar() < 1)
{
throw new Exception("Supplied AccountJournal entry must debit an 'Asset' account type.");
}
}
// get statusCreditId for stock journal type
int statusCreditId;
using (SqlCommand cmd = new SqlCommand(@"
SELECT
tblStockJournalType.StockStatusID_Credit
FROM
tblStockJournalType
WHERE
tblStockJournalType.StockJournalTypeID=@stockJournalTypeId
AND tblStockJournalType.StockStatusID_Credit Is Not Null;
", conn))
{
cmd.Parameters.AddWithValue("@stockJournalTypeId", stockJournalTypeId);
object obj = cmd.ExecuteScalar();
if (obj == null)
{
throw new Exception("Default credit status not set for StockJournalTypeID=" + stockJournalTypeId);
}
else
{
statusCreditId = (int)obj;
}
}
// get/set an skuId
int skuId = new Logic.Inventory.SkuService().GetSkuId(productId, conditionId, accountTaxCodeId, true);
// add the entry to the stock table (minus stockJournalId)
int stockId = 0;
using (SqlCommand cmd = new SqlCommand(@"
INSERT INTO tblStock
(SkuID, AccountJournalID)
OUTPUT INSERTED.StockID
VALUES
(@skuId, @accountJournalId);
", conn))
{
cmd.Parameters.AddWithValue("@skuId", skuId);
cmd.Parameters.AddWithValue("@accountJournalId", accountJournalId);
stockId = (int)cmd.ExecuteScalar();
}
// insert stock journal entry
var journalPosts = new List<(int statusId, int quantity)>();
journalPosts.Add((statusDebitId, quantity));
journalPosts.Add((statusCreditId, (quantity * -1)));
int stockJournalId = Stock.StockJournal.StockJournalInsert(sqlConnectionString, stockJournalTypeId, stockId, journalPosts, stockJournalEntryDate, true);
// update the stock table
using (SqlCommand cmd = new SqlCommand(@"
UPDATE tblStock
SET StockJournalID=@stockJournalId
WHERE StockID=@stockId;
", conn))
{
cmd.Parameters.AddWithValue("@stockJournalId", stockJournalId);
cmd.Parameters.AddWithValue("@stockId", stockId);
count = cmd.ExecuteNonQuery();
if (count < 1)
{
throw new Exception("New stock insert cancelled, failed to update StockJournalID");
}
}
scope.Complete();
return stockId;
}
private void WIP_StockDelete(int stockId)
{
int accountJournalType = 0;
int stockJournalType = 0;
// get stock and account types
using (TransactionScope scope = new TransactionScope())
using (SqlConnection conn = new SqlConnection(sqlConnectionString))
{
conn.Open();
// ensure stockId is owner-introduced
using (SqlCommand cmd = new SqlCommand(@"
SELECT
tblStockJournal.StockJournalTypeID, tblAccountJournal.AccountJournalTypeID
FROM
(tblStock INNER JOIN tblAccountJournal
ON tblStock.AccountJournalID = tblAccountJournal.AccountJournalID)
INNER JOIN tblStockJournal
ON tblStock.StockJournalID = tblStockJournal.StockJournalID
WHERE
(((tblStock.StockID)=@stockId));
", conn))
{
cmd.Parameters.AddWithValue("@stockId", stockId);
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
accountJournalType = reader.GetInt32(1);
stockJournalType = reader.GetInt32(0);
}
else
{
throw new Exception("Integrity check failed, cancelling StockDeleteOwnerIntroduced");
}
}
}
// check stock journal type is not restricted
// owner inventory introduced
if (stockJournalType == 2)
{
// no dramas
}
else
{
throw new Exception("Manual delete of this stock type is not supported, use the method that created it!");
}
// check there is only one stock journal entry for stock item
using (SqlCommand cmd = new SqlCommand(@"
SELECT Count(tblStockJournal.StockJournalID) AS CountOfStockJournalID
FROM tblStockJournal
WHERE (((tblStockJournal.StockID)=@stockId));
", conn))
{
cmd.Parameters.AddWithValue("@stockId", stockId);
int count = (int)cmd.ExecuteScalar();
if (count > 1)
{
throw new Exception("Delete " + count + " stock journal entries (other than source entry), before peforming this operation.");
}
}
// remove account journal entry
WIP_StockDeleteSubAccountJournalEntry(sqlConnectionString, stockId);
// remove stock
WIP_StockDeleteSub(sqlConnectionString, stockId);
scope.Complete();
}
}
private void WIP_StockDeleteSub(int stockId)
{
using (TransactionScope scope = new TransactionScope())
using (SqlConnection conn = new SqlConnection(sqlConnectionString))
{
conn.Open();
// check for accountJournalId on stock table
using (SqlCommand cmd = new SqlCommand(@"
SELECT AccountJournalID
FROM tblStock
WHERE StockID=@stockId;
", conn))
{
cmd.Parameters.AddWithValue("@stockId", stockId);
object obj = cmd.ExecuteScalar();
if (obj == null)
{
throw new Exception("StockID=" + stockId + " does not exist.");
}
else if (obj == DBNull.Value)
{
// nothing to do all is good
}
else
{
int id = (int)obj;
if (id > 0)
{
throw new Exception("StockID=" + stockId + " remove account journal entry using method that created it first.");
}
}
}
// get stockJournalId
int stockJournalId;
using (SqlCommand cmd = new SqlCommand(@"
SELECT StockJournalID
FROM tblStock
WHERE StockID=@stockId;
", conn))
{
cmd.Parameters.AddWithValue("@stockId", stockId);
try
{
stockJournalId = (int)cmd.ExecuteScalar();
}
catch
{
throw new Exception("Could not retrive StockJournalID for StockID=" + stockId);
}
}
// remove stockJournalId from stock table
using (SqlCommand cmd = new SqlCommand(@"
UPDATE tblStock
SET StockJournalID=NULL
WHERE StockID=@stockId;
", conn))
{
cmd.Parameters.AddWithValue("@stockId", stockId);
int count = cmd.ExecuteNonQuery();
if (count != 2) // we need to count the LastUpdated trigger!
{
throw new Exception("Failed to remove StockJournalID from stock table StockID=" + stockId);
}
}
// delete stock journal entry
Core.Stock.StockJournal.StockJournalDelete(sqlConnectionString, stockJournalId);
// delete stock table entry
using (SqlCommand cmd = new SqlCommand(@"
DELETE FROM tblStock
WHERE StockID=@stockId;
", conn))
{
cmd.Parameters.AddWithValue("@stockId", stockId);
int count = cmd.ExecuteNonQuery();
if (count != 1)
{
throw new Exception("StockID = " + stockId + " delete failed");
}
else
{
scope.Complete();
}
}
}
}
// to be used by other methods within a transaction scope
private void WIP_StockDeleteSubAccountJournalEntry(int stockId)
{
using (SqlConnection conn = new SqlConnection(sqlConnectionString))
{
conn.Open();
var trans = conn.BeginTransaction();
// get the account journal id
int accountJournalId = 0;
using (SqlCommand cmd = new SqlCommand(@"
SELECT AccountJournalID
FROM tblStock
WHERE StockID=@stockId;
", conn))
{
cmd.Transaction = trans;
cmd.Parameters.AddWithValue("@stockId", stockId);
object obj = cmd.ExecuteScalar();
if (obj == null)
{
throw new Exception("StockID=" + stockId + " does not exist.");
}
else if (obj == DBNull.Value)
{
throw new Exception("AccountJournalID not found for StockID=" + stockId);
}
else
{
accountJournalId = (int)obj;
}
}
// remove entry from stock table
using (SqlCommand cmd = new SqlCommand(@"
UPDATE tblStock
SET AccountJournalID=NULL
WHERE StockID=@stockId;
", conn))
{
cmd.Transaction = trans;
cmd.Parameters.AddWithValue("@stockId", stockId);
int count = cmd.ExecuteNonQuery();
if (count != 2) // must include last modified trigger
{
throw new Exception("Failed to set AccountJournalID to NULL on stock table for StockID=" + stockId);
}
}
// delete account journal entry
new Data.Database.Repository.Implementation.AccountJournalRepository(conn, trans).DeleteJournal(accountJournalId);
trans.Commit();
}
}
public int WIP_StockInsertPurchase(int productId, int conditionId, int accountTaxCodeId, int accountJournalId, int quantity, int statusDebitId)
{
DateTime stockJournalEntryDate;
int stockJournalTypeId = 1;
using (SqlConnection conn = new SqlConnection(sqlConnectionString))
{
conn.Open();
// retrive info from purchase invoice line/transaction
using (SqlCommand cmd = new SqlCommand(@"
SELECT tblAccountJournal.EntryDate
FROM tblAccountJournal
WHERE (((tblAccountJournal.AccountJournalID)=@accountJournalId));
", conn))
{
cmd.Parameters.AddWithValue("@accountJournalId", accountJournalId);
stockJournalEntryDate = DateTime.SpecifyKind((DateTime)cmd.ExecuteScalar(), DateTimeKind.Utc);
}
}
return WIP_StockInsertSub(productId, conditionId, accountTaxCodeId, accountJournalId, stockJournalTypeId, stockJournalEntryDate, quantity, statusDebitId);
}
public int WIP_StockInsertOwnerIntroduced(decimal amount, int quantity, int productId, int conditionId, int accountTaxCodeId, DateTime entryDate, int debitStatusId)
{
int accountJournalType = 3;
int stockJournalType = 2;
string currencyCode = "GBP";
return WIP_StockInsert(accountJournalType, stockJournalType, currencyCode, amount, quantity, productId, conditionId, accountTaxCodeId, entryDate, debitStatusId);
}
public void WIP_StockDeletePurchase(int stockId)
{
WIP_StockDeleteSub(sqlConnectionString, stockId);
}
public void WIP_StockDeleteOwnerIntroduced(int stockId)
{
WIP_StockDelete(sqlConnectionString, stockId);
}
}
}

View File

@@ -0,0 +1,39 @@
using bnhtrade.Core.Data.Database.UnitOfWork;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace bnhtrade.Core.Logic.Inventory
{
public class StockStatusService : UnitOfWorkBase
{
public StockStatusService() : base() { }
internal StockStatusService(IUnitOfWork unitOfWork) : base(unitOfWork) { }
/// <summary>
/// Return the avaliable balance of a status. Uses a more efficent sql/code. However, balance requests should
/// generally also involve a date/time (i.e. the system does allow a stock transaction in the future, therefore
/// this method may give an available quantity, but transfers before that date wouod not be possible).
/// </summary>
/// <param name="sku">SKU number</param>
/// <param name="statusId">Status ID</param>
/// <returns>Balance as quantity</returns>
private int GetAvailableBalanceBySku(string sku, int statusId)
{
return WithUnitOfWork(uow =>
{
if (string.IsNullOrWhiteSpace(sku))
{
throw new ArgumentException("SKU number is null, empty, or whitespace", nameof(sku));
}
return uow.StockJournalRepository.ReadStatusBalanceBySku(sku, statusId);
});
}
}
}

View File

@@ -0,0 +1,44 @@
using bnhtrade.Core.Data.Database.UnitOfWork;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;
namespace bnhtrade.Core.Logic.Purchase
{
public class PurchaseService : UnitOfWorkBase
{
public PurchaseService() : base() { }
internal PurchaseService(IUnitOfWork unitOfWork) : base(unitOfWork) { }
public void WIP_PurchaseLineTransactionNetInsert(int purchaseLineId, string currencyCode, decimal amountNet, DateTime entryDate, int debitAccountId = 0)
{
WithUnitOfWork(uow =>
{
uow.PurchaseRepository.WIP_PurchaseLineTransactionNetInsert(purchaseLineId, currencyCode, amountNet, entryDate, debitAccountId);
CommitIfOwned(uow);
});
}
public void WIP_PurchaseLineTransactionNetUpdate(int accountJouranlId, string currencyCode, decimal amountNet, int debitAccountId)
{
WithUnitOfWork(uow =>
{
uow.PurchaseRepository.WIP_PurchaseLineTransactionNetUpdate(accountJouranlId, currencyCode, amountNet, debitAccountId);
CommitIfOwned(uow);
});
}
public void WIP_PurchaseLineTransactionDelete(int purchaseLineId, int accountJournalId)
{
WithUnitOfWork(uow =>
{
uow.PurchaseRepository.WIP_PurchaseLineTransactionDelete(purchaseLineId, accountJournalId);
CommitIfOwned(uow);
});
}
}
}

View File

@@ -12,22 +12,9 @@ namespace bnhtrade.Core.Logic.Stock
{
}
/// <summary>
/// Return the avaliable balance of a status. Uses a more efficent sql/code. However, balance requests should
/// generally also involve a date/time (i.e. the system does allow a stock transaction in the future, therefore
/// this method may give an available quantity, but transfers before that date wouod not be possible).
/// </summary>
/// <param name="sku">SKU number</param>
/// <param name="statusId">Status ID</param>
/// <returns>Balance as quantity</returns>
private int GetAvailableBalanceBySku(string sku, int statusId)
{
return new Data.Database.Stock.ReadStatusBalance().BySku(sku, statusId);
}
/// <summary>
/// Return the avaliable balance of a status, includes datetime that each stockNumber became avaiable.
/// Useful for checking availability before moving stock/sku retrospectivly
/// Useful for checking availability before moving stock/sku retrospectivly
/// </summary>
/// <param name="skuNumber">SKU number</param>
/// <param name="statusId">Status ID</param>
@@ -131,7 +118,7 @@ namespace bnhtrade.Core.Logic.Stock
/// <returns>Dictionary of SKUs and the repective balance of each</returns>
public Dictionary<string, int> GetSkuQuantity(int statusId)
{
return new Data.Database.Stock.ReadStatusBalance().ByStatusId(statusId);
return new Data.Database.Stock.ReadStatusBalance().ReadStatusBalanceByStatusId(statusId);
}
}
}