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)