From c0f7f1a4768a30f7e525f72ba6ee684895a4f310 Mon Sep 17 00:00:00 2001 From: Bob Hodgetts Date: Wed, 9 Jul 2025 13:28:17 +0100 Subject: [PATCH] wip, this turned into a maaaaaahooosive job --- src/bnhtrade.ComTypeLib/Account/Account.cs | 4 +- .../Purchase/PurchaseLine.cs | 12 +- src/bnhtrade.ComTypeLib/Stock/Stock.cs | 8 +- .../Data/Database/Account/CreateJournal.cs | 208 ----- .../Data/Database/Account/UpdateJournal.cs | 75 -- ...ository.cs => AccountJournalRepository.cs} | 271 +++++- .../Implementation/PurchaseRepository.cs | 189 +++++ .../Implementation/StockJournalRepository.cs | 717 ++++++++++++++++ .../Repository/Implementation/_BoilerPlate.cs | 2 +- ...sitory.cs => IAccountJournalRepository.cs} | 4 +- .../Interface/IPurchaseRepository.cs | 16 + .../Interface/IStockJournalRepository.cs | 20 + .../Data/Database/Stock/JournalCrud.cs | 2 +- .../Data/Database/Stock/ReadStatusBalance.cs | 73 +- .../Data/Database/UnitOfWork/IUnitOfWork.cs | 4 +- .../Data/Database/UnitOfWork/UnitOfWork.cs | 32 +- src/bnhtrade.Core/Logic/Account/Journal.cs | 30 - .../Logic/Account/JournalService.cs | 44 + .../Logic/{Sku => Inventory}/SkuService.cs | 9 +- .../Logic/Inventory/StockJournalService.cs | 45 + .../Logic/Inventory/StockService.cs | 443 ++++++++++ .../Logic/Inventory/StockStatusService.cs | 39 + .../Logic/Purchase/PurchaseService.cs | 44 + .../Logic/Stock/StatusBalance.cs | 17 +- src/bnhtrade.Core/Program.cs | 768 +----------------- 25 files changed, 1888 insertions(+), 1188 deletions(-) delete mode 100644 src/bnhtrade.Core/Data/Database/Account/CreateJournal.cs delete mode 100644 src/bnhtrade.Core/Data/Database/Account/UpdateJournal.cs rename src/bnhtrade.Core/Data/Database/Repository/Implementation/{JournalRepository.cs => AccountJournalRepository.cs} (55%) create mode 100644 src/bnhtrade.Core/Data/Database/Repository/Implementation/PurchaseRepository.cs create mode 100644 src/bnhtrade.Core/Data/Database/Repository/Implementation/StockJournalRepository.cs rename src/bnhtrade.Core/Data/Database/Repository/Interface/{IJournalRepository.cs => IAccountJournalRepository.cs} (56%) create mode 100644 src/bnhtrade.Core/Data/Database/Repository/Interface/IPurchaseRepository.cs create mode 100644 src/bnhtrade.Core/Data/Database/Repository/Interface/IStockJournalRepository.cs delete mode 100644 src/bnhtrade.Core/Logic/Account/Journal.cs create mode 100644 src/bnhtrade.Core/Logic/Account/JournalService.cs rename src/bnhtrade.Core/Logic/{Sku => Inventory}/SkuService.cs (88%) create mode 100644 src/bnhtrade.Core/Logic/Inventory/StockJournalService.cs create mode 100644 src/bnhtrade.Core/Logic/Inventory/StockService.cs create mode 100644 src/bnhtrade.Core/Logic/Inventory/StockStatusService.cs create mode 100644 src/bnhtrade.Core/Logic/Purchase/PurchaseService.cs diff --git a/src/bnhtrade.ComTypeLib/Account/Account.cs b/src/bnhtrade.ComTypeLib/Account/Account.cs index bf784bc..bba9393 100644 --- a/src/bnhtrade.ComTypeLib/Account/Account.cs +++ b/src/bnhtrade.ComTypeLib/Account/Account.cs @@ -35,13 +35,13 @@ namespace bnhtrade.ComTypeLib public int AccountJournalInsert(ConnectionCredential sqlConnCred, int journalTypeId, DateTime entryDate, string currencyCode, [MarshalAs(UnmanagedType.Currency)] decimal amount, int debitAccountId = 0, int creditAccountId = 0, bool lockEntry = false) { - return new Core.Logic.Account.Journal().AccountJournalInsert(journalTypeId, entryDate, + return new Core.Logic.Account.JournalService().JournalInsert(journalTypeId, entryDate, currencyCode, amount, debitAccountId, creditAccountId, lockEntry); } public bool AccountJournalDelete(ConnectionCredential sqlConnCred, int accountJournalId) { - return new Core.Logic.Account.Journal().AccountJournalDelete(accountJournalId); + return new Core.Logic.Account.JournalService().JournalDelete(accountJournalId); } [return: MarshalAs(UnmanagedType.Currency)] diff --git a/src/bnhtrade.ComTypeLib/Purchase/PurchaseLine.cs b/src/bnhtrade.ComTypeLib/Purchase/PurchaseLine.cs index b4d7979..600a18f 100644 --- a/src/bnhtrade.ComTypeLib/Purchase/PurchaseLine.cs +++ b/src/bnhtrade.ComTypeLib/Purchase/PurchaseLine.cs @@ -35,32 +35,30 @@ namespace bnhtrade.ComTypeLib.Purchase string currencyCode, [MarshalAs(UnmanagedType.Currency)] decimal amountNet, DateTime entryDate) { - Core.Purchase.PurchaseQuery.WIP_PurchaseLineTransactionNetInsert(sqlConnCred.ConnectionString, - purchaseLineId, currencyCode, amountNet, entryDate); + new Core.Logic.Purchase.PurchaseService().WIP_PurchaseLineTransactionNetInsert(purchaseLineId, currencyCode, amountNet, entryDate); } public void PurchaseLineTransactionNetUpdate(ConnectionCredential sqlConnCred, int accountJouranlId, string currencyCode, [MarshalAs(UnmanagedType.Currency)] decimal amountNet, int debitAccountId) { - Core.Purchase.PurchaseQuery.WIP_PurchaseLineTransactionNetUpdate(sqlConnCred.ConnectionString, - accountJouranlId, currencyCode, amountNet, debitAccountId); + new Core.Logic.Purchase.PurchaseService().WIP_PurchaseLineTransactionNetUpdate(accountJouranlId, currencyCode, amountNet, debitAccountId); } public void PurchaseLineTransactionDelete(ConnectionCredential sqlConnCred, int purchaseLineId, int accountJournalId) { - Core.Purchase.PurchaseQuery.WIP_PurchaseLineTransactionDelete(sqlConnCred.ConnectionString, purchaseLineId, accountJournalId); + new Core.Logic.Purchase.PurchaseService().WIP_PurchaseLineTransactionDelete(purchaseLineId, accountJournalId); } public int PurchaseLineTransactionStockInsert(ConnectionCredential sqlConnCred, int accountJournalId, string currencyCode, [MarshalAs(UnmanagedType.Currency)] decimal amount, int quantity, int productId, int conditionId, int accountTaxCodeId, int stockDebitStatusId) { - return Core.Stock.StockCreate.WIP_StockInsertPurchase(sqlConnCred.ConnectionString, productId, conditionId, accountTaxCodeId, accountJournalId, quantity, stockDebitStatusId); + return new Core.Logic.Inventory.StockService().WIP_StockInsertPurchase(productId, conditionId, accountTaxCodeId, accountJournalId, quantity, stockDebitStatusId); } public void PurchaseLineTransactionStockDelete(ConnectionCredential sqlConnCred, int stockId) { - Core.Stock.StockCreate.WIP_StockDeletePurchase(sqlConnCred.ConnectionString, stockId); + new Core.Logic.Inventory.StockService().WIP_StockDeletePurchase(stockId); } } } diff --git a/src/bnhtrade.ComTypeLib/Stock/Stock.cs b/src/bnhtrade.ComTypeLib/Stock/Stock.cs index dde191c..e9d6fe9 100644 --- a/src/bnhtrade.ComTypeLib/Stock/Stock.cs +++ b/src/bnhtrade.ComTypeLib/Stock/Stock.cs @@ -43,22 +43,22 @@ namespace bnhtrade.ComTypeLib { public int StockInsertPurchase(ConnectionCredential sqlConnCred, int productId, int conditionId, int accountTaxCodeId, int accountJournalId, int quantity, int statusDebitId) { - return Core.Stock.StockCreate.WIP_StockInsertPurchase(sqlConnCred.ConnectionString, productId, conditionId, accountTaxCodeId, accountJournalId, quantity, statusDebitId); + return new Core.Logic.Inventory.StockService().WIP_StockInsertPurchase(productId, conditionId, accountTaxCodeId, accountJournalId, quantity, statusDebitId); } public int StockInsertOwnerIntroduced(ConnectionCredential sqlConnCred, [MarshalAs(UnmanagedType.Currency)] decimal amount, int quantity, int productId, int conditionId, int accountTaxCodeId, DateTime entryDate, int debitStatusId) { - return Core.Stock.StockCreate.WIP_StockInsertOwnerIntroduced(sqlConnCred.ConnectionString, amount, quantity, productId, conditionId, accountTaxCodeId, entryDate, debitStatusId); + return new Core.Logic.Inventory.StockService().WIP_StockInsertOwnerIntroduced(amount, quantity, productId, conditionId, accountTaxCodeId, entryDate, debitStatusId); } public void StockDeletePurchase(ConnectionCredential sqlConnCred, int stockId) { - Core.Stock.StockCreate.WIP_StockDeletePurchase(sqlConnCred.ConnectionString, stockId); + new Core.Logic.Inventory.StockService().WIP_StockDeletePurchase(stockId); } public void StockDeleteOwnerIntroduced(ConnectionCredential sqlConnCred, int stockId) { - Core.Stock.StockCreate.WIP_StockDeleteOwnerIntroduced(sqlConnCred.ConnectionString, stockId); + new Core.Logic.Inventory.StockService().WIP_StockDeleteOwnerIntroduced(stockId); } public int StockReallocate(ConnectionCredential sqlConnCred, int stockId, int quantity, int debitStatusId, int creditStatusId, DateTime entryDate) diff --git a/src/bnhtrade.Core/Data/Database/Account/CreateJournal.cs b/src/bnhtrade.Core/Data/Database/Account/CreateJournal.cs deleted file mode 100644 index bfbb086..0000000 --- a/src/bnhtrade.Core/Data/Database/Account/CreateJournal.cs +++ /dev/null @@ -1,208 +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.Data.Database.Account -{ - internal class CreateJournal : Connection - { - /// - /// Old code needs sorting - /// - public int AccountJournalInsert(int journalTypeId, DateTime entryDate, string currencyCode, - decimal amount, int debitAccountId = 0, int creditAccountId = 0, bool lockEntry = false) - { - int defaultDebit = 0; - int defaultCredit = 0; - - // ensure date is UTC - entryDate = DateTime.SpecifyKind(entryDate, DateTimeKind.Utc); - - // debit and credit locks are checked in journal post method - using (TransactionScope scope = new TransactionScope()) - using (SqlConnection conn = new SqlConnection(SqlConnectionString)) - { - conn.Open(); - - // insert the journal entry - int journalId; - - using (SqlCommand cmd = new SqlCommand(@" - INSERT INTO tblAccountJournal - (AccountJournalTypeID, EntryDate, IsLocked) - OUTPUT INSERTED.AccountJournalID - VALUES - (@journalTypeId, @entryDate, @lockEntry) - ", conn)) - { - // add parameters - cmd.Parameters.AddWithValue("@journalTypeId", journalTypeId); - cmd.Parameters.AddWithValue("@entryDate", entryDate.ToUniversalTime()); - cmd.Parameters.AddWithValue("@lockEntry", lockEntry); - - //execute - journalId = (int)cmd.ExecuteScalar(); - } - - // insert journal entries - //bool postResult = AccountJournalPostInsert(sqlConnectionString, journalId, entryDate, currencyCode, amount, debitAccountId, creditAccountId); - bool postResult = AccountJournalPostInsert(journalId, entryDate, currencyCode, amount, debitAccountId, creditAccountId); - - scope.Complete(); - return journalId; - } - } - - /// - /// Old code needs sorting - /// - internal bool AccountJournalPostInsert(int journalId, DateTime entryDate, - string currencyCode, decimal amount, int debitAccountId = 0, int creditAccountId = 0) - { - int defaultDebit; - int defaultCredit; - entryDate = DateTime.SpecifyKind(entryDate, DateTimeKind.Utc); - - using (TransactionScope scope = new TransactionScope()) - using (SqlConnection conn = new SqlConnection(SqlConnectionString)) - { - conn.Open(); - - // ensure their are no other entries - using (SqlCommand cmd = new SqlCommand(@" - SELECT - Count(tblAccountJournalPost.AccountJournalPostID) AS CountOfAccountJournalPostID - FROM - tblAccountJournalPost - WHERE - (((tblAccountJournalPost.AccountJournalID)=@AccountJournalID)); - ", conn)) - { - cmd.Parameters.AddWithValue("@AccountJournalID", journalId); - - int count = (int)cmd.ExecuteScalar(); - - if (count > 0) - { - throw new Exception("Unable the insert journal posts, post already present AccountJournalID=" + journalId); - } - } - - //checks - using (SqlCommand cmd = new SqlCommand(@" - SELECT - tblAccountJournalType.ChartOfAccountID_Debit, tblAccountJournalType.ChartOfAccountID_Credit - FROM - tblAccountJournal - INNER JOIN tblAccountJournalType - ON tblAccountJournal.AccountJournalTypeID = tblAccountJournalType.AccountJournalTypeID - WHERE - (((tblAccountJournal.AccountJournalID)=@journalId)); - ", conn)) - { - cmd.Parameters.AddWithValue("@journalId", journalId); - - using (SqlDataReader reader = cmd.ExecuteReader()) - { - if (reader.Read()) - { - // debit check - if (reader.IsDBNull(0)) - { - if (debitAccountId == 0) - { - throw new Exception("Debit Account ID required, default not set for journal type"); - } - } - else - { - defaultDebit = reader.GetInt32(0); - if (debitAccountId == 0) - { - debitAccountId = defaultDebit; - } - else if (debitAccountId != defaultDebit) - { - throw new Exception("Debit Account ID supplied does not match default set for journal type"); - } - - } - // credit check - if (reader.IsDBNull(1)) - { - if (creditAccountId == 0) - { - throw new Exception("Credit Account ID required, default not set for journal type"); - } - } - else - { - defaultCredit = reader.GetInt32(1); - if (creditAccountId == 0) - { - creditAccountId = defaultCredit; - } - else if (creditAccountId != defaultCredit) - { - throw new Exception("Credit Account ID supplied does not match default set for journal type"); - } - } - } - else - { - throw new Exception("AccountJournalID '" + journalId + "' does not exist."); - } - } - } - - // currency conversion - if (currencyCode != "GBP") - { - amount = new Logic.Account.CurrencyService().CurrencyConvertToGbp(currencyCode, amount, entryDate); - } - - // ensure decimal is rounded - amount = Math.Round(amount, 2); - - // insert debit post - using (SqlCommand cmd = new SqlCommand(@" - INSERT INTO tblAccountJournalPost - (AccountJournalID, AccountChartOfID, AmountGbp) - VALUES - (@AccountJournalId, @AccountChartOfId, @AmountGbp) - ", conn)) - { - // add parameters - cmd.Parameters.AddWithValue("@AccountJournalId", journalId); - cmd.Parameters.AddWithValue("@AccountChartOfId", debitAccountId); - cmd.Parameters.AddWithValue("@AmountGbp", amount); - - cmd.ExecuteNonQuery(); - } - - // insert credit post - using (SqlCommand cmd = new SqlCommand(@" - INSERT INTO tblAccountJournalPost - (AccountJournalID, AccountChartOfID, AmountGbp) - VALUES - (@AccountJournalId, @AccountChartOfId, @AmountGbp) - ", conn)) - { - // add parameters - cmd.Parameters.AddWithValue("@AccountJournalId", journalId); - cmd.Parameters.AddWithValue("@AccountChartOfId", creditAccountId); - cmd.Parameters.AddWithValue("@AmountGbp", (amount * -1)); - - cmd.ExecuteNonQuery(); - } - - scope.Complete(); - return true; - } - } - } -} diff --git a/src/bnhtrade.Core/Data/Database/Account/UpdateJournal.cs b/src/bnhtrade.Core/Data/Database/Account/UpdateJournal.cs deleted file mode 100644 index c7de34a..0000000 --- a/src/bnhtrade.Core/Data/Database/Account/UpdateJournal.cs +++ /dev/null @@ -1,75 +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.Data.Database.Account -{ - internal class UpdateJournal : Connection - { - public bool AccountJournalPostUpdate(int journalId, string currencyCode, decimal amount, int debitAccountId = 0, int creditAccountId = 0) - { - using (TransactionScope scope = new TransactionScope()) - using (SqlConnection conn = new SqlConnection(SqlConnectionString)) - { - conn.Open(); - - // retrive journal entry date - DateTime entryDate; - using (SqlCommand cmd = new SqlCommand(@" - SELECT - tblAccountJournal.EntryDate - FROM - tblAccountJournal - WHERE - (((tblAccountJournal.AccountJournalID)=@accountJournalId)); - ", conn)) - { - cmd.Parameters.AddWithValue("@accountJournalId", journalId); - - entryDate = DateTime.SpecifyKind((DateTime)cmd.ExecuteScalar(), DateTimeKind.Utc); - } - - // delete the original posts - using (SqlCommand cmd = new SqlCommand(@" - DELETE FROM - tblAccountJournalPost - WHERE - (((tblAccountJournalPost.AccountJournalID)=@accountJournalId)); - ", conn)) - { - cmd.Parameters.AddWithValue("@accountJournalId", journalId); - - cmd.ExecuteNonQuery(); - } - - //insert new posts - //bool postResult = AccountJournalPostInsert(sqlConnectionString, journalId, entryDate, currencyCode, amount, debitAccountId, creditAccountId); - bool postResult = new Data.Database.Account.CreateJournal().AccountJournalPostInsert(journalId, entryDate, currencyCode, amount, debitAccountId, creditAccountId); - - // update modified date on journal - using (SqlCommand cmd = new SqlCommand(@" - UPDATE - tblAccountJournal - SET - tblAccountJournal.LastModified=@utcNow - WHERE - (((tblAccountJournal.AccountJournalID)=@accountJournalId)); - ", conn)) - { - cmd.Parameters.AddWithValue("@accountJournalId", journalId); - cmd.Parameters.AddWithValue("@utcNow", DateTime.UtcNow); - - cmd.ExecuteNonQuery(); - } - - scope.Complete(); - } - return true; - } - - } -} diff --git a/src/bnhtrade.Core/Data/Database/Repository/Implementation/JournalRepository.cs b/src/bnhtrade.Core/Data/Database/Repository/Implementation/AccountJournalRepository.cs similarity index 55% rename from src/bnhtrade.Core/Data/Database/Repository/Implementation/JournalRepository.cs rename to src/bnhtrade.Core/Data/Database/Repository/Implementation/AccountJournalRepository.cs index 7646118..ab4c3ca 100644 --- a/src/bnhtrade.Core/Data/Database/Repository/Implementation/JournalRepository.cs +++ b/src/bnhtrade.Core/Data/Database/Repository/Implementation/AccountJournalRepository.cs @@ -11,12 +11,209 @@ using System.Transactions; namespace bnhtrade.Core.Data.Database.Repository.Implementation { - internal class JournalRepository : _Base, IJournalRepository + internal class AccountJournalRepository : _Base, IAccountJournalRepository { - public JournalRepository(IDbConnection connection, IDbTransaction transaction) : base(connection, transaction) + public AccountJournalRepository(IDbConnection connection, IDbTransaction transaction) : base(connection, transaction) { } + // + // create + // + public int AccountJournalInsert(int journalTypeId, DateTime entryDate, string currencyCode, decimal amount, int debitAccountId = 0, int creditAccountId = 0, bool lockEntry = false) + { + int defaultDebit = 0; + int defaultCredit = 0; + + // ensure date is UTC + entryDate = DateTime.SpecifyKind(entryDate, DateTimeKind.Utc); + + // debit and credit locks are checked in journal post method + using (TransactionScope scope = new TransactionScope()) + using (SqlConnection conn = new SqlConnection(SqlConnectionString)) + { + conn.Open(); + + // insert the journal entry + int journalId; + + using (SqlCommand cmd = new SqlCommand(@" + INSERT INTO tblAccountJournal + (AccountJournalTypeID, EntryDate, IsLocked) + OUTPUT INSERTED.AccountJournalID + VALUES + (@journalTypeId, @entryDate, @lockEntry) + ", conn)) + { + // add parameters + cmd.Parameters.AddWithValue("@journalTypeId", journalTypeId); + cmd.Parameters.AddWithValue("@entryDate", entryDate.ToUniversalTime()); + cmd.Parameters.AddWithValue("@lockEntry", lockEntry); + + //execute + journalId = (int)cmd.ExecuteScalar(); + } + + // insert journal entries + //bool postResult = AccountJournalPostInsert(sqlConnectionString, journalId, entryDate, currencyCode, amount, debitAccountId, creditAccountId); + bool postResult = AccountJournalPostInsert(journalId, entryDate, currencyCode, amount, debitAccountId, creditAccountId); + + scope.Complete(); + return journalId; + } + } + + /// + /// Old code needs sorting + /// + internal bool AccountJournalPostInsert(int journalId, DateTime entryDate, string currencyCode, decimal amount, int debitAccountId = 0, int creditAccountId = 0) + { + int defaultDebit; + int defaultCredit; + entryDate = DateTime.SpecifyKind(entryDate, DateTimeKind.Utc); + + using (TransactionScope scope = new TransactionScope()) + using (SqlConnection conn = new SqlConnection(SqlConnectionString)) + { + conn.Open(); + + // ensure their are no other entries + using (SqlCommand cmd = new SqlCommand(@" + SELECT + Count(tblAccountJournalPost.AccountJournalPostID) AS CountOfAccountJournalPostID + FROM + tblAccountJournalPost + WHERE + (((tblAccountJournalPost.AccountJournalID)=@AccountJournalID)); + ", conn)) + { + cmd.Parameters.AddWithValue("@AccountJournalID", journalId); + + int count = (int)cmd.ExecuteScalar(); + + if (count > 0) + { + throw new Exception("Unable the insert journal posts, post already present AccountJournalID=" + journalId); + } + } + + //checks + using (SqlCommand cmd = new SqlCommand(@" + SELECT + tblAccountJournalType.ChartOfAccountID_Debit, tblAccountJournalType.ChartOfAccountID_Credit + FROM + tblAccountJournal + INNER JOIN tblAccountJournalType + ON tblAccountJournal.AccountJournalTypeID = tblAccountJournalType.AccountJournalTypeID + WHERE + (((tblAccountJournal.AccountJournalID)=@journalId)); + ", conn)) + { + cmd.Parameters.AddWithValue("@journalId", journalId); + + using (SqlDataReader reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + // debit check + if (reader.IsDBNull(0)) + { + if (debitAccountId == 0) + { + throw new Exception("Debit Account ID required, default not set for journal type"); + } + } + else + { + defaultDebit = reader.GetInt32(0); + if (debitAccountId == 0) + { + debitAccountId = defaultDebit; + } + else if (debitAccountId != defaultDebit) + { + throw new Exception("Debit Account ID supplied does not match default set for journal type"); + } + + } + // credit check + if (reader.IsDBNull(1)) + { + if (creditAccountId == 0) + { + throw new Exception("Credit Account ID required, default not set for journal type"); + } + } + else + { + defaultCredit = reader.GetInt32(1); + if (creditAccountId == 0) + { + creditAccountId = defaultCredit; + } + else if (creditAccountId != defaultCredit) + { + throw new Exception("Credit Account ID supplied does not match default set for journal type"); + } + } + } + else + { + throw new Exception("AccountJournalID '" + journalId + "' does not exist."); + } + } + } + + // currency conversion + if (currencyCode != "GBP") + { + amount = new Logic.Account.CurrencyService().CurrencyConvertToGbp(currencyCode, amount, entryDate); + } + + // ensure decimal is rounded + amount = Math.Round(amount, 2); + + // insert debit post + using (SqlCommand cmd = new SqlCommand(@" + INSERT INTO tblAccountJournalPost + (AccountJournalID, AccountChartOfID, AmountGbp) + VALUES + (@AccountJournalId, @AccountChartOfId, @AmountGbp) + ", conn)) + { + // add parameters + cmd.Parameters.AddWithValue("@AccountJournalId", journalId); + cmd.Parameters.AddWithValue("@AccountChartOfId", debitAccountId); + cmd.Parameters.AddWithValue("@AmountGbp", amount); + + cmd.ExecuteNonQuery(); + } + + // insert credit post + using (SqlCommand cmd = new SqlCommand(@" + INSERT INTO tblAccountJournalPost + (AccountJournalID, AccountChartOfID, AmountGbp) + VALUES + (@AccountJournalId, @AccountChartOfId, @AmountGbp) + ", conn)) + { + // add parameters + cmd.Parameters.AddWithValue("@AccountJournalId", journalId); + cmd.Parameters.AddWithValue("@AccountChartOfId", creditAccountId); + cmd.Parameters.AddWithValue("@AmountGbp", (amount * -1)); + + cmd.ExecuteNonQuery(); + } + + scope.Complete(); + return true; + } + } + + // + // read + // + public Dictionary ReadJournal(List journalIdList) { var sqlBuilder = new SqlWhereBuilder(); @@ -146,7 +343,7 @@ namespace bnhtrade.Core.Data.Database.Repository.Implementation } // get journalTypes from db - var journalTypeDict = new JournalRepository(_connection, _transaction).ReadJournalType(journalTypeIdList); + var journalTypeDict = new AccountJournalRepository(_connection, _transaction).ReadJournalType(journalTypeIdList); // get accounts from db var accountDict = new AccountCodeRepository(_connection, _transaction).ReadAccountCode(accountIdList); @@ -325,6 +522,74 @@ namespace bnhtrade.Core.Data.Database.Repository.Implementation return returnList; } + // + // update + // + + public bool AccountJournalPostUpdate(int journalId, string currencyCode, decimal amount, int debitAccountId = 0, int creditAccountId = 0) + { + // retrive journal entry date + DateTime entryDate; + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT + tblAccountJournal.EntryDate + FROM + tblAccountJournal + WHERE + (((tblAccountJournal.AccountJournalID)=@accountJournalId));"; + + cmd.Parameters.AddWithValue("@accountJournalId", journalId); + + entryDate = DateTime.SpecifyKind((DateTime)cmd.ExecuteScalar(), DateTimeKind.Utc); + } + + // delete the original posts + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + DELETE FROM + tblAccountJournalPost + WHERE + (((tblAccountJournalPost.AccountJournalID)=@accountJournalId));"; + + cmd.Parameters.AddWithValue("@accountJournalId", journalId); + + cmd.ExecuteNonQuery(); + } + + //insert new posts + bool postResult = AccountJournalPostInsert(journalId, entryDate, currencyCode, amount, debitAccountId, creditAccountId); + + // update modified date on journal + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + UPDATE + tblAccountJournal + SET + tblAccountJournal.LastModified=@utcNow + WHERE + (((tblAccountJournal.AccountJournalID)=@accountJournalId));"; + + cmd.Parameters.AddWithValue("@accountJournalId", journalId); + cmd.Parameters.AddWithValue("@utcNow", DateTime.UtcNow); + + cmd.ExecuteNonQuery(); + } + + return true; + } + + + // + // delete + // + /// /// Old code needs sorting /// diff --git a/src/bnhtrade.Core/Data/Database/Repository/Implementation/PurchaseRepository.cs b/src/bnhtrade.Core/Data/Database/Repository/Implementation/PurchaseRepository.cs new file mode 100644 index 0000000..60c370e --- /dev/null +++ b/src/bnhtrade.Core/Data/Database/Repository/Implementation/PurchaseRepository.cs @@ -0,0 +1,189 @@ +using bnhtrade.Core.Data.Database.Repository.Interface; +using Microsoft.Data.SqlClient; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Transactions; + +namespace bnhtrade.Core.Data.Database.Repository.Implementation +{ + internal class PurchaseRepository : _Base, IPurchaseRepository + { + private static int accountJournalTypeIdNet = 1; + private static int accountJournalTypeIdTax = 2; + private static int stockJournalTypeId = 1; + private static int creditAccountId = 59; + private static int creditStatusId = 13; + private static int defaultAccountId = 138; + + public PurchaseRepository(IDbConnection connection, IDbTransaction transaction) : base(connection, transaction) + { + } + + public void WIP_PurchaseLineTransactionNetInsert(int purchaseLineId, string currencyCode, decimal amountNet, DateTime entryDate, int debitAccountId = 0) + { + // default to 'Inventory, Receivable/Processing' + if (debitAccountId < 1) + { + debitAccountId = defaultAccountId; + } + + // create account journal entry + int journalId = new Data.Database.Repository.Implementation.AccountJournalRepository(_connection, _transaction). + AccountJournalInsert(accountJournalTypeIdNet, entryDate, currencyCode, amountNet, debitAccountId); + + // add transaction to purchase line transaction table + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + INSERT INTO + tblPurchaseLineTransaction + ( PurchaseLineID, AccountJournalID ) + VALUES + ( @purchaseLineID, @accountJournalID );"; + + cmd.Parameters.AddWithValue("@purchaseLineID", purchaseLineId); + cmd.Parameters.AddWithValue("@accountJournalID", journalId); + + int count = cmd.ExecuteNonQuery(); + + if (count != 1) + { + throw new Exception("Failed to insert record to tblPurchaseLineTransaction table"); + } + } + } + + public void WIP_PurchaseLineTransactionNetUpdate(int accountJouranlId, string currencyCode, decimal amountNet, int debitAccountId) + { + // stock accountId check + if (debitAccountId == 86) + { + int count = 0; + + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT Count(tblStock.StockID) AS CountOfStockID + FROM tblStock + WHERE (((tblStock.AccountJournalID)=@accountJouranlId)); +"; + cmd.Parameters.AddWithValue("@accountJouranlId", accountJouranlId); + + count = (int)cmd.ExecuteScalar(); + } + + if (count == 0) + { + throw new Exception("Add account journal entry to stock before attempting this operation."); + } + else if (count > 1) + { + throw new Exception("Houston we have a problem! An account journal entry is assigned to " + count + " stock lines."); + } + } + + // make the update + bool result = new Data.Database.Repository.Implementation.AccountJournalRepository(_connection, _transaction). + AccountJournalPostUpdate(accountJouranlId, currencyCode, amountNet, debitAccountId, creditAccountId); + } + + // delete after testing.... + + //public void WIP_PurchaseLineTransactionStockDelete(string sqlConnectionString, int accountJournalId, int stockId) + //{ + // using (TransactionScope scope = new TransactionScope()) + // using (SqlConnection conn = new SqlConnection(sqlConnectionString)) + // { + // conn.Open(); + + // // get stock cost + // (int quantity, decimal totalCost) result = Stock.StockJournal.StockGetTotalQuantityAndCost(sqlConnectionString, stockId); + // decimal amount = result.totalCost; + + // // delete accountJournalId from stock table + // using (SqlCommand cmd = new SqlCommand(@" + // UPDATE tblStock + // SET AccountJournalID=Null + // WHERE StockID=@stockId AND AccountJournalID=@accountJournalId; + // ", conn)) + // { + // cmd.Parameters.AddWithValue("@stockId", stockId); + // cmd.Parameters.AddWithValue("@accountJournalId", accountJournalId); + + // int count = cmd.ExecuteNonQuery(); + + // if (count != 1) + // { + // throw new Exception("Integrity check failure! StockID=" + stockId + ", AccountJournalID=" + accountJournalId + + // " combination not found on stock table."); + // } + // } + + // // reset the account journal to default + // Account.AccountQuery.AccountJournalPostUpdate(sqlConnectionString, accountJournalId, "GBP", amount, defaultAccountId, creditAccountId); + + // // delete the stock entry + // Stock.StockCreate.WIP_StockDeleteSub(sqlConnectionString, stockId); + + // scope.Complete(); + // } + //} + + public void WIP_PurchaseLineTransactionDelete(int purchaseLineId, int accountJournalId) + { + // check accountJournalId does not exist in stock table + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT StockID + FROM tblStock + WHERE AccountJournalID=@accountJournalId;"; + + cmd.Parameters.AddWithValue("@accountJournalId", accountJournalId); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + if (reader.Read()) + { + throw new Exception("Integrity check failure! AccountJournalID=" + accountJournalId + " exists multiple time in stock table!"); + } + else + { + throw new Exception("Delete stock first before proceeding, AccountJournalID=" + accountJournalId); + } + } + } + } + + // delete line in purchase line transaction table + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + DELETE FROM tblPurchaseLineTransaction + WHERE AccountJournalID=@accountJournalId;"; + + cmd.Parameters.AddWithValue("@accountJournalId", accountJournalId); + + int count = cmd.ExecuteNonQuery(); + + if (count != 1) + { + throw new Exception("Operation cancelled, failed to delete entry in tblPurchaseLineTransaction WHERE AccountJournalID=" + accountJournalId); + } + } + + // delete account journal entry + new AccountJournalRepository(_connection, _transaction).DeleteJournal(accountJournalId); + } + } +} diff --git a/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockJournalRepository.cs b/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockJournalRepository.cs new file mode 100644 index 0000000..58aa142 --- /dev/null +++ b/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockJournalRepository.cs @@ -0,0 +1,717 @@ +using bnhtrade.Core.Data.Database._BoilerPlate; +using bnhtrade.Core.Data.Database.Repository.Interface; +using Microsoft.Data.SqlClient; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Transactions; + +namespace bnhtrade.Core.Data.Database.Repository.Implementation +{ + internal class StockJournalRepository : _Base, IStockJournalRepository + { + public StockJournalRepository(IDbConnection connection, IDbTransaction transaction) : base(connection, transaction) + { + } + + // + // Create + // + + // only to be assessable via code, use stock relocate to move stock bewtween status' + public int StockJournalInsert(int journalTypeId, int stockId, List<(int statusId, int quantity)> journalPosts, + DateTime entryDate, bool isNewStock = false) + { + /* + * TODO: currently the consistancy check checks the journal after the entry has been inserted to the db, if the check fails + * the transaction scope is disposed, and the ne journal entries roll back. However, if this is done within a higher + * 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 some slight performance benefits. + * + * 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. + */ + + + // balance and status IsCredit checks made by post insert function + + // create the journal entry + int stockJournalId; + //consitancy check is required? + bool consistencyRequired = true; + // get date of most recent debit for status' that I will be crediting + if (isNewStock == false) + { + // build sql string + string stringSql = @" + SELECT + tblStockJournal.EntryDate + FROM + tblStockJournal + INNER JOIN tblStockJournalPost + ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID + WHERE + tblStockJournal.StockID=@stockId + AND EntryDate>=@entryDate + AND tblStockJournalPost.Quantity>0 + AND ("; + + bool firstDone = false; + foreach (var item in journalPosts) + { + if (item.quantity < 0) + { + if (firstDone) + { + stringSql = stringSql + " OR "; + } + stringSql = stringSql + "tblStockJournalPost.StockStatusID=" + item.statusId; + firstDone = true; + } + } + stringSql = stringSql + ");"; + + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.CommandText = stringSql; + cmd.Transaction = _transaction as SqlTransaction; + + cmd.Parameters.AddWithValue("@stockId", stockId); + cmd.Parameters.AddWithValue("@entryDate", entryDate.ToUniversalTime()); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.HasRows) + { + consistencyRequired = true; + } + else + { + consistencyRequired = false; + } + } + } + } + + // create journal entry + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + INSERT INTO tblStockJournal ( EntryDate, StockJournalTypeID, StockID, IsLocked ) + OUTPUT INSERTED.StockJournalID + VALUES ( @EntryDate, @journalTypeId, @stockID, @isLocked );"; + + cmd.Parameters.AddWithValue("@stockID", stockId); + cmd.Parameters.AddWithValue("@journalTypeId", journalTypeId); + cmd.Parameters.AddWithValue("@EntryDate", entryDate.ToUniversalTime()); + cmd.Parameters.AddWithValue("@isLocked", isNewStock); + + //execute + stockJournalId = (int)cmd.ExecuteScalar(); + } + + // insert journal posts into database + //new Data.Database.Stock + Core.Stock.StockJournal.StockJournalPostInsert(conn, stockId, stockJournalId, journalPosts, isNewStock); + + // consistency check + bool consistencyResult = true; + if (consistencyRequired) + { + consistencyResult = false; + // build list of effected status' + var statusIdEffected = new List(); + foreach (var item in journalPosts) + { + if (item.quantity < 0) + { + statusIdEffected.Add(item.statusId); + } + } + // run check + consistencyResult = Stock.StockJournal.WIP_StockJournalConsistencyCheck(sqlConnectionString, stockId, statusIdEffected); + } + + if (consistencyResult) + { + // commit + scope.Complete(); + return stockJournalId; + } + else + { + throw new Exception("Unable to insert stock journal entry, consistancy check failed."); + } + } + + + + + + // + // Read + // + + public int ReadStatusBalanceBySku(string sku, int statusId) + { + int statusBalance = new int(); + + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT SUM(tblStockJournalPost.Quantity) AS Balance + FROM tblStockJournal INNER JOIN + tblStockJournalPost ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID INNER JOIN + tblStock ON tblStockJournal.StockID = tblStock.StockID INNER JOIN + tblSku ON tblStock.SkuID = tblSku.skuSkuID + WHERE (tblStockJournalPost.StockStatusID = @statusId) AND (tblSku.skuSkuNumber = @sku);"; + + cmd.Parameters.AddWithValue("@sku", sku); + cmd.Parameters.AddWithValue("@statusId", statusId); + + // execute + object obj = cmd.ExecuteScalar(); + if (obj == null || obj == DBNull.Value) + { + statusBalance = 0; + } + else + { + statusBalance = (int)obj; + } + } + return statusBalance; + } + + public int ReadStatusBalanceByStockNumber(int stockNumber, int statusId) + { + int statusBalance = new int(); + + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT SUM(tblStockJournalPost.Quantity) AS Balance + FROM tblStockJournal INNER JOIN + tblStockJournalPost ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID INNER JOIN + tblStock ON tblStockJournal.StockID = tblStock.StockID + WHERE (tblStockJournalPost.StockStatusID = @statusId) AND (tblStock.StockNumber = @stockNumber);"; + + cmd.Parameters.AddWithValue("@stockNumber", stockNumber); + cmd.Parameters.AddWithValue("@statusId", statusId); + + // execute + object obj = cmd.ExecuteScalar(); + if (obj == null || obj == DBNull.Value) + { + statusBalance = 0; + } + else + { + statusBalance = (int)obj; + } + } + return statusBalance; + } + + public int ReadStatusBalanceByStockId(int stockId, int statusId) + { + int statusBalance = new int(); + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT + SUM(tblStockJournalPost.Quantity) AS Balance + FROM + tblStockJournal + INNER JOIN tblStockJournalPost + ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID + WHERE + (tblStockJournal.StockID = @stockId ) + AND (tblStockJournalPost.StockStatusID = @statusId );"; + + cmd.Parameters.AddWithValue("@stockId", stockId); + cmd.Parameters.AddWithValue("@statusId", statusId); + + // execute + object obj = cmd.ExecuteScalar(); + if (obj == null || obj == DBNull.Value) + { + statusBalance = 0; + } + else + { + statusBalance = (int)obj; + } + } + return statusBalance; + } + + // + // update + // + + + // + // Delete + // + + public void StockJournalDelete(int stockJournalId) + { + // get date for journal entry + DateTime entryDate; + int stockId; + using (SqlCommand cmd = new SqlCommand(@" + SELECT tblStockJournal.EntryDate, StockID + FROM tblStockJournal + WHERE (((tblStockJournal.StockJournalID)=@stockJournalId)); + ", conn)) + { + // add parameters + cmd.Parameters.AddWithValue("@stockJournalId", stockJournalId); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + entryDate = DateTime.SpecifyKind(reader.GetDateTime(0), DateTimeKind.Utc); + stockId = reader.GetInt32(1); + } + else + { + throw new Exception("StockJournalID=" + stockJournalId + " does not exist!"); + } + } + + } + + // is consistancy check required + bool consistancyCheck; + // build list of debits that are to be deleted + var debitList = new List(); + using (SqlCommand cmd = new SqlCommand(@" + SELECT tblStockJournalPost.StockStatusID + FROM tblStockJournalPost + WHERE (((tblStockJournalPost.StockJournalID)=@stockJournalId) AND ((tblStockJournalPost.Quantity)>0)); + ", conn)) + { + // add parameters + cmd.Parameters.AddWithValue("@stockJournalId", stockJournalId); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.HasRows) + { + while (reader.Read()) + { + debitList.Add(reader.GetInt32(0)); + } + } + else + { + throw new Exception("StockJournalID=" + stockJournalId + " has no debits with quantity greater than zero!"); + } + } + } + + // check no credits for stockId & debit combination have been made since delete entry + string stringSql = @" + SELECT + tblStockJournal.EntryDate + FROM + tblStockJournal + INNER JOIN tblStockJournalPost + ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID + WHERE + tblStockJournal.StockID=@stockId + AND tblStockJournalPost.Quantity<0 + AND EntryDate>=@entryDate + AND ("; + + bool firstDone = false; + foreach (var item in debitList) + { + if (firstDone) + { + stringSql = stringSql + " OR "; + } + stringSql = stringSql + "tblStockJournalPost.StockStatusID=" + item; + firstDone = true; + } + stringSql = stringSql + ");"; + + using (SqlCommand cmd = new SqlCommand(stringSql, conn)) + { + cmd.Parameters.AddWithValue("@stockId", stockId); + cmd.Parameters.AddWithValue("@entryDate", entryDate.ToUniversalTime()); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.HasRows) + { + consistancyCheck = true; + } + else + { + consistancyCheck = false; + } + } + } + + // delete the posts + StockJournalPostDelete(conn, stockJournalId); + + // delete journal entry + using (SqlCommand cmd = new SqlCommand(@" + DELETE FROM tblStockJournal + WHERE StockJournalID=@stockJournalId; + ", conn)) + { + // add parameters + cmd.Parameters.AddWithValue("@stockJournalId", stockJournalId); + + int count = cmd.ExecuteNonQuery(); + + if (count != 1) + { + throw new Exception("Failed to delete stock journal header."); + } + } + + // consistanct check + bool consistencyResult = true; + if (consistancyCheck) + { + // run check + consistencyResult = Stock.StockJournal.WIP_StockJournalConsistencyCheck(sqlConnectionString, stockId, debitList); + } + + if (consistencyResult) + { + // commit + scope.Complete(); + } + else + { + throw new Exception("Unable to delete stock journal entry, consistancy check failed."); + } + } + + private void StockJournalPostInsert(int stockId, int stockJournalId, + List<(int statusId, int quantity)> journalPosts, bool isNewStock = false) + { + //checks + if (journalPosts.Count > 2) + { + // I have purposely made the code to accept split transaction incase of future requirements, however, db design is simpler this way. + throw new Exception("Stock journal does not currently support split transactions (greater than two posts)." + journalPosts.Count + " number posts attempted."); + } + else if (journalPosts.Count < 2) + { + // list not long enough + throw new Exception("Stock journal entry requires minium of two posts, entry of " + journalPosts.Count + " number posts attempted."); + } + + if (journalPosts.Sum(item => item.quantity) != 0) + { + // credits and debits do not match + throw new Exception("Sum of credits and debits do not resolve to zero."); + } + + // group credits and debits by status + var dicStatusQty = new Dictionary(); + foreach (var post in journalPosts) + { + if (dicStatusQty.ContainsKey(post.statusId) == false) + { + dicStatusQty.Add(post.statusId, post.quantity); + } + else + { + dicStatusQty[post.statusId] = dicStatusQty[post.statusId] + post.quantity; + } + } + + // get isCreditOnly for each status + var dicStatusIsCreditOnly = new Dictionary(); + foreach (var item in dicStatusQty) + { + using (SqlCommand cmd = new SqlCommand(@" + SELECT IsCreditOnly FROM tblStockStatus WHERE StockStatusID=@statusId; + ", conn)) + { + cmd.Parameters.AddWithValue("@statusId", item.Key); + + dicStatusIsCreditOnly.Add(item.Key, (bool)cmd.ExecuteScalar()); + } + } + + // check there is only one IsCreditOnly in list and it is allowed in this instance + int isCreditOnlyCount = 0; + foreach (var item in dicStatusIsCreditOnly) + { + if (item.Value) + { + isCreditOnlyCount = isCreditOnlyCount + 1; + } + } + if (isNewStock == false && isCreditOnlyCount > 0) + { + throw new Exception("Attempted credit or debit to 'Is Credit Only' status not allowed, in this instance."); + } + if (isNewStock == true && isCreditOnlyCount != 1) + { + throw new Exception("StockID=" + stockId + ", each stock line must have only have one IsCreditOnly=True status assigned to it."); + } + + // ensure debit (or zero credit) isn't made to credit only status + // need to do this check via original post list (i.e. journalPosts) + foreach (var post in journalPosts) + { + // debit check + if (post.quantity >= 0) + { + if (dicStatusIsCreditOnly[post.statusId] == true) + { + throw new Exception("Cannot make debit, or zero quantity credit, to credit only status. StatusID=" + post.statusId); + } + } + } + + // balance check for any credits (that are not isCreditOnly=true) + foreach (var item in dicStatusQty) + { + if (item.Value < 0 && dicStatusIsCreditOnly[item.Key] == false) + { + int quantity = new Data.Database.Stock.ReadStatusBalance().ReadStatusBalanceByStockId(stockId, item.Key); + + if (quantity + item.Value < 0) + { + throw new Exception("Credit status balance check failed. Available balance " + quantity + ", attempted credit " + item.Value * -1 + "."); + } + } + } + + // get this far... + // insert journal posts into database + foreach (var post in journalPosts) + { + using (SqlCommand cmd = new SqlCommand(@" + INSERT INTO tblStockJournalPost ( StockJournalID, StockStatusID, Quantity ) + VALUES ( @StockJournalId, @stockStatudId, @quantity ); + ", conn)) + { + cmd.Parameters.AddWithValue("@StockJournalId", stockJournalId); + cmd.Parameters.AddWithValue("@stockStatudId", post.statusId); + cmd.Parameters.AddWithValue("@quantity", post.quantity); + + // execute + cmd.ExecuteNonQuery(); + } + } + } + + private void StockJournalPostDelete(int stockJournalId) + { + using (SqlCommand cmd = new SqlCommand(@" + DELETE FROM tblStockJournalPost + WHERE StockJournalID=@stockJournalId + ", conn)) + { + cmd.Parameters.AddWithValue("@StockJournalId", stockJournalId); + + // execute + cmd.ExecuteNonQuery(); + + // the calling method must compete any transaction-scope on the connection + } + } + + public bool WIP_StockJournalConsistencyCheck( int stockId, List statusIdEffected = null) + { + if (statusIdEffected == null) + { + statusIdEffected = new List(); + } + + // if no list supplied, build list of all used status' for stockId + if (statusIdEffected.Count == 0) + { + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT + tblStockJournalPost.StockStatusID + FROM + tblStockJournal + INNER JOIN tblStockJournalPost + ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID + WHERE + tblStockJournal.StockID=@stockId + GROUP BY + tblStockJournalPost.StockStatusID;"; + + cmd.Parameters.AddWithValue("@stockId", stockId); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.HasRows) + { + while (reader.Read()) + { + statusIdEffected.Add(reader.GetInt32(0)); + } + } + else + { + throw new Exception("No stock journal entries exist for StockID=" + stockId); + } + } + } + + // build the sql string to build creditCreate bool + var dicStatusCreditOnly = new Dictionary(); + string sqlString = @" + SELECT + tblStockStatus.StockStatusID, tblStockStatus.IsCreditOnly + FROM + tblStockStatus "; + + for (var i = 0; i < statusIdEffected.Count; i++) + { + if (i == 0) + { + sqlString = sqlString + " WHERE tblStockStatus.StockStatusID=" + statusIdEffected[i]; + } + else + { + sqlString = sqlString + " OR tblStockStatus.StockStatusID=" + statusIdEffected[i]; + } + + //if (i == (statusIdEffected.Count - 1)) + //{ + // sqlString = sqlString + ";"; + //} + } + sqlString = sqlString + " GROUP BY tblStockStatus.StockStatusID, tblStockStatus.IsCreditOnly;"; + + // run query & build dictionaries + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = sqlString; + cmd.Parameters.AddWithValue("@stockId", stockId); + + using (SqlDataReader reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + dicStatusCreditOnly.Add(reader.GetInt32(0), reader.GetBoolean(1)); + + while (reader.Read()) + { + dicStatusCreditOnly.Add(reader.GetInt32(0), reader.GetBoolean(1)); + } + } + else + { + throw new Exception("Error, no journal entries found for StockID=" + stockId); + } + } + } + + // check integrity of supplied statusIds + foreach (int statusId in statusIdEffected) + { + if (!dicStatusCreditOnly.ContainsKey(statusId)) + { + throw new Exception("Supplied StatusId (" + statusId + ") doesn't exist for StockId=" + stockId); + } + } + + // loop through each statudId and check integrity, if createdEnabled=false + foreach (int statudId in statusIdEffected) + { + if (dicStatusCreditOnly[statudId] == false) + { + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT + tblStockJournal.EntryDate, tblStockJournalPost.Quantity + FROM + tblStockJournal + INNER JOIN tblStockJournalPost + ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID + WHERE + tblStockJournal.StockID=@stockId + AND tblStockJournalPost.StockStatusID=@statudId + ORDER BY + tblStockJournal.EntryDate;"; + + cmd.Parameters.AddWithValue("@stockId", stockId); + cmd.Parameters.AddWithValue("@statudId", statudId); + + using (SqlDataReader reader = cmd.ExecuteReader()) + { + // read first line into variables + reader.Read(); + int quantity = reader.GetInt32(1); + DateTime entryDate = DateTime.SpecifyKind(reader.GetDateTime(0), DateTimeKind.Utc); + + while (true) + { + // compare to next values + if (reader.Read()) + { + DateTime nextEntryDate = DateTime.SpecifyKind(reader.GetDateTime(0), DateTimeKind.Utc); + // don't check quantites for transactions on same date + if (entryDate == nextEntryDate) + { + entryDate = nextEntryDate; + quantity = quantity + reader.GetInt32(1); + } + // check if dates are different + else + { + if (quantity < 0) + { + return false; + } + else + { + entryDate = nextEntryDate; + quantity = quantity + reader.GetInt32(1); + } + } + } + // end if no records, check quantity + else + { + if (quantity < 0) + { + return false; + } + break; + } + } + } + } + } + } + } + // get this far, all good + return true; + } + } +} diff --git a/src/bnhtrade.Core/Data/Database/Repository/Implementation/_BoilerPlate.cs b/src/bnhtrade.Core/Data/Database/Repository/Implementation/_BoilerPlate.cs index c7f4974..03ae6da 100644 --- a/src/bnhtrade.Core/Data/Database/Repository/Implementation/_BoilerPlate.cs +++ b/src/bnhtrade.Core/Data/Database/Repository/Implementation/_BoilerPlate.cs @@ -44,8 +44,8 @@ namespace bnhtrade.Core.Data.Database.Repository.Implementation using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) { - cmd.CommandText = sql; cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = sql; sqlwhere.AddParametersToSqlCommand(cmd); diff --git a/src/bnhtrade.Core/Data/Database/Repository/Interface/IJournalRepository.cs b/src/bnhtrade.Core/Data/Database/Repository/Interface/IAccountJournalRepository.cs similarity index 56% rename from src/bnhtrade.Core/Data/Database/Repository/Interface/IJournalRepository.cs rename to src/bnhtrade.Core/Data/Database/Repository/Interface/IAccountJournalRepository.cs index 8ad3d04..6de9735 100644 --- a/src/bnhtrade.Core/Data/Database/Repository/Interface/IJournalRepository.cs +++ b/src/bnhtrade.Core/Data/Database/Repository/Interface/IAccountJournalRepository.cs @@ -6,8 +6,10 @@ using System.Threading.Tasks; namespace bnhtrade.Core.Data.Database.Repository.Interface { - internal interface IJournalRepository + internal interface IAccountJournalRepository { + int AccountJournalInsert(int journalTypeId, DateTime entryDate, string currencyCode, decimal amount, int debitAccountId = 0, int creditAccountId = 0, bool lockEntry = false); + bool AccountJournalPostInsert(int journalId, DateTime entryDate, string currencyCode, decimal amount, int debitAccountId = 0, int creditAccountId = 0); Dictionary ReadJournal(List journalIdList); bool ReadJournalIsLocked(int journalId); Dictionary ReadJournalType(List journalTypeIds = null, List typeTitles = null); diff --git a/src/bnhtrade.Core/Data/Database/Repository/Interface/IPurchaseRepository.cs b/src/bnhtrade.Core/Data/Database/Repository/Interface/IPurchaseRepository.cs new file mode 100644 index 0000000..4e6bbbb --- /dev/null +++ b/src/bnhtrade.Core/Data/Database/Repository/Interface/IPurchaseRepository.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace bnhtrade.Core.Data.Database.Repository.Interface +{ + internal interface IPurchaseRepository + { + void WIP_PurchaseLineTransactionNetInsert(int purchaseLineId, string currencyCode, decimal amountNet, DateTime entryDate, int debitAccountId = 0); + void WIP_PurchaseLineTransactionNetUpdate(int accountJouranlId, string currencyCode, decimal amountNet, int debitAccountId); + void WIP_PurchaseLineTransactionDelete(int purchaseLineId, int accountJournalId); + + } +} diff --git a/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockJournalRepository.cs b/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockJournalRepository.cs new file mode 100644 index 0000000..54e4296 --- /dev/null +++ b/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockJournalRepository.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace bnhtrade.Core.Data.Database.Repository.Interface +{ + internal interface IStockJournalRepository + { + int ReadStatusBalanceBySku(string sku, int statusId); + int ReadStatusBalanceByStockNumber(int stockNumber, int statusId); + int ReadStatusBalanceByStockId(int stockId, int statusId); + Dictionary ReadStatusBalanceByStatusId(int statusId); + void StockJournalDelete(int stockJournalId); + void StockJournalPostInsert(int stockId, int stockJournalId, List<(int statusId, int quantity)> journalPosts, bool isNewStock = false); + void StockJournalPostDelete(int stockJournalId); + bool WIP_StockJournalConsistencyCheck(int stockId, List statusIdEffected = null); + } +} diff --git a/src/bnhtrade.Core/Data/Database/Stock/JournalCrud.cs b/src/bnhtrade.Core/Data/Database/Stock/JournalCrud.cs index 65b0dfe..4364612 100644 --- a/src/bnhtrade.Core/Data/Database/Stock/JournalCrud.cs +++ b/src/bnhtrade.Core/Data/Database/Stock/JournalCrud.cs @@ -373,7 +373,7 @@ namespace bnhtrade.Core.Data.Database.Stock { if (item.Value < 0 && dicStatusIsCreditOnly[item.Key] == false) { - int quantity = new Data.Database.Stock.ReadStatusBalance().ByStockId(stockId, item.Key); + int quantity = new Data.Database.Stock.ReadStatusBalance().ReadStatusBalanceByStockId(stockId, item.Key); if (quantity + item.Value < 0) { diff --git a/src/bnhtrade.Core/Data/Database/Stock/ReadStatusBalance.cs b/src/bnhtrade.Core/Data/Database/Stock/ReadStatusBalance.cs index 93ae836..0d30750 100644 --- a/src/bnhtrade.Core/Data/Database/Stock/ReadStatusBalance.cs +++ b/src/bnhtrade.Core/Data/Database/Stock/ReadStatusBalance.cs @@ -13,76 +13,7 @@ namespace bnhtrade.Core.Data.Database.Stock { } - public int BySku(string sku, int statusId) - { - int statusBalance = new int(); - using (SqlConnection conn = new SqlConnection(SqlConnectionString)) - { - conn.Open(); - - using (SqlCommand cmd = new SqlCommand(@" - SELECT SUM(tblStockJournalPost.Quantity) AS Balance - FROM tblStockJournal INNER JOIN - tblStockJournalPost ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID INNER JOIN - tblStock ON tblStockJournal.StockID = tblStock.StockID INNER JOIN - tblSku ON tblStock.SkuID = tblSku.skuSkuID - WHERE (tblStockJournalPost.StockStatusID = @statusId) AND (tblSku.skuSkuNumber = @sku) - ", conn)) - { - // add parameters - cmd.Parameters.AddWithValue("@sku", sku); - cmd.Parameters.AddWithValue("@statusId", statusId); - - // execute - object obj = cmd.ExecuteScalar(); - if (obj == null || obj == DBNull.Value) - { - statusBalance = 0; - } - else - { - statusBalance = (int)obj; - } - } - } - return statusBalance; - } - - public int ByStockNumber(int stockNumber, int statusId) - { - int statusBalance = new int(); - using (SqlConnection conn = new SqlConnection(SqlConnectionString)) - { - conn.Open(); - - using (SqlCommand cmd = new SqlCommand(@" - SELECT SUM(tblStockJournalPost.Quantity) AS Balance - FROM tblStockJournal INNER JOIN - tblStockJournalPost ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID INNER JOIN - tblStock ON tblStockJournal.StockID = tblStock.StockID - WHERE (tblStockJournalPost.StockStatusID = @statusId) AND (tblStock.StockNumber = @stockNumber) - ", conn)) - { - // add parameters - cmd.Parameters.AddWithValue("@stockNumber", stockNumber); - cmd.Parameters.AddWithValue("@statusId", statusId); - - // execute - object obj = cmd.ExecuteScalar(); - if (obj == null || obj == DBNull.Value) - { - statusBalance = 0; - } - else - { - statusBalance = (int)obj; - } - } - } - return statusBalance; - } - - public int ByStockId(int stockId, int statusId) + public int ReadStatusBalanceByStockId(int stockId, int statusId) { int statusBalance = new int(); using (SqlConnection conn = new SqlConnection(SqlConnectionString)) @@ -125,7 +56,7 @@ namespace bnhtrade.Core.Data.Database.Stock /// /// The stock status id /// Dictionary of SKUs and the balance of each - public Dictionary ByStatusId(int statusId) + public Dictionary ReadStatusBalanceByStatusId(int statusId) { var returnList = new Dictionary(); diff --git a/src/bnhtrade.Core/Data/Database/UnitOfWork/IUnitOfWork.cs b/src/bnhtrade.Core/Data/Database/UnitOfWork/IUnitOfWork.cs index cfe61d8..4f5dd05 100644 --- a/src/bnhtrade.Core/Data/Database/UnitOfWork/IUnitOfWork.cs +++ b/src/bnhtrade.Core/Data/Database/UnitOfWork/IUnitOfWork.cs @@ -16,9 +16,11 @@ namespace bnhtrade.Core.Data.Database.UnitOfWork IExportInvoiceRepository ExportInvoiceRepository { get; } IAmazonSettlementRepository AmazonSettlementRepository { get; } IInvoiceRepository InvoiceRepository { get; } - IJournalRepository JournalRepository { get; } + IPurchaseRepository PurchaseRepository { get; } + IAccountJournalRepository JournalRepository { get; } ISequenceGenerator SequenceGenerator { get; } ISkuRepository SkuRepository { get; } + IStockJournalRepository StockJournalRepository { get; } IStockRepository StockRepository { get; } // Methods to manage the transaction diff --git a/src/bnhtrade.Core/Data/Database/UnitOfWork/UnitOfWork.cs b/src/bnhtrade.Core/Data/Database/UnitOfWork/UnitOfWork.cs index 4ab6005..9cbf4cf 100644 --- a/src/bnhtrade.Core/Data/Database/UnitOfWork/UnitOfWork.cs +++ b/src/bnhtrade.Core/Data/Database/UnitOfWork/UnitOfWork.cs @@ -24,9 +24,11 @@ namespace bnhtrade.Core.Data.Database.UnitOfWork private ICurrencyRepository _currencyRepository; private IExportInvoiceRepository _exportInvoiceRepository; private IInvoiceRepository _invoiceRepository; - private IJournalRepository _journalRepository; + private IPurchaseRepository _purchaseRepository; + private IAccountJournalRepository _journalRepository; private ISequenceGenerator _sequenceGenerator; private ISkuRepository _skuRepository; + private IStockJournalRepository _stockJournalRespository; private IStockRepository _stockRepository; internal UnitOfWork() @@ -108,13 +110,25 @@ namespace bnhtrade.Core.Data.Database.UnitOfWork } } - public IJournalRepository JournalRepository + public IPurchaseRepository PurchaseRepository + { + get + { + if (_purchaseRepository == null) + { + _purchaseRepository = new PurchaseRepository(_connection, _transaction); + } + return _purchaseRepository; + } + } + + public IAccountJournalRepository JournalRepository { get { if (_journalRepository == null) { - _journalRepository = new JournalRepository(_connection, _transaction); + _journalRepository = new AccountJournalRepository(_connection, _transaction); } return _journalRepository; } @@ -144,6 +158,18 @@ namespace bnhtrade.Core.Data.Database.UnitOfWork } } + public IStockJournalRepository StockJournalRepository + { + get + { + if (_stockJournalRespository == null) + { + _stockJournalRespository = new StockJournalRepository(_connection, _transaction); + } + return _stockJournalRespository; + } + } + public IStockRepository StockRepository { get diff --git a/src/bnhtrade.Core/Logic/Account/Journal.cs b/src/bnhtrade.Core/Logic/Account/Journal.cs deleted file mode 100644 index 8cd434b..0000000 --- a/src/bnhtrade.Core/Logic/Account/Journal.cs +++ /dev/null @@ -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; - }); - } - } -} diff --git a/src/bnhtrade.Core/Logic/Account/JournalService.cs b/src/bnhtrade.Core/Logic/Account/JournalService.cs new file mode 100644 index 0000000..a9fd445 --- /dev/null +++ b/src/bnhtrade.Core/Logic/Account/JournalService.cs @@ -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; + }); + } + } +} diff --git a/src/bnhtrade.Core/Logic/Sku/SkuService.cs b/src/bnhtrade.Core/Logic/Inventory/SkuService.cs similarity index 88% rename from src/bnhtrade.Core/Logic/Sku/SkuService.cs rename to src/bnhtrade.Core/Logic/Inventory/SkuService.cs index 2ad1696..9151c22 100644 --- a/src/bnhtrade.Core/Logic/Sku/SkuService.cs +++ b/src/bnhtrade.Core/Logic/Inventory/SkuService.cs @@ -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) { } + /// /// Used for retriving an SKU ID by parameters. If no match, can create a new SKU if required. /// diff --git a/src/bnhtrade.Core/Logic/Inventory/StockJournalService.cs b/src/bnhtrade.Core/Logic/Inventory/StockJournalService.cs new file mode 100644 index 0000000..cb974cf --- /dev/null +++ b/src/bnhtrade.Core/Logic/Inventory/StockJournalService.cs @@ -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 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); + }); + } + } +} diff --git a/src/bnhtrade.Core/Logic/Inventory/StockService.cs b/src/bnhtrade.Core/Logic/Inventory/StockService.cs new file mode 100644 index 0000000..c20588f --- /dev/null +++ b/src/bnhtrade.Core/Logic/Inventory/StockService.cs @@ -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); + } + + } +} diff --git a/src/bnhtrade.Core/Logic/Inventory/StockStatusService.cs b/src/bnhtrade.Core/Logic/Inventory/StockStatusService.cs new file mode 100644 index 0000000..55f8bb4 --- /dev/null +++ b/src/bnhtrade.Core/Logic/Inventory/StockStatusService.cs @@ -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) { } + + /// + /// 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). + /// + /// SKU number + /// Status ID + /// Balance as quantity + 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); + }); + } + + + } +} diff --git a/src/bnhtrade.Core/Logic/Purchase/PurchaseService.cs b/src/bnhtrade.Core/Logic/Purchase/PurchaseService.cs new file mode 100644 index 0000000..ea1cc8e --- /dev/null +++ b/src/bnhtrade.Core/Logic/Purchase/PurchaseService.cs @@ -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); + }); + } + } +} diff --git a/src/bnhtrade.Core/Logic/Stock/StatusBalance.cs b/src/bnhtrade.Core/Logic/Stock/StatusBalance.cs index d5fa3d6..34c7330 100644 --- a/src/bnhtrade.Core/Logic/Stock/StatusBalance.cs +++ b/src/bnhtrade.Core/Logic/Stock/StatusBalance.cs @@ -12,22 +12,9 @@ namespace bnhtrade.Core.Logic.Stock { } - /// - /// 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). - /// - /// SKU number - /// Status ID - /// Balance as quantity - private int GetAvailableBalanceBySku(string sku, int statusId) - { - return new Data.Database.Stock.ReadStatusBalance().BySku(sku, statusId); - } - /// /// 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 /// /// SKU number /// Status ID @@ -131,7 +118,7 @@ namespace bnhtrade.Core.Logic.Stock /// Dictionary of SKUs and the repective balance of each public Dictionary GetSkuQuantity(int statusId) { - return new Data.Database.Stock.ReadStatusBalance().ByStatusId(statusId); + return new Data.Database.Stock.ReadStatusBalance().ReadStatusBalanceByStatusId(statusId); } } } diff --git a/src/bnhtrade.Core/Program.cs b/src/bnhtrade.Core/Program.cs index c3fa03d..60f95b5 100644 --- a/src/bnhtrade.Core/Program.cs +++ b/src/bnhtrade.Core/Program.cs @@ -1,4 +1,5 @@ -using bnhtrade.Core.Logic; +using bnhtrade.Core.Data.Database.UnitOfWork; +using bnhtrade.Core.Logic; using Microsoft.Data.SqlClient; using System; using System.Collections.Generic; @@ -234,573 +235,8 @@ namespace bnhtrade.Core namespace Stock { - public class StockCreate : UnitOfWorkBase + public class StockJournal : UnitOfWorkBase { - private int WIP_StockInsert(string sqlConnectionString, 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.Journal().AccountJournalInsert(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(string sqlConnectionString, int productId, int conditionId, int accountTaxCodeId, - int accountJournalId, int stockJournalTypeId, DateTime stockJournalEntryDate, int quantity, int statusDebitId) - { - stockJournalEntryDate = DateTime.SpecifyKind(stockJournalEntryDate, DateTimeKind.Utc); - - using (TransactionScope scope = new TransactionScope()) - using (SqlConnection conn = new SqlConnection(sqlConnectionString)) - { - conn.Open(); - - // 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.Sku.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 static void WIP_StockDelete(string sqlConnectionString, 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 - Stock.StockCreate.WIP_StockDeleteSubAccountJournalEntry(sqlConnectionString, stockId); - - // remove stock - Stock.StockCreate.WIP_StockDeleteSub(sqlConnectionString, stockId); - - scope.Complete(); - } - } - - private static void WIP_StockDeleteSub(string sqlConnectionString, 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 - 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 static void WIP_StockDeleteSubAccountJournalEntry(string sqlConnectionString, 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.JournalRepository(conn, trans).DeleteJournal(accountJournalId); - - trans.Commit(); - } - } - - public static int WIP_StockInsertPurchase(string sqlConnectionString, 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 new Stock.StockCreate().WIP_StockInsertSub(sqlConnectionString, productId, conditionId, accountTaxCodeId, accountJournalId, stockJournalTypeId, stockJournalEntryDate, quantity, statusDebitId); - } - - public static int WIP_StockInsertOwnerIntroduced(string sqlConnectionString, decimal amount, int quantity, int productId, int conditionId, int accountTaxCodeId, DateTime entryDate, int debitStatusId) - { - int accountJournalType = 3; - int stockJournalType = 2; - string currencyCode = "GBP"; - - return new Stock.StockCreate().WIP_StockInsert(sqlConnectionString, accountJournalType, stockJournalType, currencyCode, amount, quantity, productId, conditionId, accountTaxCodeId, entryDate, debitStatusId); - } - - public static void WIP_StockDeletePurchase(string sqlConnectionString, int stockId) - { - WIP_StockDeleteSub(sqlConnectionString, stockId); - } - - public static void WIP_StockDeleteOwnerIntroduced(string sqlConnectionString, int stockId) - { - WIP_StockDelete(sqlConnectionString, stockId); - } - - } - - public class StockJournal - { - // only to be assessable via code, use stock relocate to move stock bewtween status' - public static int StockJournalInsert(string sqlConnectionString, int journalTypeId, int stockId, List<(int statusId, int quantity)> journalPosts, - DateTime entryDate, bool isNewStock = false) - { - /* - * TODO: currently the consistancy check checks the journal after the entry has been inserted to the db, if the check fails - * the transaction scope is disposed, and the ne journal entries roll back. However, if this is done within a higher - * 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 some slight performance benefits. - * - * 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. - */ - - - // balance and status IsCredit checks made by post insert function - - // create the journal entry - int stockJournalId; - using (TransactionScope scope = new TransactionScope()) - using (SqlConnection conn = new SqlConnection(sqlConnectionString)) - { - conn.Open(); - - //consitancy check is required? - bool consistencyRequired = true; - // get date of most recent debit for status' that I will be crediting - if (isNewStock == false) - { - // build sql string - string stringSql = @" - SELECT - tblStockJournal.EntryDate - FROM - tblStockJournal - INNER JOIN tblStockJournalPost - ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID - WHERE - tblStockJournal.StockID=@stockId - AND EntryDate>=@entryDate - AND tblStockJournalPost.Quantity>0 - AND ("; - - bool firstDone = false; - foreach (var item in journalPosts) - { - if (item.quantity < 0) - { - if (firstDone) - { - stringSql = stringSql + " OR "; - } - stringSql = stringSql + "tblStockJournalPost.StockStatusID=" + item.statusId; - firstDone = true; - } - } - stringSql = stringSql + ");"; - - using (SqlCommand cmd = new SqlCommand(stringSql, conn)) - { - cmd.Parameters.AddWithValue("@stockId", stockId); - cmd.Parameters.AddWithValue("@entryDate", entryDate.ToUniversalTime()); - - using (var reader = cmd.ExecuteReader()) - { - if (reader.HasRows) - { - consistencyRequired = true; - } - else - { - consistencyRequired = false; - } - } - } - } - - // create journal entry - using (SqlCommand cmd = new SqlCommand(@" - INSERT INTO tblStockJournal ( EntryDate, StockJournalTypeID, StockID, IsLocked ) - OUTPUT INSERTED.StockJournalID - VALUES ( @EntryDate, @journalTypeId, @stockID, @isLocked ); - ", conn)) - { - // add parameters - cmd.Parameters.AddWithValue("@stockID", stockId); - cmd.Parameters.AddWithValue("@journalTypeId", journalTypeId); - cmd.Parameters.AddWithValue("@EntryDate", entryDate.ToUniversalTime()); - cmd.Parameters.AddWithValue("@isLocked", isNewStock); - - //execute - stockJournalId = (int)cmd.ExecuteScalar(); - } - - // insert journal posts into database - //new Data.Database.Stock - Core.Stock.StockJournal.StockJournalPostInsert(conn, stockId, stockJournalId, journalPosts, isNewStock); - - // consistency check - bool consistencyResult = true; - if (consistencyRequired) - { - consistencyResult = false; - // build list of effected status' - var statusIdEffected = new List(); - foreach (var item in journalPosts) - { - if (item.quantity < 0) - { - statusIdEffected.Add(item.statusId); - } - } - // run check - consistencyResult = Stock.StockJournal.WIP_StockJournalConsistencyCheck(sqlConnectionString, stockId, statusIdEffected); - } - - if (consistencyResult) - { - // commit - scope.Complete(); - return stockJournalId; - } - else - { - throw new Exception("Unable to insert stock journal entry, consistancy check failed."); - } - } - } - public static void StockJournalDelete(string sqlConnectionString, int stockJournalId) { using (TransactionScope scope = new TransactionScope()) @@ -1034,7 +470,7 @@ namespace bnhtrade.Core { if (item.Value < 0 && dicStatusIsCreditOnly[item.Key] == false) { - int quantity = new Data.Database.Stock.ReadStatusBalance().ByStockId(stockId, item.Key); + int quantity = new Data.Database.Stock.ReadStatusBalance().ReadStatusBalanceByStockId(stockId, item.Key); if (quantity + item.Value < 0) { @@ -1323,202 +759,6 @@ namespace bnhtrade.Core } } - namespace Purchase - { - public class PurchaseQuery - { - private static int accountJournalTypeIdNet = 1; - private static int accountJournalTypeIdTax = 2; - private static int stockJournalTypeId = 1; - private static int creditAccountId = 59; - private static int creditStatusId = 13; - private static int defaultAccountId = 138; - - public static void WIP_PurchaseLineTransactionNetInsert(string sqlConnectionString, int purchaseLineId, - string currencyCode, decimal amountNet, DateTime entryDate, int debitAccountId = 0) - { - // default to 'Inventory, Receivable/Processing' - if (debitAccountId < 1) - { - debitAccountId = defaultAccountId; - } - - using (TransactionScope scope = new TransactionScope()) - using (SqlConnection conn = new SqlConnection(sqlConnectionString)) - { - conn.Open(); - - // create account journal entry - int journalId = new Data.Database.Account.CreateJournal().AccountJournalInsert( - accountJournalTypeIdNet, entryDate, currencyCode, amountNet, debitAccountId); - - // add transaction to purchase line transaction table - using (SqlCommand cmd = new SqlCommand(@" - INSERT INTO - tblPurchaseLineTransaction - ( PurchaseLineID, AccountJournalID ) - VALUES - ( @purchaseLineID, @accountJournalID ); - ", conn)) - { - cmd.Parameters.AddWithValue("@purchaseLineID", purchaseLineId); - cmd.Parameters.AddWithValue("@accountJournalID", journalId); - - int count = cmd.ExecuteNonQuery(); - - if (count != 1) - { - throw new Exception("Operation cancelled, failed to update tblPurchaseLineTransaction!"); - } - } - - //commit the transaction and return value - scope.Complete(); - } - } - - public static void WIP_PurchaseLineTransactionNetUpdate(string sqlConnectionString, int accountJouranlId, - string currencyCode, decimal amountNet, int debitAccountId) - { - using (TransactionScope scope = new TransactionScope()) - using (SqlConnection conn = new SqlConnection(sqlConnectionString)) - { - conn.Open(); - - // stock accountId check - if (debitAccountId == 86) - { - int count = 0; - - using (SqlCommand cmd = new SqlCommand(@" - SELECT Count(tblStock.StockID) AS CountOfStockID - FROM tblStock - WHERE (((tblStock.AccountJournalID)=@accountJouranlId)); - ", conn)) - { - cmd.Parameters.AddWithValue("@accountJouranlId", accountJouranlId); - - count = (int)cmd.ExecuteScalar(); - } - - if (count == 0) - { - throw new Exception("Add account journal entry to stock before attempting this operation."); - } - else if (count > 1) - { - throw new Exception("Houston we have a problem! An account journal entry is assigned to " + count + " stock lines."); - } - } - - // make the update - bool result = new Data.Database.Account.UpdateJournal().AccountJournalPostUpdate(accountJouranlId, currencyCode, amountNet, debitAccountId, creditAccountId); - - scope.Complete(); - } - } - - // delete after testing.... - - //public static void WIP_PurchaseLineTransactionStockDelete(string sqlConnectionString, int accountJournalId, int stockId) - //{ - // using (TransactionScope scope = new TransactionScope()) - // using (SqlConnection conn = new SqlConnection(sqlConnectionString)) - // { - // conn.Open(); - - // // get stock cost - // (int quantity, decimal totalCost) result = Stock.StockJournal.StockGetTotalQuantityAndCost(sqlConnectionString, stockId); - // decimal amount = result.totalCost; - - // // delete accountJournalId from stock table - // using (SqlCommand cmd = new SqlCommand(@" - // UPDATE tblStock - // SET AccountJournalID=Null - // WHERE StockID=@stockId AND AccountJournalID=@accountJournalId; - // ", conn)) - // { - // cmd.Parameters.AddWithValue("@stockId", stockId); - // cmd.Parameters.AddWithValue("@accountJournalId", accountJournalId); - - // int count = cmd.ExecuteNonQuery(); - - // if (count != 1) - // { - // throw new Exception("Integrity check failure! StockID=" + stockId + ", AccountJournalID=" + accountJournalId + - // " combination not found on stock table."); - // } - // } - - // // reset the account journal to default - // Account.AccountQuery.AccountJournalPostUpdate(sqlConnectionString, accountJournalId, "GBP", amount, defaultAccountId, creditAccountId); - - // // delete the stock entry - // Stock.StockCreate.WIP_StockDeleteSub(sqlConnectionString, stockId); - - // scope.Complete(); - // } - //} - - public static void WIP_PurchaseLineTransactionDelete(string sqlConnectionString, int purchaseLineId, int accountJournalId) - { - using (SqlConnection conn = new SqlConnection(sqlConnectionString)) - { - conn.Open(); - var trans = conn.BeginTransaction(); - - // check accountJournalId does not exist in stock table - using (SqlCommand cmd = new SqlCommand(@" - SELECT StockID - FROM tblStock - WHERE AccountJournalID=@accountJournalId; - ", conn)) - { - cmd.Transaction = trans; - cmd.Parameters.AddWithValue("@accountJournalId", accountJournalId); - - using (var reader = cmd.ExecuteReader()) - { - if (reader.Read()) - { - if (reader.Read()) - { - throw new Exception("Integrity check failure! AccountJournalID=" + accountJournalId + " exists multiple time in stock table!"); - } - else - { - throw new Exception("Delete stock first before proceeding, AccountJournalID=" + accountJournalId); - } - } - } - } - - // delete line in purchase line transaction table - using (SqlCommand cmd = new SqlCommand(@" - DELETE FROM tblPurchaseLineTransaction - WHERE AccountJournalID=@accountJournalId; - ", conn)) - { - cmd.Transaction = trans; - cmd.Parameters.AddWithValue("@accountJournalId", accountJournalId); - - int count = cmd.ExecuteNonQuery(); - - if (count != 1) - { - throw new Exception("Operation cancelled, failed to delete entry in tblPurchaseLineTransaction WHERE AccountJournalID=" + accountJournalId); - } - } - - // delete account journal entry - new Data.Database.Repository.Implementation.JournalRepository(conn, trans).DeleteJournal(accountJournalId); - - trans.Commit(); - } - } - } - } - public class MiscFunction { public static string TraceMessage(