I would create a table to hold the inventory, along with a separate table to hold patient's on-hand inventory.
This is best described using a minimal, complete, and verifiable example.
First, we'll create the tables we need. I'm doing this in tempdb
to avoid conflicting with objects you may have created already.
USE tempdb;
--clean up so we can run this multiple times with changes
DROP PROCEDURE IF EXISTS dbo.[UpdatePatientInventory];
DROP VIEW IF EXISTS dbo.[Inventory_Balances];
DROP TABLE IF EXISTS dbo.[Inventory], dbo.[Patient_Product_OnHand], dbo.[Patient], dbo.[Product], dbo.[TransactionTypes];
GO
--details about each product
CREATE TABLE dbo.[Product]
(
[ProductId] int NOT NULL
IDENTITY(1,1)
CONSTRAINT [Product_pk]
PRIMARY KEY
CLUSTERED
, [ProductName] varchar(100) NOT NULL
);
--details about each patient
CREATE TABLE dbo.[Patient]
(
[PatientId] int NOT NULL
IDENTITY(1,1)
CONSTRAINT [Patients_pk]
PRIMARY KEY
CLUSTERED
, [PatientName] varchar(100) NOT NULL
);
--Describes the types of transaction entries
CREATE TABLE dbo.[TransactionTypes]
(
[TransactionTypeId] int NOT NULL
IDENTITY(1,1)
CONSTRAINT [TransactionTypes_pk]
PRIMARY KEY
CLUSTERED
, [TypeName] varchar(100) NOT NULL
);
--contains multiple transactional rows for each drug added to inventory, or
--removed from inventory, or transferred to a patient
CREATE TABLE dbo.[Inventory]
(
[InventoryId] int NOT NULL
IDENTITY(1,1)
CONSTRAINT [Inventory_pk]
PRIMARY KEY
CLUSTERED
, [ProductId] int NOT NULL
CONSTRAINT [Inventory_Product_fk]
FOREIGN KEY
REFERENCES dbo.[Product]([ProductId])
, [TransactionTypeId] int NOT NULL
CONSTRAINT [Inventory_TransactionTypeId_fk]
FOREIGN KEY
REFERENCES dbo.[TransactionTypes]([TransactionTypeId])
, [TransactionDate] datetimeoffset(3) NOT NULL
CONSTRAINT [Inventory_tx_date]
DEFAULT ((SYSUTCDATETIME()))
, [Quantity] int NOT NULL
);
--transactions in/out of patients custody
CREATE TABLE dbo.[Patient_Product_OnHand]
(
[PatientId] int NOT NULL
CONSTRAINT [Patient_Product_OnHand_PatientId_fk]
FOREIGN KEY
REFERENCES dbo.[Patient]([PatientId])
, [ProductId] int NOT NULL
CONSTRAINT [Patient_Product_OnHand_ProductId_fk]
FOREIGN KEY
REFERENCES dbo.[Product]([ProductId])
, [TransactionDate] datetimeoffset(3) NOT NULL
CONSTRAINT [Patient_Product_OnHand_tx_date]
DEFAULT ((SYSUTCDATETIME()))
, [Quantity] int NOT NULL
, CONSTRAINT [Patient_Product_OnHand_pk]
PRIMARY KEY
NONCLUSTERED
([PatientId], [ProductId])
, INDEX [Patient_Product_OnHand_cx]
CLUSTERED
([TransactionDate], [PatientId], [ProductId])
);
GO
Here we're inserting some rows of data into each table.
--insert some test values
INSERT INTO dbo.[Product] ([ProductName])
VALUES ('Acetominophen')
, ('Acetylsalicylic Acid');
INSERT INTO dbo.[Patient] ([PatientName])
VALUES ('Sally')
, ('John');
INSERT INTO dbo.[TransactionTypes] ([TypeName])
VALUES ('Product Received')
, ('Product Sold')
, ('Product Assigned to Patient');
INSERT INTO dbo.Inventory ([ProductId], [Quantity], [TransactionDate], [TransactionTypeId])
VALUES (1, 12, '2021-01-01T00:00:00.000Z', 1)
, (2, 6, '2021-01-02T12:34:56.789Z', 1);
This is what the inventory looks like now:
SELECT i.[InventoryId]
, p.[ProductName]
, i.[Quantity]
, i.[TransactionDate]
, [Transaction Type] = tt.[TypeName]
FROM dbo.[Inventory] i
INNER JOIN dbo.[Product] p ON i.[ProductId] = p.[ProductId]
INNER JOIN dbo.[TransactionTypes] tt ON i.[TransactionTypeId] = tt.[TransactionTypeId]
GO
InventoryId |
ProductName |
Quantity |
TransactionDate |
Transaction Type |
1 |
Acetominophen |
12 |
2021-01-01 00:00:00.000 +00:00 |
Product Received |
2 |
Acetylsalicylic Acid |
6 |
2021-01-02 12:34:56.789 +00:00 |
Product Received |
I'm using a stored procedure to perform transfers between our inventory, and patient's custody:
CREATE PROCEDURE dbo.[UpdatePatientInventory]
(
@PatientId int
, @ProductId int
, @TransactionDate datetimeoffset(3) = NULL
, @Quantity int
)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @TransactionDate IS NULL
BEGIN
SET @TransactionDate = SYSUTCDATETIME();
END
DECLARE @msg nvarchar(1000);
BEGIN TRANSACTION
BEGIN TRY
INSERT INTO dbo.Patient_Product_OnHand ([PatientId], [ProductId], [TransactionDate], [Quantity])
VALUES (@PatientId, @ProductId, @TransactionDate, @Quantity);
INSERT INTO dbo.Inventory ([ProductId], [TransactionDate], [Quantity], [TransactionTypeId])
VALUES (@ProductId, @TransactionDate, 0 - @Quantity, 3);
IF @@TRANCOUNT > 0 COMMIT TRANSACTION;
END TRY
BEGIN CATCH
SET @msg = ERROR_MESSAGE() + N' at line ' + CONVERT(nvarchar(10), ERROR_LINE());
RAISERROR (@msg, 0, 1) WITH NOWAIT;
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
SET @msg = N'Transaction rolled back.';
RAISERROR (@msg, 0, 1) WITH NOWAIT;
END CATCH
END
GO
Moving product into patient's custody looks like:
EXEC dbo.UpdatePatientInventory @PatientId = 1, @ProductId = 1, @Quantity = 4;
This view shows the current inventory, with transactional details and balances at every stage:
CREATE VIEW dbo.[Inventory_Balances]
AS
SELECT p.[ProductName]
, i.[TransactionDate]
, [Transaction Type] = tt.[TypeName]
, [Transaction Quantity] = i.[Quantity]
, [Balance] = SUM(i.[Quantity]) OVER (PARTITION BY i.[ProductId] ORDER BY i.[TransactionDate] ROWS UNBOUNDED PRECEDING)
FROM dbo.[Inventory] i
INNER JOIN dbo.[TransactionTypes] tt ON i.[TransactionTypeId] = tt.[TransactionTypeId]
INNER JOIN dbo.[Product] p ON i.[ProductId] = p.[ProductId]
GO
Looking at the output of the view:
SELECT *
FROM dbo.[Inventory_Balances] ib
ORDER BY ib.[ProductName]
, ib.[TransactionDate]
ProductName |
TransactionDate |
Transaction Type |
Transaction Quantity |
Balance |
Acetominophen |
2021-01-01 00:00:00.000 +00:00 |
Product Received |
12 |
12 |
Acetominophen |
2021-10-08 19:26:01.329 +00:00 |
Product Assigned to Patient |
-4 |
8 |
Acetylsalicylic Acid |
2021-01-02 12:34:56.789 +00:00 |
Product Received |
6 |
6 |
And a simple query showing the current balance in the custody of each patient:
SELECT pa.[PatientName]
, pr.[ProductName]
, ppo.TransactionDate
, [Balance] = SUM(ppo.Quantity) OVER (PARTITION BY ppo.[PatientId], ppo.[ProductId] ORDER BY ppo.[TransactionDate] ROWS UNBOUNDED PRECEDING)
FROM dbo.[Patient] pa
INNER JOIN dbo.[Patient_Product_OnHand] ppo ON pa.[PatientId] = ppo.[PatientId]
INNER JOIN dbo.[Product] pr ON ppo.[ProductId] = pr.[ProductId];
PatientName |
ProductName |
TransactionDate |
Balance |
Sally |
Acetominophen |
2021-10-08 19:26:01.329 +00:00 |
4 |