Skip to content

Commit

Permalink
remove unused field, update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
erikvw committed Nov 10, 2024
1 parent 6a1266a commit 211f040
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 37 deletions.
63 changes: 62 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,69 @@

edc-pharmacy
------------
EDC pharmacy is a simple pharmacy module for randomized control trials that can be integrated into clinicedc/edc projects.

Concepts: order, receive, repack, stock request, transfer
The module includes stock management to enable a research project team to track chain-of-custody of investigational product from a central site to each research site and finally to each patient.
Stock items are physically labeled using the integrated labelling functionality. Generated labels use a randomly generated stock code and code128 barcodes.

When integrated with an clinicedc/edc project, study site requests for stock can be generated using the subject's randomization assignment, followup schedule, and prescription.

Installation
============

.. code-block:: bash
pip install edc_pharmacy
More likely, ``edc_pharmacy`` is installed as a requirement of a ``clinicedc/edc`` project.


Overview
========
Concepts
++++++++

* order (central)
* receive/label/confirm (central)
* repack/label/confirm (central)
* generate stock request PRN (site)
* allocate to subject (central)
* pack (central)
* transfer/confirm (central to site)

Also:

* medication
* formulation
* prescription

Features
++++++++

* Tracks lot# with randomization assignment
* prints code128 label sheets (py_labels2, django_pylabel, edc_pylabel)
* generates a stock request based on subjects with valid prescriptions (Rx) using the next scheduled visit (see edc_appointment, edc_visit_tracking, edc_visit_schedule)
* stock are created in data but only available if confirmed by scanning barcode into system.


Details
=======

Qty vs Unit QTY
+++++++++++++++

* QTY is the container count, e.g. 5 bottles of 128 tablets
* UNIT_QTY is the total number of items in the container. A bottle of 128 has ``unit_qty`` of 128 and a ``qty`` of 1.
* all stock items start with a ``qty_in``=1 and ``qty_out``=0 while the ``unit_qty`` = ``qty_in`` * container.qty or as in the example above, ``unit_qty` = 1 * 128 = 128
* If the ``unit_qty_out`` equals the initial ``unit_qty_in``, e.g 128==128, the qty_out is set to 1. A stock item with qty_in=1 and qty_out=1 is not available / in stock.



Repack
++++++

Create new stock from an existing stock item. The container of the new stock item cannot be the same as the source container.
For example, create bottles of 128 tabs from a single bulk barrel of tablets.



Expand Down
1 change: 1 addition & 0 deletions edc_pharmacy/admin/actions/confirm_stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

@admin.display(description="Confirm repacked and labelled stock")
def confirm_stock_action(modeladmin, request, queryset: QuerySet[RepackRequest | Receive]):
"""See also : utils.confirm_stock"""
if queryset.count() > 1 or queryset.count() == 0:
messages.add_message(
request,
Expand Down
13 changes: 8 additions & 5 deletions edc_pharmacy/admin/stock/stock_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,14 @@ class StockAdmin(ModelAdminMixin, admin.ModelAdmin):
{"fields": ("qty_in", "qty_out", "unit_qty_in", "unit_qty_out")},
),
(
"Receive",
{"fields": ("receive_item",)},
),
(
"Repack",
"Receive / Repack",
{
"fields": (
"receive_item",
"repack_request",
"from_stock",
"confirmed_by",
"confirmed_datetime",
)
},
),
Expand Down Expand Up @@ -94,6 +93,8 @@ class StockAdmin(ModelAdminMixin, admin.ModelAdmin):
"product__assignment__name",
"location__display_name",
"container",
"confirmed_by",
"confirmed_datetime",
HasOrderNumFilter,
HasReceiveNumFilter,
HasRepackNumFilter,
Expand All @@ -113,6 +114,8 @@ class StockAdmin(ModelAdminMixin, admin.ModelAdmin):
readonly_fields = (
"code",
"confirmed",
"confirmed_by",
"confirmed_datetime",
"container",
"from_stock",
"location",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Generated by Django 5.1.2 on 2024-11-10 18:21

import django.db.models.deletion
import edc_utils.date
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("edc_pharmacy", "0040_remove_historicalstock_random_identifier_and_more"),
]

operations = [
migrations.RemoveField(
model_name="historicalstock",
name="confirmed_by_identifier",
),
migrations.RemoveField(
model_name="stock",
name="confirmed_by_identifier",
),
migrations.AlterField(
model_name="historicalstock",
name="allocation",
field=models.ForeignKey(
blank=True,
db_constraint=False,
help_text="Subject allocation",
null=True,
on_delete=django.db.models.deletion.DO_NOTHING,
related_name="+",
to="edc_pharmacy.allocation",
),
),
migrations.AlterField(
model_name="historicalstock",
name="code",
field=models.CharField(
blank=True,
db_index=True,
help_text="A unique alphanumeric code",
max_length=15,
null=True,
),
),
migrations.AlterField(
model_name="historicalstock",
name="confirmed",
field=models.BooleanField(
default=False,
help_text="True if stock was labelled and confirmed; False if stock was received/repacked but never confirmed.",
),
),
migrations.AlterField(
model_name="historicalstock",
name="stock_datetime",
field=models.DateTimeField(
default=edc_utils.date.get_utcnow, help_text="date stock record created"
),
),
migrations.AlterField(
model_name="historicalstock",
name="stock_identifier",
field=models.CharField(
blank=True,
db_index=True,
help_text="A sequential unique identifier for the stock",
max_length=36,
null=True,
),
),
migrations.AlterField(
model_name="stock",
name="allocation",
field=models.ForeignKey(
blank=True,
help_text="Subject allocation",
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="edc_pharmacy.allocation",
),
),
migrations.AlterField(
model_name="stock",
name="code",
field=models.CharField(
blank=True,
help_text="A unique alphanumeric code",
max_length=15,
null=True,
unique=True,
),
),
migrations.AlterField(
model_name="stock",
name="confirmed",
field=models.BooleanField(
default=False,
help_text="True if stock was labelled and confirmed; False if stock was received/repacked but never confirmed.",
),
),
migrations.AlterField(
model_name="stock",
name="stock_datetime",
field=models.DateTimeField(
default=edc_utils.date.get_utcnow, help_text="date stock record created"
),
),
migrations.AlterField(
model_name="stock",
name="stock_identifier",
field=models.CharField(
blank=True,
help_text="A sequential unique identifier for the stock",
max_length=36,
null=True,
unique=True,
),
),
]
72 changes: 51 additions & 21 deletions edc_pharmacy/models/stock/stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,25 @@

class Stock(BaseUuidModel):

stock_identifier = models.CharField(max_length=36, unique=True, null=True, blank=True)
stock_identifier = models.CharField(
max_length=36,
unique=True,
null=True,
blank=True,
help_text="A sequential unique identifier for the stock",
)

code = models.CharField(max_length=15, unique=True, null=True, blank=True)
code = models.CharField(
max_length=15,
unique=True,
null=True,
blank=True,
help_text="A unique alphanumeric code",
)

stock_datetime = models.DateTimeField(default=get_utcnow)
stock_datetime = models.DateTimeField(
default=get_utcnow, help_text="date stock record created"
)

receive_item = models.ForeignKey(
ReceiveItem, on_delete=models.PROTECT, null=True, blank=False
Expand All @@ -41,15 +55,28 @@ class Stock(BaseUuidModel):
)

from_stock = models.ForeignKey(
"edc_pharmacy.stock", related_name="source_stock", on_delete=models.PROTECT, null=True
"edc_pharmacy.stock",
related_name="source_stock",
on_delete=models.PROTECT,
null=True,
)

allocation = models.ForeignKey(Allocation, on_delete=models.PROTECT, null=True, blank=True)
allocation = models.ForeignKey(
Allocation,
on_delete=models.PROTECT,
null=True,
blank=True,
help_text="Subject allocation",
)

product = models.ForeignKey(Product, on_delete=models.PROTECT)

lot = models.ForeignKey(Lot, on_delete=models.PROTECT, null=True, blank=False)

container = models.ForeignKey(Container, on_delete=models.PROTECT, null=True, blank=False)

location = models.ForeignKey(Location, on_delete=PROTECT, null=True, blank=False)

qty_in = models.DecimalField(
null=True,
blank=False,
Expand Down Expand Up @@ -80,20 +107,21 @@ class Stock(BaseUuidModel):
validators=[MinValueValidator(0)],
)

location = models.ForeignKey(Location, on_delete=PROTECT, null=True, blank=False)

lot = models.ForeignKey(Lot, on_delete=models.PROTECT, null=True, blank=False)

status = models.CharField(max_length=25, choices=STOCK_STATUS, default=AVAILABLE)

description = models.CharField(max_length=100, null=True, blank=True)

confirmed = models.BooleanField(default=False)
confirmed = models.BooleanField(
default=False,
help_text=(
"True if stock was labelled and confirmed; "
"False if stock was received/repacked but never confirmed."
),
)
confirmed_datetime = models.DateTimeField(null=True, blank=True)
confirmed_by = models.CharField(
max_length=150, null=True, blank=True, help_text="label_lower"
)
confirmed_by_identifier = models.CharField(max_length=36, null=True, blank=True)

allocated_datetime = models.DateTimeField(null=True, blank=True)
subject_identifier = models.CharField(max_length=50, null=True, blank=True)
Expand All @@ -117,19 +145,17 @@ def save(self, *args, **kwargs):
self.product = self.get_receive_item().order_item.product
if not self.description:
self.description = f"{self.product.name} - {self.container.name}"
# if not self.code:
# self.code = generate_code_with_checksum_from_id(int(self.stock_identifier))
# if self.stock_request_item:
# single_site = site_sites.get(self.stock_request_item.stock_request.site.id)
# if single_site.name != self.location.name:
# self.status = RESERVED
self.verify_assignment()
self.verify_assignment(self.from_stock)
self.verify_qty()
self.verify_assignment_or_raise()
self.verify_assignment_or_raise(self.from_stock)
self.update_and_verify_qty_or_raise()
self.update_status()
super().save(*args, **kwargs)

def verify_qty(self):
def update_and_verify_qty_or_raise(self):
if self.unit_qty_in > 0:
if self.unit_qty_in == self.unit_qty_out:
self.qty_out = 1
Expand All @@ -138,7 +164,10 @@ def verify_qty(self):
if self.qty_out > 1 or self.qty_in > 1:
raise StockError("QTY OUT, QTY IN can only be 0 or 1.")

def verify_assignment(self, stock: models.ForeignKey[Stock] | None = None):
def verify_assignment_or_raise(
self, stock: models.ForeignKey[Stock] | None = None
) -> None:
"""Verify that the LOT and PRODUCT assignments match."""
if not stock:
stock = self
if stock.product.assignment != stock.lot.assignment:
Expand All @@ -153,11 +182,12 @@ def update_status(self):
self.status = AVAILABLE

def get_receive_item(self) -> ReceiveItem:
obj = self
"""Recursively fetch the original receive item."""
obj: Stock = self
receive_item = self.receive_item
while not receive_item:
obj = obj.from_stock
receive_item = obj.receive_item # noqa
obj = obj.from_stock # noqa
receive_item = obj.receive_item
return receive_item

class Meta(BaseUuidModel.Meta):
Expand Down
Loading

0 comments on commit 211f040

Please sign in to comment.