diff --git a/edc_pharmacy/admin/actions/process_repack_request.py b/edc_pharmacy/admin/actions/process_repack_request.py
index cbd2dc7..cb06015 100644
--- a/edc_pharmacy/admin/actions/process_repack_request.py
+++ b/edc_pharmacy/admin/actions/process_repack_request.py
@@ -1,30 +1,74 @@
from __future__ import annotations
+from celery import current_app
from django.contrib import admin, messages
from django.http import HttpResponseRedirect
from django.urls import reverse
-from django.utils.translation import gettext
+from django.utils.html import format_html
+from edc_utils.celery import run_task_sync_or_async
-from ...utils import process_repack_request
+from ...tasks.process_repack_request import process_repack_request_queryset
@admin.action(description="Process repack request")
def process_repack_request_action(modeladmin, request, queryset):
- if queryset.count() > 1 or queryset.count() == 0:
+ """Action to process repack request.
+
+ Redirects to process_repack_request.
+
+ If celery is running, will run through the entire queryset otherwise
+ just the first instance in the queryset.
+
+ """
+ repack_request_pks = [obj.pk for obj in queryset]
+
+ # if celery is not running, just keep the first pk
+ i = current_app.control.inspect()
+ if not i.active():
+ repack_request_pks = repack_request_pks[:1]
+
+ # run task / func and update or clear the task_id
+ task = run_task_sync_or_async(
+ process_repack_request_queryset, repack_request_pks=repack_request_pks
+ )
+ task_id = getattr(task, "id", None)
+ queryset.update(task_id=task_id)
+
+ # add messages for user
+ messages.add_message(
+ request,
+ messages.SUCCESS,
+ format_html(
+ "Repack request submitted.
Next, go to the ACTION menu below and "
+ "(1)`Print labels`. Then (2) Label your stock containers with the printed labels. "
+ "Once all stock is labelled, go to the ACTION menu below and "
+ "(3) Select `Confirm repacked and labelled stock`. "
+ f"Scan in the labels to CONFIRM the stock. ({task_id})"
+ ),
+ )
+ if task_id:
messages.add_message(
request,
- messages.ERROR,
- gettext("Select one and only one item"),
+ messages.INFO,
+ f"Task {task_id} is processing your repack requests.",
)
else:
- repack_obj = queryset.first()
- if repack_obj.processed:
- messages.add_message(
- request, messages.ERROR, "Nothing to do. Repack request already processed"
- )
- else:
- process_repack_request(repack_obj)
- url = reverse("edc_pharmacy_admin:edc_pharmacy_repackrequest_changelist")
- url = f"{url}?q={repack_obj.from_stock.code}"
- return HttpResponseRedirect(url)
- return None
+ repack_request = queryset.first()
+ messages.add_message(
+ request,
+ messages.INFO,
+ (
+ f"Processed only 1 of {queryset.count()} repack requests selected. "
+ f"See {repack_request}."
+ ),
+ )
+ messages.add_message(
+ request, messages.ERROR, "Task workers not running. Contact data management."
+ )
+
+ # redirect to changelist
+ url = reverse("edc_pharmacy_admin:edc_pharmacy_repackrequest_changelist")
+ if queryset.count() == 1:
+ repack_request = queryset.first()
+ url = f"{url}?q={repack_request.from_stock.code}"
+ return HttpResponseRedirect(url)
diff --git a/edc_pharmacy/admin/list_filters.py b/edc_pharmacy/admin/list_filters.py
index a430b01..15ea208 100644
--- a/edc_pharmacy/admin/list_filters.py
+++ b/edc_pharmacy/admin/list_filters.py
@@ -49,13 +49,50 @@ def queryset(self, request, queryset):
from_stock = True
if self.value() == YES:
opts = dict(from_stock__isnull=False) if from_stock else {}
- qs = queryset.filter(allocation__isnull=False, **opts)
+ qs = queryset.filter(
+ allocation__isnull=False,
+ container__may_request_as=True,
+ **opts,
+ )
elif self.value() == NO:
opts = dict(from_stock__isnull=False) if from_stock else {}
- qs = queryset.filter(allocation__isnull=True, **opts)
+ qs = queryset.filter(
+ allocation__isnull=True,
+ container__may_request_as=True,
+ **opts,
+ )
elif self.value() == NOT_APPLICABLE:
opts = dict(from_stock__isnull=True) if from_stock else {}
- qs = queryset.filter(allocation__isnull=True, **opts)
+ qs = queryset.filter(
+ allocation__isnull=True,
+ container__may_request_as=False,
+ **opts,
+ )
+ return qs
+
+
+class TransferredListFilter(SimpleListFilter):
+ title = "Transferred"
+ parameter_name = "transferred"
+
+ def lookups(self, request, model_admin):
+ return YES_NO_NA
+
+ def queryset(self, request, queryset):
+ qs = None
+ if self.value():
+ if self.value() == YES:
+ qs = queryset.filter(
+ container__may_request_as=True,
+ allocation__stock_request_item__stock_request__location=F("location"),
+ )
+ elif self.value() == NO:
+ qs = queryset.filter(
+ ~Q(allocation__stock_request_item__stock_request__location=F("location")),
+ container__may_request_as=True,
+ )
+ elif self.value() == NOT_APPLICABLE:
+ qs = queryset.filter(allocation__isnull=True, container__may_request_as=False)
return qs
diff --git a/edc_pharmacy/admin/stock/repack_request_admin.py b/edc_pharmacy/admin/stock/repack_request_admin.py
index 9c41218..048f3bd 100644
--- a/edc_pharmacy/admin/stock/repack_request_admin.py
+++ b/edc_pharmacy/admin/stock/repack_request_admin.py
@@ -1,3 +1,7 @@
+from decimal import Decimal
+
+from celery.result import AsyncResult
+from celery.states import SUCCESS
from django.contrib import admin
from django.contrib.admin.widgets import AutocompleteSelect
from django.template.loader import render_to_string
@@ -9,7 +13,11 @@
from ...forms import RepackRequestForm
from ...models import RepackRequest
from ...utils import format_qty
-from ..actions import confirm_repacked_stock_action, print_labels_from_repack_request
+from ..actions import (
+ confirm_repacked_stock_action,
+ print_labels_from_repack_request,
+ process_repack_request_action,
+)
from ..model_admin_mixin import ModelAdminMixin
@@ -23,7 +31,11 @@ class RequestRepackAdmin(ModelAdminMixin, admin.ModelAdmin):
autocomplete_fields = ["from_stock", "container"]
form = RepackRequestForm
- actions = [print_labels_from_repack_request, confirm_repacked_stock_action]
+ actions = [
+ process_repack_request_action,
+ print_labels_from_repack_request,
+ confirm_repacked_stock_action,
+ ]
change_list_note = render_to_string(
"edc_pharmacy/stock/instructions/repack_instructions.html"
@@ -38,8 +50,8 @@ class RequestRepackAdmin(ModelAdminMixin, admin.ModelAdmin):
"repack_datetime",
"from_stock",
"container",
- "qty",
- "processed",
+ "requested_qty",
+ "processed_qty",
)
},
),
@@ -50,11 +62,12 @@ class RequestRepackAdmin(ModelAdminMixin, admin.ModelAdmin):
"identifier",
"repack_date",
"from_stock_changelist",
- "formatted_qty",
+ "stock_changelist",
+ "formatted_requested_qty",
+ "formatted_processed_qty",
"container",
"from_stock__product__name",
- "processed",
- "stock_changelist",
+ "task_status",
)
search_fields = (
@@ -63,6 +76,8 @@ class RequestRepackAdmin(ModelAdminMixin, admin.ModelAdmin):
"from_stock__code",
)
+ readonly_fields = ("processed_qty", "task_id")
+
@admin.display(description="Repack date", ordering="repack_datetime")
def repack_date(self, obj):
return to_local(obj.repack_datetime).date()
@@ -74,7 +89,7 @@ def stock_changelist(self, obj):
context = dict(url=url, label="Stock", title="Go to stock")
return render_to_string("edc_pharmacy/stock/items_as_link.html", context=context)
- @admin.display(description="From stock")
+ @admin.display(description="From stock", ordering="from_stock__code")
def from_stock_changelist(self, obj):
url = reverse("edc_pharmacy_admin:edc_pharmacy_stock_changelist")
url = f"{url}?q={obj.from_stock.code}"
@@ -85,19 +100,31 @@ def from_stock_changelist(self, obj):
def identifier(self, obj):
return obj.repack_identifier
- @admin.display(description="QTY", ordering="qty")
- def formatted_qty(self, obj):
- return format_qty(obj.qty, obj.container)
+ @admin.display(description="Requested", ordering="requested_qty")
+ def formatted_requested_qty(self, obj):
+ return format_qty(obj.requested_qty, obj.container)
+
+ @admin.display(description="Processed", ordering="processed_qty")
+ def formatted_processed_qty(self, obj):
+ result = AsyncResult(str(obj.task_id)) if obj.task_id else None
+ if result and result.status != SUCCESS:
+ return None
+ return format_qty(obj.processed_qty, obj.container)
+
+ @admin.display(description="Task")
+ def task_status(self, obj):
+ if obj.task_id:
+ result = AsyncResult(str(obj.task_id))
+ return getattr(result, "status", None)
+ return None
def get_readonly_fields(self, request, obj=None):
- if obj and obj.processed:
+ if obj and (obj.processed_qty or Decimal(0)) > Decimal(0):
f = [
"repack_identifier",
"repack_datetime",
"container",
"from_stock",
- "processed",
- "qty",
]
return self.readonly_fields + tuple(f)
return self.readonly_fields
diff --git a/edc_pharmacy/admin/stock/stock_admin.py b/edc_pharmacy/admin/stock/stock_admin.py
index 0e23682..1d08c7c 100644
--- a/edc_pharmacy/admin/stock/stock_admin.py
+++ b/edc_pharmacy/admin/stock/stock_admin.py
@@ -3,6 +3,7 @@
from django.urls import reverse
from django.utils.html import format_html
from django_audit_fields.admin import audit_fieldset_tuple
+from edc_constants.constants import YES
from ...admin_site import edc_pharmacy_admin
from ...exceptions import AllocationError, AssignmentError
@@ -20,6 +21,7 @@
HasReceiveNumFilter,
HasRepackNumFilter,
ProductAssignmentListFilter,
+ TransferredListFilter,
)
from ..model_admin_mixin import ModelAdminMixin
from ..remove_fields_for_blinded_users import remove_fields_for_blinded_users
@@ -91,7 +93,7 @@ class StockAdmin(ModelAdminMixin, admin.ModelAdmin):
"from_stock_changelist",
"formatted_confirmed",
"allocated",
- "formatted_at_location",
+ "transferred",
"formulation",
"verified_assignment",
"qty",
@@ -109,7 +111,7 @@ class StockAdmin(ModelAdminMixin, admin.ModelAdmin):
list_filter = (
"confirmed",
AllocationListFilter,
- "at_location",
+ TransferredListFilter,
ProductAssignmentListFilter,
"product__formulation__description",
"product__assignment__name",
@@ -195,7 +197,11 @@ def formatted_code(self, obj):
def qty(self, obj):
return format_qty(obj.qty_in - obj.qty_out, obj.container)
- @admin.display(description="Units", ordering="qty")
+ @admin.display(description="T", boolean=True)
+ def transferred(self, obj):
+ return True if obj.transferred == YES else False
+
+ @admin.display(description="Units", ordering="unit_qty_out")
def unit_qty(self, obj):
return format_qty(obj.unit_qty_in - obj.unit_qty_out, obj.container)
@@ -207,10 +213,6 @@ def identifier(self, obj):
def formatted_confirmed(self, obj):
return obj.confirmed
- @admin.display(description="T", ordering="at_location", boolean=True)
- def formatted_at_location(self, obj):
- return obj.at_location
-
@admin.display(description="A", ordering="allocation", boolean=True)
def allocated(self, obj):
if obj.allocation:
diff --git a/edc_pharmacy/forms/stock/repack_request_form.py b/edc_pharmacy/forms/stock/repack_request_form.py
index 0eb764e..decd2ad 100644
--- a/edc_pharmacy/forms/stock/repack_request_form.py
+++ b/edc_pharmacy/forms/stock/repack_request_form.py
@@ -29,6 +29,11 @@ def clean(self):
> cleaned_data.get("from_stock").container.qty
):
raise forms.ValidationError({"container": "Cannot pack into larger container."})
+ if cleaned_data.get("requested_qty") and self.instance.processed_qty:
+ if cleaned_data.get("requested_qty") < self.instance.processed_qty:
+ raise forms.ValidationError(
+ {"requested_qty": "Cannot be less than the number of containers processed"}
+ )
return cleaned_data
class Meta:
diff --git a/edc_pharmacy/migrations/0027_rename_at_location_historicalstock_transferred_and_more.py b/edc_pharmacy/migrations/0027_rename_at_location_historicalstock_transferred_and_more.py
new file mode 100644
index 0000000..8a7c9f1
--- /dev/null
+++ b/edc_pharmacy/migrations/0027_rename_at_location_historicalstock_transferred_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.1.2 on 2024-11-15 14:46
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("edc_pharmacy", "0026_historicalstockrequest_cutoff_datetime_and_more"),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name="historicalstock",
+ old_name="at_location",
+ new_name="transferred",
+ ),
+ migrations.RenameField(
+ model_name="stock",
+ old_name="at_location",
+ new_name="transferred",
+ ),
+ ]
diff --git a/edc_pharmacy/migrations/0028_remove_historicalstock_transferred_and_more.py b/edc_pharmacy/migrations/0028_remove_historicalstock_transferred_and_more.py
new file mode 100644
index 0000000..4611c5a
--- /dev/null
+++ b/edc_pharmacy/migrations/0028_remove_historicalstock_transferred_and_more.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.1.2 on 2024-11-15 14:54
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("edc_pharmacy", "0027_rename_at_location_historicalstock_transferred_and_more"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="historicalstock",
+ name="transferred",
+ ),
+ migrations.RemoveField(
+ model_name="stock",
+ name="transferred",
+ ),
+ ]
diff --git a/edc_pharmacy/migrations/0029_remove_historicalrepackrequest_qty_and_more.py b/edc_pharmacy/migrations/0029_remove_historicalrepackrequest_qty_and_more.py
new file mode 100644
index 0000000..7380fb3
--- /dev/null
+++ b/edc_pharmacy/migrations/0029_remove_historicalrepackrequest_qty_and_more.py
@@ -0,0 +1,41 @@
+# Generated by Django 5.1.2 on 2024-11-16 16:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("edc_pharmacy", "0028_remove_historicalstock_transferred_and_more"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="historicalrepackrequest",
+ name="qty",
+ ),
+ migrations.RemoveField(
+ model_name="repackrequest",
+ name="qty",
+ ),
+ migrations.AddField(
+ model_name="historicalrepackrequest",
+ name="requested_qty",
+ field=models.DecimalField(
+ decimal_places=2,
+ max_digits=20,
+ null=True,
+ verbose_name="Number of containers requested",
+ ),
+ ),
+ migrations.AddField(
+ model_name="repackrequest",
+ name="requested_qty",
+ field=models.DecimalField(
+ decimal_places=2,
+ max_digits=20,
+ null=True,
+ verbose_name="Number of containers requested",
+ ),
+ ),
+ ]
diff --git a/edc_pharmacy/migrations/0030_remove_historicalrepackrequest_processed_and_more.py b/edc_pharmacy/migrations/0030_remove_historicalrepackrequest_processed_and_more.py
new file mode 100644
index 0000000..4aec2f8
--- /dev/null
+++ b/edc_pharmacy/migrations/0030_remove_historicalrepackrequest_processed_and_more.py
@@ -0,0 +1,41 @@
+# Generated by Django 5.1.2 on 2024-11-16 16:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("edc_pharmacy", "0029_remove_historicalrepackrequest_qty_and_more"),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name="historicalrepackrequest",
+ name="processed",
+ ),
+ migrations.RemoveField(
+ model_name="repackrequest",
+ name="processed",
+ ),
+ migrations.AddField(
+ model_name="historicalrepackrequest",
+ name="processed_qty",
+ field=models.DecimalField(
+ decimal_places=2,
+ max_digits=20,
+ null=True,
+ verbose_name="Number of containers requested",
+ ),
+ ),
+ migrations.AddField(
+ model_name="repackrequest",
+ name="processed_qty",
+ field=models.DecimalField(
+ decimal_places=2,
+ max_digits=20,
+ null=True,
+ verbose_name="Number of containers requested",
+ ),
+ ),
+ ]
diff --git a/edc_pharmacy/migrations/0031_historicalrepackrequest_task_id_and_more.py b/edc_pharmacy/migrations/0031_historicalrepackrequest_task_id_and_more.py
new file mode 100644
index 0000000..e4bd917
--- /dev/null
+++ b/edc_pharmacy/migrations/0031_historicalrepackrequest_task_id_and_more.py
@@ -0,0 +1,61 @@
+# Generated by Django 5.1.2 on 2024-11-16 17:32
+
+import django.core.validators
+from decimal import Decimal
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("edc_pharmacy", "0030_remove_historicalrepackrequest_processed_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="historicalrepackrequest",
+ name="task_id",
+ field=models.UUIDField(null=True),
+ ),
+ migrations.AddField(
+ model_name="repackrequest",
+ name="task_id",
+ field=models.UUIDField(null=True),
+ ),
+ migrations.AlterField(
+ model_name="historicalrepackrequest",
+ name="processed_qty",
+ field=models.DecimalField(
+ decimal_places=2, max_digits=20, null=True, verbose_name="Containers processed"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="historicalrepackrequest",
+ name="requested_qty",
+ field=models.DecimalField(
+ decimal_places=2,
+ max_digits=20,
+ null=True,
+ validators=[django.core.validators.MinValueValidator(Decimal("0.0"))],
+ verbose_name="Containers requested",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="repackrequest",
+ name="processed_qty",
+ field=models.DecimalField(
+ decimal_places=2, max_digits=20, null=True, verbose_name="Containers processed"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="repackrequest",
+ name="requested_qty",
+ field=models.DecimalField(
+ decimal_places=2,
+ max_digits=20,
+ null=True,
+ validators=[django.core.validators.MinValueValidator(Decimal("0.0"))],
+ verbose_name="Containers requested",
+ ),
+ ),
+ ]
diff --git a/edc_pharmacy/models/signals.py b/edc_pharmacy/models/signals.py
index 8148da1..bf661d9 100644
--- a/edc_pharmacy/models/signals.py
+++ b/edc_pharmacy/models/signals.py
@@ -1,14 +1,15 @@
from decimal import Decimal
-from django.db.models import F, Sum
+from django.db.models import Sum
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from edc_constants.constants import COMPLETE, PARTIAL
+from edc_utils.celery import run_task_sync_or_async
from ..dispense import Dispensing
-from ..exceptions import InsufficientStockError
from ..model_mixins import StudyMedicationCrfModelMixin
-from ..utils import process_repack_request, update_previous_refill_end_datetime
+from ..tasks import process_repack_request
+from ..utils import update_previous_refill_end_datetime
from .dispensing_history import DispensingHistory
from .stock import (
OrderItem,
@@ -24,21 +25,17 @@
def stock_on_post_save(sender, instance, raw, created, update_fields, **kwargs):
"""Update unit qty"""
if not raw and not update_fields:
- instance.unit_qty_in = Decimal(instance.qty_in) * instance.container.qty
-
- if instance.from_stock:
- instance.from_stock.unit_qty_out += instance.unit_qty_in
- if instance.from_stock.unit_qty_out > instance.from_stock.unit_qty_in:
- raise InsufficientStockError("Unit QTY OUT cannot exceed Unit QTY IN.")
- instance.from_stock.save(update_fields=["unit_qty_out"])
-
- instance.unit_qty_out = Decimal(instance.qty_out) * instance.container.qty
- if instance.unit_qty_out > instance.unit_qty_in:
- raise InsufficientStockError("Unit QTY OUT cannot exceed Unit QTY IN.")
-
- instance.qty = F("qty_in") - F("qty_out")
-
- instance.save(update_fields=["unit_qty_in", "unit_qty_out", "qty"])
+ pass
+ # instance.unit_qty_in = Decimal(instance.qty_in) * instance.container.qty
+ # if instance.from_stock:
+ # instance.from_stock.unit_qty_out += instance.unit_qty_in
+ # instance.from_stock.qty = F("qty_in") - F("qty_out")
+ # if instance.from_stock.unit_qty_out > instance.from_stock.unit_qty_in:
+ # raise InsufficientStockError("Unit QTY OUT cannot exceed Unit QTY IN.")
+ # instance.from_stock.save(update_fields=["unit_qty_out"])
+ # instance.qty = F("qty_in") - F("qty_out")
+ #
+ # instance.save(update_fields=["unit_qty_in", "unit_qty_out", "qty"])
@receiver(post_save, sender=OrderItem, dispatch_uid="update_order_item_on_post_save")
@@ -114,8 +111,7 @@ def repack_request_on_post_save(
sender, instance, raw, created, update_fields, **kwargs
) -> None:
if not raw and not update_fields:
- if not instance.processed:
- process_repack_request(instance)
+ run_task_sync_or_async(process_repack_request, repack_request_id=str(instance.id))
@receiver(post_delete, sender=ReceiveItem, dispatch_uid="receive_item_on_post_delete")
diff --git a/edc_pharmacy/models/stock/repack_request.py b/edc_pharmacy/models/stock/repack_request.py
index fe983ff..6f425df 100644
--- a/edc_pharmacy/models/stock/repack_request.py
+++ b/edc_pharmacy/models/stock/repack_request.py
@@ -1,3 +1,6 @@
+from decimal import Decimal
+
+from django.core.validators import MinValueValidator
from django.db import models
from edc_model.models import BaseUuidModel, HistoricalRecords
from edc_utils import get_utcnow
@@ -48,14 +51,27 @@ class RepackRequest(BaseUuidModel):
limit_choices_to={"may_repack_as": True},
)
- qty = models.DecimalField(
- verbose_name="Quantity", null=True, blank=False, decimal_places=2, max_digits=20
+ requested_qty = models.DecimalField(
+ verbose_name="Containers requested",
+ null=True,
+ blank=False,
+ decimal_places=2,
+ max_digits=20,
+ validators=[MinValueValidator(Decimal("0.0"))],
)
- processed = models.BooleanField(default=False)
+ processed_qty = models.DecimalField(
+ verbose_name="Containers processed",
+ null=True,
+ blank=False,
+ decimal_places=2,
+ max_digits=20,
+ )
stock_count = models.IntegerField(null=True, blank=True)
+ task_id = models.UUIDField(null=True)
+
objects = Manager()
history = HistoricalRecords()
@@ -73,6 +89,7 @@ def save(self, *args, **kwargs):
"Unconfirmed stock item. Only confirmed stock items may "
"be used to repack. Perhaps catch this in the form"
)
+ self.processed_qty = Decimal(0) if self.processed_qty is None else self.processed_qty
super().save(*args, **kwargs)
class Meta(BaseUuidModel.Meta):
diff --git a/edc_pharmacy/models/stock/stock.py b/edc_pharmacy/models/stock/stock.py
index 43c293a..226b271 100644
--- a/edc_pharmacy/models/stock/stock.py
+++ b/edc_pharmacy/models/stock/stock.py
@@ -4,14 +4,15 @@
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
-from django.db.models import PROTECT
+from django.db.models import PROTECT, F
+from edc_constants.constants import NO, NOT_APPLICABLE, YES
from edc_model.models import BaseUuidModel, HistoricalRecords
from edc_utils import get_utcnow
from sequences import get_next_value
from ...choices import STOCK_STATUS
from ...constants import ALLOCATED, AVAILABLE, ZERO_ITEM
-from ...exceptions import AllocationError, AssignmentError
+from ...exceptions import AllocationError, AssignmentError, InsufficientStockError
from ...utils import get_random_code
from .allocation import Allocation
from .container import Container
@@ -71,7 +72,7 @@ class Stock(BaseUuidModel):
help_text="Subject allocation",
)
- at_location = models.BooleanField(default=False)
+ # transferred = models.BooleanField(default=False)
dispense = models.OneToOneField(
Dispense,
@@ -167,19 +168,47 @@ def save(self, *args, **kwargs):
self.verify_assignment_or_raise()
self.verify_assignment_or_raise(self.from_stock)
self.update_status()
- self.update_at_location()
+ # self.update_transferred_status()
+ self.update_qty()
super().save(*args, **kwargs)
- def update_at_location(self):
- """An item is at location if it has not been allocated or
- if the stock request location == the stock location.
- """
- if not self.allocation:
- self.at_location = True
- elif self.allocation.stock_request_item.stock_request.location != self.location:
- self.at_location = False
- elif self.allocation.stock_request_item.stock_request.location == self.location:
- self.at_location = True
+ def update_qty(self):
+ self.unit_qty_in = Decimal(self.qty_in) * self.container.qty
+ if self.from_stock:
+ self.from_stock.unit_qty_out += self.unit_qty_in
+ self.from_stock.qty = F("qty_in") - F("qty_out")
+ if self.from_stock.unit_qty_out > self.from_stock.unit_qty_in:
+ raise InsufficientStockError("Unit QTY OUT cannot exceed Unit QTY IN.")
+ self.from_stock.save(update_fields=["unit_qty_out"])
+ self.qty = self.qty_in - self.qty_out
+
+ @property
+ def transferred(self) -> str:
+ transferred = NOT_APPLICABLE
+ if (
+ self.allocation
+ and self.allocation.stock_request_item.stock_request.location == self.location
+ and self.container.may_request_as
+ ):
+ transferred = YES
+ elif (
+ self.allocation
+ and self.allocation.stock_request_item.stock_request.location != self.location
+ and self.container.may_request_as
+ ):
+ transferred = NO
+ return transferred
+
+ # def update_transferred_status(self):
+ # """An item is at location if it has not been allocated or
+ # if the stock request location == the stock location.
+ # """
+ # if not self.allocation:
+ # self.transferred = False
+ # elif self.allocation.stock_request_item.stock_request.location != self.location:
+ # self.transferred = False
+ # elif self.allocation.stock_request_item.stock_request.location == self.location:
+ # self.transferred = True
def verify_assignment_or_raise(
self, stock: models.ForeignKey[Stock] | None = None
diff --git a/edc_pharmacy/tasks/__init__.py b/edc_pharmacy/tasks/__init__.py
new file mode 100644
index 0000000..1c8241f
--- /dev/null
+++ b/edc_pharmacy/tasks/__init__.py
@@ -0,0 +1 @@
+from .process_repack_request import process_repack_request
diff --git a/edc_pharmacy/tasks/process_repack_request.py b/edc_pharmacy/tasks/process_repack_request.py
new file mode 100644
index 0000000..2832fce
--- /dev/null
+++ b/edc_pharmacy/tasks/process_repack_request.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from uuid import UUID
+
+from celery import current_app, shared_task
+
+from ..utils import process_repack_request as process_repack_request_util
+
+if TYPE_CHECKING:
+
+ from ..models import RepackRequest
+
+
+@shared_task
+def process_repack_request(repack_request: RepackRequest):
+ process_repack_request_util(repack_request_id=repack_request.id)
+ return None
+
+
+@shared_task
+def process_repack_request_queryset(repack_request_pks: list[UUID]):
+ i = current_app.control.inspect()
+ active_workers = i.active()
+ if not active_workers:
+ repack_request_pks = repack_request_pks[:1]
+ for pk in repack_request_pks:
+ process_repack_request_util(repack_request_id=pk)
+ return None
diff --git a/edc_pharmacy/urls.py b/edc_pharmacy/urls.py
index 4227307..83063c6 100644
--- a/edc_pharmacy/urls.py
+++ b/edc_pharmacy/urls.py
@@ -3,6 +3,7 @@
from .admin_site import edc_pharmacy_admin
from .views import (
AllocateToSubjectView,
+ CeleryTaskStatusView,
ConfirmStockFromInstanceView,
ConfirmStockFromQuerySetView,
HomeView,
@@ -55,6 +56,11 @@
PrintLabelsView.as_view(),
name="print_labels_url",
),
+ path(
+ "task_status//",
+ CeleryTaskStatusView.as_view(),
+ name="celery_task_status_url",
+ ),
path("admin/", edc_pharmacy_admin.urls),
path("", HomeView.as_view(), name="home_url"),
]
diff --git a/edc_pharmacy/utils/format_qty.py b/edc_pharmacy/utils/format_qty.py
index 0410fbf..14424a0 100644
--- a/edc_pharmacy/utils/format_qty.py
+++ b/edc_pharmacy/utils/format_qty.py
@@ -8,6 +8,7 @@
def format_qty(qty: Decimal, container: Container):
+ qty = 0 if qty is None else qty
if container.qty_decimal_places == 0:
return str(int(qty))
elif container.qty_decimal_places == 1:
diff --git a/edc_pharmacy/utils/process_repack_request.py b/edc_pharmacy/utils/process_repack_request.py
index 8ee846c..931f5c9 100644
--- a/edc_pharmacy/utils/process_repack_request.py
+++ b/edc_pharmacy/utils/process_repack_request.py
@@ -1,51 +1,69 @@
from __future__ import annotations
-from typing import TYPE_CHECKING
+from uuid import UUID
-from django.contrib import messages
-from django.core.handlers.wsgi import WSGIRequest
+from celery.result import AsyncResult
+from django.apps import apps as django_apps
+from django.db import transaction
-from edc_pharmacy.exceptions import RepackError
+from ..exceptions import InsufficientStockError, RepackError
-if TYPE_CHECKING:
- from ..models import RepackRequest
-
-def process_repack_request(
- repack_request: RepackRequest | None = None, request: WSGIRequest | None = None
-) -> RepackRequest:
+def process_repack_request(repack_request_id: UUID | None = None) -> None:
"""Take from stock and fill container as new stock item.
Do not change location here.
"""
-
- if repack_request.from_stock and not repack_request.processed:
+ repack_request_model_cls = django_apps.get_model("edc_pharmacy.repackrequest")
+ stock_model_cls = django_apps.get_model("edc_pharmacy.stock")
+ repack_request = repack_request_model_cls.objects.get(id=repack_request_id)
+ repack_request.task_id = None
+ try:
+ celery_status = AsyncResult(task_id=repack_request.task_id).state
+ except TypeError:
+ celery_status = None
+ if not celery_status:
+ pass
+ else:
+ repack_request.processed_qty = repack_request.processed_qty = (
+ stock_model_cls.objects.filter(repack_request=repack_request).count()
+ )
+ repack_request.requested_qty = (
+ repack_request.processed_qty
+ if not repack_request.requested_qty
+ else repack_request.requested_qty
+ )
+ count = 0
+ number_to_process = repack_request.requested_qty - repack_request.processed_qty
if not repack_request.from_stock.confirmed:
- raise RepackError("Stock not confirmed")
- stock_model_cls = repack_request.from_stock.__class__
- for index in range(0, int(repack_request.qty)):
- stock_model_cls.objects.create(
- receive_item=None,
- qty_in=1,
- qty_out=0,
- qty=1,
- from_stock=repack_request.from_stock,
- container=repack_request.container,
- location=repack_request.from_stock.location,
- repack_request=repack_request,
- confirmed=False,
- lot=repack_request.from_stock.lot,
- )
- repack_request.processed = True
- repack_request.save()
- if request:
- messages.add_message(
- request,
- messages.SUCCESS,
- (
- "Repack request submitted. Next, print labels and label the stock. "
- "Once all stock is labelled, go back to Repack and scan in the "
- "labels to confirm the stock"
- ),
- )
- return repack_request
+ raise RepackError("Source stock item not confirmed")
+ else:
+ stock_model_cls = repack_request.from_stock.__class__
+ for index in range(0, int(number_to_process)):
+ try:
+ with transaction.atomic():
+ stock_model_cls.objects.create(
+ receive_item=None,
+ qty_in=1,
+ qty_out=0,
+ qty=1,
+ from_stock=repack_request.from_stock,
+ container=repack_request.container,
+ location=repack_request.from_stock.location,
+ repack_request=repack_request,
+ confirmed=False,
+ lot=repack_request.from_stock.lot,
+ )
+ except InsufficientStockError:
+ break
+ else:
+ count += repack_request.container.qty
+ if (
+ repack_request.container.qty
+ > repack_request.from_stock.container.qty - count
+ ):
+ break
+
+ if number_to_process > 0:
+ repack_request.processed_qty += count
+ repack_request.save(update_fields=["requested_qty", "processed_qty", "task_id"])
diff --git a/edc_pharmacy/views/__init__.py b/edc_pharmacy/views/__init__.py
index 516e41f..477ca75 100644
--- a/edc_pharmacy/views/__init__.py
+++ b/edc_pharmacy/views/__init__.py
@@ -1,4 +1,5 @@
from .allocate_to_subject_view import AllocateToSubjectView
+from .celery_task_status_view import CeleryTaskStatusView
from .confirm_stock_from_instance_view import ConfirmStockFromInstanceView
from .confirm_stock_from_queryset_view import ConfirmStockFromQuerySetView
from .home_view import HomeView
diff --git a/edc_pharmacy/views/allocate_to_subject_view.py b/edc_pharmacy/views/allocate_to_subject_view.py
index e8e9a06..cef6a5f 100644
--- a/edc_pharmacy/views/allocate_to_subject_view.py
+++ b/edc_pharmacy/views/allocate_to_subject_view.py
@@ -126,8 +126,8 @@ def stock_already_allocated(self, stock_codes: list[str]) -> bool:
messages.ERROR,
f"Stock already allocated. Got {','.join(assigned_codes)}.",
)
- return False
- return True
+ return True
+ return False
def post(self, request, *args, **kwargs):
stock_codes = request.POST.getlist("codes") if request.POST.get("codes") else None
@@ -137,7 +137,9 @@ def post(self, request, *args, **kwargs):
stock_request = StockRequest.objects.get(id=kwargs.get("stock_request"))
if self.validate_containers(stock_codes, stock_request):
assignment = self.get_assignment(assignment_id)
- subject_identifiers = [] if self.stock_already_allocated() else subject_identifiers
+ subject_identifiers = (
+ [] if self.stock_already_allocated(stock_codes) else subject_identifiers
+ )
if subject_identifiers and assignment:
allocation_data = dict(zip(stock_codes, subject_identifiers))
try:
diff --git a/edc_pharmacy/views/celery_task_status_view.py b/edc_pharmacy/views/celery_task_status_view.py
new file mode 100644
index 0000000..df3fd3a
--- /dev/null
+++ b/edc_pharmacy/views/celery_task_status_view.py
@@ -0,0 +1,15 @@
+from celery.result import AsyncResult
+from django.http import JsonResponse
+from django.views import View
+
+
+class CeleryTaskStatusView(View):
+
+ def get(self, request, *args, **kwargs):
+ task_id = request.GET.get("task_id")
+ if task_id:
+ result = AsyncResult(task_id)
+ return JsonResponse(
+ {"task_id": task_id, "status": result.status, "result": result.result}
+ )
+ return JsonResponse({"error": "No task_id provided"}, status=400)