From dd08c319f1641a07c9bd4bfc850b6ae2fd02a1f6 Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Mon, 31 Mar 2025 17:39:17 +0200 Subject: [PATCH 1/6] Fix migration history --- .../migrations/0018_item_contained_in.py | 19 ------------------- ...0018_location_item_alter_category_icon.py} | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 aleksis/apps/plank/migrations/0018_item_contained_in.py rename aleksis/apps/plank/migrations/{0019_location_item_alter_category_icon.py => 0018_location_item_alter_category_icon.py} (86%) diff --git a/aleksis/apps/plank/migrations/0018_item_contained_in.py b/aleksis/apps/plank/migrations/0018_item_contained_in.py deleted file mode 100644 index 1e8c89b..0000000 --- a/aleksis/apps/plank/migrations/0018_item_contained_in.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.11 on 2024-05-01 09:50 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('plank', '0017_category_manufacturer_item_type_remove_inventory'), - ] - - operations = [ - migrations.AddField( - model_name='item', - name='contained_in', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='contains', to='plank.item', verbose_name='Contained in'), - ), - ] diff --git a/aleksis/apps/plank/migrations/0019_location_item_alter_category_icon.py b/aleksis/apps/plank/migrations/0018_location_item_alter_category_icon.py similarity index 86% rename from aleksis/apps/plank/migrations/0019_location_item_alter_category_icon.py rename to aleksis/apps/plank/migrations/0018_location_item_alter_category_icon.py index 0ff33c4..06a24e7 100644 --- a/aleksis/apps/plank/migrations/0019_location_item_alter_category_icon.py +++ b/aleksis/apps/plank/migrations/0018_location_item_alter_category_icon.py @@ -7,7 +7,7 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('plank', '0018_item_contained_in'), + ('plank', '0017_category_manufacturer_item_type_remove_inventory'), ] operations = [ -- GitLab From a8328d7ad22bc6470a988fb38d6afde944d0b3ac Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Mon, 31 Mar 2025 17:40:38 +0200 Subject: [PATCH 2/6] Add anonymous option to item and show it in frontend --- aleksis/apps/plank/forms.py | 2 + .../components/attributeRows/CountRow.vue | 32 +++++++++++++++ .../frontend/components/inputs/CountInput.vue | 40 +++++++++++++++++++ .../components/inputs/IsAnonymousInput.vue | 32 +++++++++++++++ .../components/item/AnonymousChip.vue | 13 ++++++ .../frontend/components/item/ItemBaseList.vue | 9 +++++ .../frontend/components/item/ItemDetail.vue | 3 ++ .../frontend/components/item/ItemStatus.vue | 1 + .../components/item/fragments.graphql | 2 + aleksis/apps/plank/frontend/defaultIcons.js | 1 + aleksis/apps/plank/frontend/defaultItems.js | 2 + aleksis/apps/plank/frontend/messages/en.json | 6 ++- aleksis/apps/plank/frontend/rules.js | 3 ++ .../plank/migrations/0019_anonymous_item.py | 23 +++++++++++ aleksis/apps/plank/models.py | 8 ++++ aleksis/apps/plank/schema.py | 2 + 16 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 aleksis/apps/plank/frontend/components/attributeRows/CountRow.vue create mode 100644 aleksis/apps/plank/frontend/components/inputs/CountInput.vue create mode 100644 aleksis/apps/plank/frontend/components/inputs/IsAnonymousInput.vue create mode 100644 aleksis/apps/plank/frontend/components/item/AnonymousChip.vue create mode 100644 aleksis/apps/plank/migrations/0019_anonymous_item.py diff --git a/aleksis/apps/plank/forms.py b/aleksis/apps/plank/forms.py index b2fc283..d78b69f 100644 --- a/aleksis/apps/plank/forms.py +++ b/aleksis/apps/plank/forms.py @@ -45,6 +45,8 @@ class ItemForm(ModelForm): "location", "serial_number", "inventory", + "is_anonymous", + "count", ) diff --git a/aleksis/apps/plank/frontend/components/attributeRows/CountRow.vue b/aleksis/apps/plank/frontend/components/attributeRows/CountRow.vue new file mode 100644 index 0000000..63706eb --- /dev/null +++ b/aleksis/apps/plank/frontend/components/attributeRows/CountRow.vue @@ -0,0 +1,32 @@ +<template> + <attribute-row + :icon="defaultIcons.count" + :label="$t('plank.labels.available_count')" + > + {{ data.count }} + <anonymous-chip /> + </attribute-row> +</template> + +<script> +import AttributeRow from "./AttributeRow.vue"; +import defaultIcons from "../../defaultIcons"; +import AnonymousChip from "../item/AnonymousChip.vue"; + +export default { + name: "CountRow", + components: { AnonymousChip, AttributeRow }, + props: { + data: { + type: Object, + required: false, + default: null, + }, + }, + data() { + return { + defaultIcons: defaultIcons, + }; + }, +}; +</script> diff --git a/aleksis/apps/plank/frontend/components/inputs/CountInput.vue b/aleksis/apps/plank/frontend/components/inputs/CountInput.vue new file mode 100644 index 0000000..cd53869 --- /dev/null +++ b/aleksis/apps/plank/frontend/components/inputs/CountInput.vue @@ -0,0 +1,40 @@ +<template> + <v-text-field + filled + v-model.number="innerValue" + type="number" + :label="$t('plank.labels.count')" + :rules="rules.count" + :prepend-icon="defaultIcons.count" + ></v-text-field> +</template> + +<script> +import rules from "../../rules"; +import defaultIcons from "../../defaultIcons"; + +export default { + name: "CountInput", + props: { + value: { + type: Number | String, + required: true, + }, + }, + data() { + return { + rules: rules, + defaultIcons: defaultIcons, + innerValue: this.value, + }; + }, + watch: { + value(val) { + this.innerValue = val; + }, + innerValue(val) { + this.$emit("input", val); + }, + }, +}; +</script> diff --git a/aleksis/apps/plank/frontend/components/inputs/IsAnonymousInput.vue b/aleksis/apps/plank/frontend/components/inputs/IsAnonymousInput.vue new file mode 100644 index 0000000..196dd9d --- /dev/null +++ b/aleksis/apps/plank/frontend/components/inputs/IsAnonymousInput.vue @@ -0,0 +1,32 @@ +<template> + <v-switch + inset + v-model="innerValue" + :label="$t('plank.labels.is_anonymous')" + ></v-switch> +</template> + +<script> +export default { + name: "IsAnonymousInput", + props: { + value: { + type: Boolean, + required: true, + }, + }, + data() { + return { + innerValue: this.value, + }; + }, + watch: { + value(val) { + this.innerValue = val; + }, + innerValue(val) { + this.$emit("input", val); + }, + }, +}; +</script> diff --git a/aleksis/apps/plank/frontend/components/item/AnonymousChip.vue b/aleksis/apps/plank/frontend/components/item/AnonymousChip.vue new file mode 100644 index 0000000..1df0b17 --- /dev/null +++ b/aleksis/apps/plank/frontend/components/item/AnonymousChip.vue @@ -0,0 +1,13 @@ +<script> +export default { + name: "AnonymousChip", +}; +</script> + +<template> + <v-chip label small outlined color="purple" v-bind="$attrs"> + {{ $t("plank.item.anonymous") }} + </v-chip> +</template> + +<style scoped></style> diff --git a/aleksis/apps/plank/frontend/components/item/ItemBaseList.vue b/aleksis/apps/plank/frontend/components/item/ItemBaseList.vue index c784300..528e523 100644 --- a/aleksis/apps/plank/frontend/components/item/ItemBaseList.vue +++ b/aleksis/apps/plank/frontend/components/item/ItemBaseList.vue @@ -46,6 +46,7 @@ :to="{ name: 'plank.location', params: { id: item.isLocation.id } }" class="ml-2" /> + <anonymous-chip v-if="item.isAnonymous" class="ml-2" /> </template> <!-- eslint-disable-next-line vue/valid-v-slot --> <template #item.status="{ item }"> @@ -61,6 +62,8 @@ v-model="currentItem.itemType" :item-types="formData.itemTypes" /> + <is-anonymous-input v-model="currentItem.isAnonymous" /> + <count-input v-if="currentItem.isAnonymous" v-model="currentItem.count" /> <name-input v-model="currentItem.name" /> <location-input v-model="currentItem.location" @@ -88,12 +91,16 @@ import SerialNumberInput from "../inputs/SerialNumberInput.vue"; import ItemTypeInput from "../inputs/ItemTypeInput.vue"; import ScanInput from "../inputs/ScanInput.vue"; import ContainerChip from "./ContainerChip.vue"; +import AnonymousChip from "./AnonymousChip.vue"; +import IsAnonymousInput from "../inputs/IsAnonymousInput.vue"; +import CountInput from "../inputs/CountInput.vue"; import { items, deleteItem, updateItem, itemFormData } from "./queries.graphql"; export default { name: "ItemBaseList", components: { + IsAnonymousInput, ContainerChip, SerialNumberInput, LocationInput, @@ -107,6 +114,8 @@ export default { ItemId, ItemTypeInput, ScanInput, + AnonymousChip, + CountInput, }, apollo: { formData: { diff --git a/aleksis/apps/plank/frontend/components/item/ItemDetail.vue b/aleksis/apps/plank/frontend/components/item/ItemDetail.vue index 80c42bd..f2a15b3 100644 --- a/aleksis/apps/plank/frontend/components/item/ItemDetail.vue +++ b/aleksis/apps/plank/frontend/components/item/ItemDetail.vue @@ -7,6 +7,7 @@ <attributes-table> <barcode-row :data="item.barcode" /> <inventory-row :data="item.inventory" /> + <count-row v-if="item.isAnonymous" :data="item" /> <is-location-row :data="item.isLocation" /> <category-row :data="item.category" /> <item-type-row :data="item.itemType" /> @@ -40,6 +41,7 @@ import LastTimeSeenAtRow from "../attributeRows/LastTimeSeenAtRow.vue"; import ItemStatusRow from "../attributeRows/ItemStatusRow.vue"; import CheckOutHistoryTable from "./CheckOutHistoryTable.vue"; import IsLocationRow from "../attributeRows/IsLocationRow.vue"; +import CountRow from "../attributeRows/CountRow.vue"; import { item } from "./queries.graphql"; @@ -59,6 +61,7 @@ export default { DetailPage, AttributesTable, ItemStatusRow, + CountRow, }, data() { return { diff --git a/aleksis/apps/plank/frontend/components/item/ItemStatus.vue b/aleksis/apps/plank/frontend/components/item/ItemStatus.vue index a8802ae..c8acc3a 100644 --- a/aleksis/apps/plank/frontend/components/item/ItemStatus.vue +++ b/aleksis/apps/plank/frontend/components/item/ItemStatus.vue @@ -2,6 +2,7 @@ <div v-if="item.isAvailable"> <v-chip color="green" text-color="white" label small> {{ $t("plank.item.status.available") }} + <template v-if="item.isAnonymous">({{ item.count }})</template> </v-chip> <span v-if="item.location"> ({{ item.location.name }}) </span> diff --git a/aleksis/apps/plank/frontend/components/item/fragments.graphql b/aleksis/apps/plank/frontend/components/item/fragments.graphql index 7422f71..86ab947 100644 --- a/aleksis/apps/plank/frontend/components/item/fragments.graphql +++ b/aleksis/apps/plank/frontend/components/item/fragments.graphql @@ -30,6 +30,8 @@ fragment ItemExtendedParts on ItemGrapheneType { notes lastTimeSeenAt isAvailable + isAnonymous + count currentCheckedOutItem { id process { diff --git a/aleksis/apps/plank/frontend/defaultIcons.js b/aleksis/apps/plank/frontend/defaultIcons.js index aff29b5..4e9eb9f 100644 --- a/aleksis/apps/plank/frontend/defaultIcons.js +++ b/aleksis/apps/plank/frontend/defaultIcons.js @@ -23,4 +23,5 @@ export default { date: "mdi-calendar-outline", checkedOutAt: "mdi-clock-outline", containedIn: "mdi-archive-outline", + count: "mdi-counter", }; diff --git a/aleksis/apps/plank/frontend/defaultItems.js b/aleksis/apps/plank/frontend/defaultItems.js index 95c4dda..9d3af86 100644 --- a/aleksis/apps/plank/frontend/defaultItems.js +++ b/aleksis/apps/plank/frontend/defaultItems.js @@ -25,6 +25,8 @@ export default { itemType: null, location: null, serialNumber: "", + isAnonymous: false, + count: 1, }, checkOutProcess: { checkInUntil: null, diff --git a/aleksis/apps/plank/frontend/messages/en.json b/aleksis/apps/plank/frontend/messages/en.json index b586c55..b98f1d8 100644 --- a/aleksis/apps/plank/frontend/messages/en.json +++ b/aleksis/apps/plank/frontend/messages/en.json @@ -100,6 +100,7 @@ "menu_title": "Items", "delete_question": "Are you sure that you want to delete this item?", "container": "Container", + "anonymous": "Anonymous", "actions": { "create": "Create new item", "edit": "Edit item", @@ -210,7 +211,10 @@ "borrowing_person": "Checked out to", "lending_person": "Checked out by", "check_in_until": "Check in until", - "is_location": "Is location" + "is_location": "Is location", + "is_anonymous": "Is anonymous", + "count": "Count", + "available_count": "Available count" }, "loading": "Loading ...", "actions": { diff --git a/aleksis/apps/plank/frontend/rules.js b/aleksis/apps/plank/frontend/rules.js index 334cfb7..a28bb77 100644 --- a/aleksis/apps/plank/frontend/rules.js +++ b/aleksis/apps/plank/frontend/rules.js @@ -16,4 +16,7 @@ export default { (v) => !v || new Date(v) >= new Date() || "Date has to be after today", ], item: [], + count: [ + (v) => Number.parseInt(v) > 0 || "Count has to larger or equal than zero", + ], }; diff --git a/aleksis/apps/plank/migrations/0019_anonymous_item.py b/aleksis/apps/plank/migrations/0019_anonymous_item.py new file mode 100644 index 0000000..a31d1da --- /dev/null +++ b/aleksis/apps/plank/migrations/0019_anonymous_item.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.7 on 2025-03-31 11:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plank', '0018_location_item_alter_category_icon'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='count', + field=models.PositiveSmallIntegerField(default=1, verbose_name='Item count'), + ), + migrations.AddField( + model_name='item', + name='is_anonymous', + field=models.BooleanField(default=False, verbose_name='Is anonymous?'), + ), + ] diff --git a/aleksis/apps/plank/models.py b/aleksis/apps/plank/models.py index 1331cb6..f1fb4b8 100644 --- a/aleksis/apps/plank/models.py +++ b/aleksis/apps/plank/models.py @@ -267,6 +267,9 @@ class Item(ExtensibleModel): ) last_time_seen_at = models.DateTimeField(auto_now=True, verbose_name=_("Last time seen at")) + is_anonymous = models.BooleanField(default=False, verbose_name=_("Is anonymous?")) + count = models.PositiveSmallIntegerField(default=1, verbose_name=_("Item count")) + class Meta: ordering = ["name", "item_type__name"] verbose_name = _("Item") @@ -282,6 +285,9 @@ class Item(ExtensibleModel): if not self.barcode: self.barcode = self.gen_barcode() + if not self.is_anonymous: + self.count = 1 + super().save(*args, **kwargs) def gen_barcode(self) -> str: @@ -298,6 +304,8 @@ class Item(ExtensibleModel): @property def is_available(self): """Check if item is available.""" + if self.is_anonymous: + return self.count > 0 return not self.current_checked_out_item @property diff --git a/aleksis/apps/plank/schema.py b/aleksis/apps/plank/schema.py index 4d4040e..fb97e06 100644 --- a/aleksis/apps/plank/schema.py +++ b/aleksis/apps/plank/schema.py @@ -269,6 +269,8 @@ class ItemGrapheneType(PermissionsForTypeMixin, DjangoObjectType): "inventory", "is_location", "id", + "is_anonymous", + "count", ) -- GitLab From 8f018713d92715483b483d2493923269c4c85869 Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Mon, 31 Mar 2025 17:40:57 +0200 Subject: [PATCH 3/6] Fix displaying of datetimes --- .../components/attributeRows/CheckInUntilRow.vue | 2 +- .../components/attributeRows/CheckedOutAtRow.vue | 2 +- .../components/attributeRows/LastTimeSeenAtRow.vue | 2 +- .../check_out_process/CheckOutProcessBaseList.vue | 2 +- .../components/item/CheckOutHistoryTable.vue | 14 +++++++++++++- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/aleksis/apps/plank/frontend/components/attributeRows/CheckInUntilRow.vue b/aleksis/apps/plank/frontend/components/attributeRows/CheckInUntilRow.vue index 85b920a..801a6ea 100644 --- a/aleksis/apps/plank/frontend/components/attributeRows/CheckInUntilRow.vue +++ b/aleksis/apps/plank/frontend/components/attributeRows/CheckInUntilRow.vue @@ -3,7 +3,7 @@ :icon="defaultIcons.date" :label="$t('plank.labels.check_in_until')" > - {{ data ? data : "–" }} + {{ data ? $d($parseISODate(data), "short") : "–" }} </attribute-row> </template> diff --git a/aleksis/apps/plank/frontend/components/attributeRows/CheckedOutAtRow.vue b/aleksis/apps/plank/frontend/components/attributeRows/CheckedOutAtRow.vue index c2cbf25..f13acf0 100644 --- a/aleksis/apps/plank/frontend/components/attributeRows/CheckedOutAtRow.vue +++ b/aleksis/apps/plank/frontend/components/attributeRows/CheckedOutAtRow.vue @@ -3,7 +3,7 @@ :icon="defaultIcons.checkedOutAt" :label="$t('plank.labels.checked_out_at')" > - {{ data ? data : "–" }} + {{ data ? $d($parseISODate(data), "shortDateTime") : "–" }} </attribute-row> </template> diff --git a/aleksis/apps/plank/frontend/components/attributeRows/LastTimeSeenAtRow.vue b/aleksis/apps/plank/frontend/components/attributeRows/LastTimeSeenAtRow.vue index 725b2f3..2753479 100644 --- a/aleksis/apps/plank/frontend/components/attributeRows/LastTimeSeenAtRow.vue +++ b/aleksis/apps/plank/frontend/components/attributeRows/LastTimeSeenAtRow.vue @@ -3,7 +3,7 @@ :icon="defaultIcons.lastTimeSeenAt" :label="$t('plank.labels.last_time_seen_at')" > - {{ data ? data : "–" }} + {{ data ? $d($parseISODate(data), "shortDateTime") : "–" }} </attribute-row> </template> diff --git a/aleksis/apps/plank/frontend/components/check_out_process/CheckOutProcessBaseList.vue b/aleksis/apps/plank/frontend/components/check_out_process/CheckOutProcessBaseList.vue index 5d5bb3f..39cc3cf 100644 --- a/aleksis/apps/plank/frontend/components/check_out_process/CheckOutProcessBaseList.vue +++ b/aleksis/apps/plank/frontend/components/check_out_process/CheckOutProcessBaseList.vue @@ -21,7 +21,7 @@ <router-link :to="{ name: 'plank.checkOutProcess', params: { id: item.id } }" > - {{ item.checkedOutAt }} + {{ $d($parseISODate(item.checkedOutAt), "shortDateTime") }} </router-link> </template> <!-- eslint-disable-next-line vue/valid-v-slot --> diff --git a/aleksis/apps/plank/frontend/components/item/CheckOutHistoryTable.vue b/aleksis/apps/plank/frontend/components/item/CheckOutHistoryTable.vue index 795f7b3..4a7802c 100644 --- a/aleksis/apps/plank/frontend/components/item/CheckOutHistoryTable.vue +++ b/aleksis/apps/plank/frontend/components/item/CheckOutHistoryTable.vue @@ -17,10 +17,22 @@ </template> <!-- eslint-disable-next-line vue/valid-v-slot --> <template #item.actions="{ item }"> - <v-btn text color="primary"> + <v-btn + text + color="primary" + :to="{ name: 'plank.checkOutProcess', params: { id: item.id } }" + > {{ $t("plank.actions.show_more") }} </v-btn> </template> + <!-- eslint-disable-next-line vue/valid-v-slot --> + <template #item.process.checkedOutAt="{ item }"> + {{ $d($parseISODate(item.process.checkedOutAt), "shortDateTime") }} + </template> + <!-- eslint-disable-next-line vue/valid-v-slot --> + <template #item.checkedInAt="{ item }"> + {{ $d($parseISODate(item.checkedInAt), "shortDateTime") }} + </template> </v-data-table> </template> -- GitLab From bf89e2e3096d56664aa37eb647c15941088fadf9 Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Mon, 31 Mar 2025 17:41:09 +0200 Subject: [PATCH 4/6] Use Core 4.0.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e799d7e..54b3b9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ url = "https://edugit.org/api/v4/projects/461/packages/pypi/simple" priority = "supplemental" [tool.poetry.dependencies] python = "^3.10" -aleksis-core = "^4.0.0.dev11" +aleksis-core = "^4.0.0" [tool.poetry.plugins."aleksis.app"] plank = "aleksis.apps.plank.apps:PlankConfig" -- GitLab From a608d478090dd267af9aa797fa17be0a6a40343c Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Tue, 1 Apr 2025 13:46:25 +0200 Subject: [PATCH 5/6] Implement check out of anonymous items --- .../components/check_out/CheckOutPage.vue | 58 ++++++++++++-- .../CheckOutProcessDetail.vue | 26 ++++++- .../check_out_process/queries.graphql | 1 + .../frontend/components/inputs/CountInput.vue | 2 +- .../components/item/CheckOutHistoryTable.vue | 13 +++- .../frontend/components/item/queries.graphql | 1 + aleksis/apps/plank/frontend/messages/en.json | 1 + .../migrations/0020_checkedoutitem_count.py | 18 +++++ aleksis/apps/plank/models.py | 6 ++ aleksis/apps/plank/schema.py | 76 +++++++++++-------- .../templates/plank/reports/check-out.html | 2 +- 11 files changed, 159 insertions(+), 45 deletions(-) create mode 100644 aleksis/apps/plank/migrations/0020_checkedoutitem_count.py diff --git a/aleksis/apps/plank/frontend/components/check_out/CheckOutPage.vue b/aleksis/apps/plank/frontend/components/check_out/CheckOutPage.vue index e702995..335cd91 100644 --- a/aleksis/apps/plank/frontend/components/check_out/CheckOutPage.vue +++ b/aleksis/apps/plank/frontend/components/check_out/CheckOutPage.vue @@ -41,21 +41,36 @@ <v-list> <template v-for="(item, index) in items"> <v-list-item :key="item.id"> + <v-list-item-icon class="mr-2"> + <v-text-field + class="count-input" + v-model.number="item.checkOutCount" + outlined + dense + hide-details + ></v-text-field> + </v-list-item-icon> <v-list-item-content> <v-list-item-title> <item-label :item="item" :with-barcode="false" /> + </v-list-item-title> + <v-list-item-subtitle> + <item-id :item="item" /> <v-chip v-if="item?.byContainer" label small outlined color="purple" + class="mx-1" > {{ item.location.name }} </v-chip> - </v-list-item-title> - <v-list-item-subtitle> - <item-id :item="item" /> + <anonymous-chip + small + v-if="item.isAnonymous" + class="mx-1" + /> </v-list-item-subtitle> </v-list-item-content> <v-list-item-action> @@ -96,7 +111,7 @@ input: { inventory, borrowingPerson: person, - items: itemIds, + items: mutationItems, checkOutCondition, checkInUntil: checkInUntil || null, }, @@ -153,6 +168,9 @@ <span v-if="snackbarStatus === 'already_added'"> {{ $t("plank.check_out.process.messages.already_added") }} </span> + <span v-else-if="snackbarStatus === 'no_more_available'"> + {{ $t("plank.check_out.process.messages.no_more_available") }} + </span> <span v-else-if="snackbarStatus === 'not_found'"> {{ $t("plank.check_out.process.messages.not_found") }} </span> @@ -182,9 +200,11 @@ import { checkOutInventories, checkOut, } from "./queries.graphql"; +import AnonymousChip from "../item/AnonymousChip.vue"; export default { name: "CheckOutPage", components: { + AnonymousChip, CheckInUntilInput, CheckOutConditionInput, InventoryInput, @@ -232,6 +252,11 @@ export default { itemIds() { return this.items.map((item) => item.id); }, + mutationItems() { + return this.items.map((item) => { + return { id: item.id, count: item.checkOutCount }; + }); + }, fullInventory() { if (!this.inventories) { return; @@ -271,11 +296,23 @@ export default { if (!data.item.item) { this.snackbarStatus = "not_found"; } else if (this.itemIds.includes(data.item.item.id)) { - this.snackbarStatus = "already_added"; + if (data.item.item.isAnonymous) { + let existingItem = this.items.find( + (i) => i.id === data.item.item.id, + ); + if (existingItem.checkOutCount < data.item.item.count) { + existingItem.checkOutCount++; + existingItem.count = data.item.item.count; + } else { + this.snackbarStatus = "no_more_available"; + } + } else { + this.snackbarStatus = "already_added"; + } } else if (!data.item.item.isAvailable) { this.snackbarStatus = "checked_out"; } else { - this.items.push(data.item.item); + this.items.push({ ...data.item.item, checkOutCount: 1 }); this.snackbarStatus = "added"; } @@ -325,3 +362,12 @@ export default { }, }; </script> + +<style scoped> +.count-input { + width: 40px; +} +.count-input input { + text-align: center; +} +</style> diff --git a/aleksis/apps/plank/frontend/components/check_out_process/CheckOutProcessDetail.vue b/aleksis/apps/plank/frontend/components/check_out_process/CheckOutProcessDetail.vue index fe05463..8ac5298 100644 --- a/aleksis/apps/plank/frontend/components/check_out_process/CheckOutProcessDetail.vue +++ b/aleksis/apps/plank/frontend/components/check_out_process/CheckOutProcessDetail.vue @@ -2,11 +2,21 @@ <detail-page :id="$route.params.id" :gql-query="gqlCheckOutProcess"> <template #title="{ item }"></template> <template #additional-actions="{ item }"> - <v-btn color="primary" :href="item.checkOutForm" target="_blank"> + <v-btn + color="primary" + :href="item.checkOutForm" + target="_blank" + class="ml-1" + > <v-icon left>mdi-file-pdf-box</v-icon> {{ $t("plank.check_out.process.download_form") }} </v-btn> - <v-btn color="primary" :href="item.checkInForm" target="_blank"> + <v-btn + color="primary" + :href="item.checkInForm" + target="_blank" + class="ml-1" + > <v-icon left>mdi-file-pdf-box</v-icon> {{ $t("plank.check_in.process.download_form") }} </v-btn> @@ -34,9 +44,19 @@ <template v-for="(checkedOutItem, index) in item.checkedOutItems" > - <v-list-item :key="checkedOutItem.id"> + <v-list-item + :key="checkedOutItem.id" + :to="{ + name: 'plank.item', + params: { id: checkedOutItem.item.id }, + }" + > <v-list-item-content> <v-list-item-title> + <template v-if="checkedOutItem.count !== 1"> + <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text --> + {{ checkedOutItem.count }} × + </template> <item-label :item="checkedOutItem.item" :with-barcode="false" diff --git a/aleksis/apps/plank/frontend/components/check_out_process/queries.graphql b/aleksis/apps/plank/frontend/components/check_out_process/queries.graphql index 0d952fc..c6cb71d 100644 --- a/aleksis/apps/plank/frontend/components/check_out_process/queries.graphql +++ b/aleksis/apps/plank/frontend/components/check_out_process/queries.graphql @@ -15,6 +15,7 @@ query checkOutProcess($id: ID!) { ...CheckOutProcessExtendedParts checkedOutItems { id + count checkedIn item { ...ItemBaseParts diff --git a/aleksis/apps/plank/frontend/components/inputs/CountInput.vue b/aleksis/apps/plank/frontend/components/inputs/CountInput.vue index cd53869..6e82c79 100644 --- a/aleksis/apps/plank/frontend/components/inputs/CountInput.vue +++ b/aleksis/apps/plank/frontend/components/inputs/CountInput.vue @@ -17,7 +17,7 @@ export default { name: "CountInput", props: { value: { - type: Number | String, + type: Number, required: true, }, }, diff --git a/aleksis/apps/plank/frontend/components/item/CheckOutHistoryTable.vue b/aleksis/apps/plank/frontend/components/item/CheckOutHistoryTable.vue index 4a7802c..14a040d 100644 --- a/aleksis/apps/plank/frontend/components/item/CheckOutHistoryTable.vue +++ b/aleksis/apps/plank/frontend/components/item/CheckOutHistoryTable.vue @@ -20,7 +20,7 @@ <v-btn text color="primary" - :to="{ name: 'plank.checkOutProcess', params: { id: item.id } }" + :to="{ name: 'plank.checkOutProcess', params: { id: item.process.id } }" > {{ $t("plank.actions.show_more") }} </v-btn> @@ -31,7 +31,11 @@ </template> <!-- eslint-disable-next-line vue/valid-v-slot --> <template #item.checkedInAt="{ item }"> - {{ $d($parseISODate(item.checkedInAt), "shortDateTime") }} + {{ + item.checkedInAt + ? $d($parseISODate(item.checkedInAt), "shortDateTime") + : item.checkedInAt + }} </template> </v-data-table> </template> @@ -57,6 +61,10 @@ export default { value: "process.checkedOutAt", text: this.$t("plank.labels.checked_out_at"), }, + { + value: "count", + text: this.$t("plank.labels.count"), + }, { value: "checkedInAt", text: this.$t("plank.labels.checked_in_at"), @@ -70,7 +78,6 @@ export default { text: this.$t("plank.actions.title"), sortable: false, }, - // FIXME ACtions column with link ]; }, }, diff --git a/aleksis/apps/plank/frontend/components/item/queries.graphql b/aleksis/apps/plank/frontend/components/item/queries.graphql index 5065114..0bae38c 100644 --- a/aleksis/apps/plank/frontend/components/item/queries.graphql +++ b/aleksis/apps/plank/frontend/components/item/queries.graphql @@ -22,6 +22,7 @@ query item($id: ID!) { checkedOutItems { id checkedInAt + count process { id checkedOutAt diff --git a/aleksis/apps/plank/frontend/messages/en.json b/aleksis/apps/plank/frontend/messages/en.json index b98f1d8..33c3e53 100644 --- a/aleksis/apps/plank/frontend/messages/en.json +++ b/aleksis/apps/plank/frontend/messages/en.json @@ -154,6 +154,7 @@ "another_check_out": "Do another check out", "messages": { "already_added": "This item has already been added to the check out list.", + "no_more_available": "There are no more items available.", "not_found": "There is no item with this ID or barcode.", "checked_out": "This item is currently checked out.", "added": "The item has been added to the check out list." diff --git a/aleksis/apps/plank/migrations/0020_checkedoutitem_count.py b/aleksis/apps/plank/migrations/0020_checkedoutitem_count.py new file mode 100644 index 0000000..2cc25db --- /dev/null +++ b/aleksis/apps/plank/migrations/0020_checkedoutitem_count.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.7 on 2025-04-01 11:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('plank', '0019_anonymous_item'), + ] + + operations = [ + migrations.AddField( + model_name='checkedoutitem', + name='count', + field=models.PositiveIntegerField(default=1, verbose_name='Count'), + ), + ] diff --git a/aleksis/apps/plank/models.py b/aleksis/apps/plank/models.py index f1fb4b8..66e80de 100644 --- a/aleksis/apps/plank/models.py +++ b/aleksis/apps/plank/models.py @@ -461,6 +461,7 @@ class CheckedOutItem(ExtensibleModel): item = models.ForeignKey( Item, on_delete=models.CASCADE, related_name="checked_out_items", verbose_name=_("Item") ) + count = models.PositiveIntegerField(verbose_name=_("Count"), default=1) process = models.ForeignKey( CheckOutProcess, on_delete=models.CASCADE, @@ -478,6 +479,11 @@ class CheckedOutItem(ExtensibleModel): null=True, ) + def save(self, *args, **kwargs): + if self.item.is_anonymous: + self.count = 1 + super().save(*args, **kwargs) + class Meta: ordering = [ "-process__check_in_until", diff --git a/aleksis/apps/plank/schema.py b/aleksis/apps/plank/schema.py index fb97e06..affee54 100644 --- a/aleksis/apps/plank/schema.py +++ b/aleksis/apps/plank/schema.py @@ -1,3 +1,4 @@ +from django.db import transaction from django.db.models import Model, Q from django.urls import reverse from django.utils.translation import gettext as _ @@ -222,7 +223,7 @@ class DeleteManufacturerMutation(DeleteMutation): class CheckedOutItemType(DjangoObjectType): class Meta: model = CheckedOutItem - fields = ("item", "id", "process", "checked_in", "checked_in_at", "checked_in_by") + fields = ("item", "id", "process", "count", "checked_in", "checked_in_at", "checked_in_by") class ItemGrapheneType(PermissionsForTypeMixin, DjangoObjectType): @@ -456,10 +457,15 @@ class SortOrCheckMutation(graphene.Mutation): return SortOrCheckMutation(item=item, status=status) +class CheckOutInputItemType(graphene.InputObjectType): + id = graphene.ID(required=True) + count = graphene.Int(required=True) + + class CheckOutInputType(graphene.InputObjectType): inventory = graphene.ID(required=True) borrowing_person = graphene.ID(required=True) - items = graphene.List(graphene.ID, required=True) + items = graphene.List(CheckOutInputItemType, required=True) check_out_condition = graphene.ID(required=False) check_in_until = graphene.Date(required=False) @@ -472,35 +478,43 @@ class CheckOutMutation(graphene.Mutation): @staticmethod def mutate(root, info, input, **kwargs): # noqa - # FIXME PERMISSION CHECK - - print("START MUTATE") - inventory = Inventory.objects.get(pk=input.inventory) - print(inventory) - borrowing_person = Person.objects.get(pk=input.borrowing_person) - print(borrowing_person) - check_out_condition = None - if input.check_out_condition: - check_out_condition = CheckOutCondition.objects.get(pk=input.check_out_condition) - print(check_out_condition) - items = Item.objects.filter(pk__in=input.items) - print(items) - process = CheckOutProcess.objects.create( - inventory=inventory, - lending_person=info.context.user.person, - borrowing_person=borrowing_person, - condition=check_out_condition, - check_in_until=input.check_in_until, - is_check_out_in_process=False, - ) - print(process) - checked_out_items = [] - for item in items: - if not item.is_available: - continue - checked_out_item = CheckedOutItem(process=process, item=item) - checked_out_items.append(checked_out_item) - CheckedOutItem.objects.bulk_create(checked_out_items) + with transaction.atomic(): + inventory = Inventory.objects.get(pk=input.inventory) + borrowing_person = Person.objects.get(pk=input.borrowing_person) + check_out_condition = None + if input.check_out_condition: + check_out_condition = CheckOutCondition.objects.get(pk=input.check_out_condition) + + count_by_id = {int(item.id): item.count for item in input.items} + items = Item.objects.filter(pk__in=[item.id for item in input.items]) + + process = CheckOutProcess.objects.create( + inventory=inventory, + lending_person=info.context.user.person, + borrowing_person=borrowing_person, + condition=check_out_condition, + check_in_until=input.check_in_until, + is_check_out_in_process=False, + ) + + checked_out_items = [] + for item in items: + if not info.context.user.has_perm("plank.check_out_rule", item): + return + + count = count_by_id[item.id] if item.is_anonymous else 1 + if not item.is_available: + continue + if item.is_anonymous and item.count - count < 0: + continue + + if item.is_anonymous: + item.count -= count + item.save() + + checked_out_item = CheckedOutItem(process=process, item=item, count=count) + checked_out_items.append(checked_out_item) + CheckedOutItem.objects.bulk_create(checked_out_items) return CheckOutMutation(process=process) diff --git a/aleksis/apps/plank/templates/plank/reports/check-out.html b/aleksis/apps/plank/templates/plank/reports/check-out.html index 1768cd2..dfab362 100644 --- a/aleksis/apps/plank/templates/plank/reports/check-out.html +++ b/aleksis/apps/plank/templates/plank/reports/check-out.html @@ -31,7 +31,7 @@ </div> <ul class="browser-default"> {% for checked_out_item in process.checked_out_items.all %} - <li>{{ checked_out_item.item.name }} ({% if checked_out_item.item.item_type %}{{ checked_out_item.item.item_type.name }} + <li>{% if checked_out_item.count != 1 %}{{ checked_out_item.count }} × {% endif %}{{ checked_out_item.item.name }} ({% if checked_out_item.item.item_type %}{{ checked_out_item.item.item_type.name }} , {% endif %}{{ checked_out_item.item.barcode }}) </li> {% endfor %} -- GitLab From b6d8c3e5e4e950af868b55d315f61c8640be9c5e Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Thu, 3 Apr 2025 12:16:24 +0200 Subject: [PATCH 6/6] Support check in of an. objects --- .../components/check_in/CheckInPage.vue | 130 +++++++++++++++--- .../components/check_in/checkIn.graphql | 29 +++- .../components/check_out/CheckOutPage.vue | 2 +- aleksis/apps/plank/frontend/messages/en.json | 7 +- aleksis/apps/plank/models.py | 32 +++-- aleksis/apps/plank/schema.py | 28 ++-- 6 files changed, 182 insertions(+), 46 deletions(-) diff --git a/aleksis/apps/plank/frontend/components/check_in/CheckInPage.vue b/aleksis/apps/plank/frontend/components/check_in/CheckInPage.vue index 176bdda..09c548a 100644 --- a/aleksis/apps/plank/frontend/components/check_in/CheckInPage.vue +++ b/aleksis/apps/plank/frontend/components/check_in/CheckInPage.vue @@ -4,28 +4,70 @@ <v-card> <v-card-title>{{ $t("plank.check_in.process.title") }}</v-card-title> <v-card-text> - <ApolloMutation - :mutation="gqlCheckIn" - :variables="{ idOrBarcode }" - @done="checkedIn" + <scan-input + v-model="idOrBarcode" + :hint="$t('plank.check_in.process.scan_hint')" + ref="scanInput" + persistent-hint + :disabled="loading" + @submit="search" + /> + <v-btn + color="primary" + @click="search" + class="mb-4" + :loading="loading" + :disabled="loading" > - <template #default="{ mutate, loading, error }"> - <scan-input - v-model="idOrBarcode" - :hint="$t('plank.check_in.process.scan_hint')" - ref="scanInput" - persistent-hint - @submit="mutate" - /> - <v-btn color="primary" @click="mutate" class="mb-4"> - {{ $t("plank.check_in.process.check_in") }} - </v-btn> - </template> - </ApolloMutation> + {{ $t("plank.check_in.process.check_in") }} + </v-btn> </v-card-text> </v-card> </v-col> + <v-dialog :value="this.itemsToSelect.length > 0" persistent max-width="600"> + <v-card> + <v-card-title>{{ + $t("plank.check_in.process.select_item") + }}</v-card-title> + <v-card-text>{{ + $t("plank.check_in.process.select_item_description") + }}</v-card-text> + <v-list two-line> + <v-list-item + v-for="item in itemsToSelect" + :key="item.id" + @click="checkIn(item.id)" + > + <v-list-item-content> + <v-list-item-title> + <template v-if="item.count !== 1"> + <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text --> + {{ item.count }} × + </template> + <item-label :item="item.item" /> + </v-list-item-title> + <v-list-item-subtitle> + {{ + $t("plank.check_in.process.checked_out_to_at", { + id: item.process.id, + to: item.process.borrowingPerson.fullName, + at: $d( + $parseISODate(item.process.checkedOutAt), + "shortDateTime", + ), + }) + }} + </v-list-item-subtitle> + </v-list-item-content> + <v-list-item-icon> + <v-icon>mdi-chevron-right</v-icon> + </v-list-item-icon> + </v-list-item> + </v-list> + </v-card> + </v-dialog> + <v-col cols="12" lg="6"> <v-card v-if="checkedOutItems.length > 0"> <v-card-title> @@ -38,6 +80,10 @@ v-for="checkedOutItem in checkedOutItems" :key="checkedOutItem.id" > + <template v-if="checkedOutItem.count !== 1"> + <!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text --> + {{ checkedOutItem.count }} × + </template> {{ checkedOutItem.item.name }} (#{{ checkedOutItem.item.id }}) </p> </v-card-text> @@ -79,11 +125,14 @@ <script> import ScanInput from "../inputs/ScanInput.vue"; -import { checkIn } from "./checkIn.graphql"; +import ItemLabel from "../item/ItemLabel.vue"; + +import { checkIn, checkedOutItemsById } from "./checkIn.graphql"; export default { name: "CheckInPage", components: { ScanInput, + ItemLabel, }, data() { return { @@ -91,7 +140,10 @@ export default { snackbar: false, snackbarStatus: "", checkedOutItems: [], + itemsToSelect: [], gqlCheckIn: checkIn, + gqlCheckedOutItemsById: checkedOutItemsById, + loading: false, }; }, computed: { @@ -107,6 +159,48 @@ export default { this.idOrBarcode = ""; this.focusScanInput(); }, + search() { + this.loading = true; + this.$apollo + .query({ + query: checkedOutItemsById, + variables: { id: this.idOrBarcode }, + fetchPolicy: "no-cache", + }) + .then(({ data }) => { + console.log(data); + const items = data.checkedOutItemsById; + if (items.length === 0) { + this.snackbar = true; + this.snackbarStatus = "not_found"; + } else if (items.length === 1) { + this.checkIn(items[0].id); + } else { + this.itemsToSelect = items; + } + }) + .finally(() => { + this.loading = false; + }); + }, + checkIn(id) { + console.log("check in", id); + this.$apollo + .mutate({ + mutation: checkIn, + variables: { id }, + }) + .then(({ data }) => { + console.log(data); + const { status, checkedOutItems } = data.checkIn; + this.snackbarStatus = status; + this.snackbar = true; + if (checkedOutItems?.length > 0) { + this.checkedOutItems = checkedOutItems; + } + this.itemsToSelect = []; + }); + }, checkedIn({ data }) { const { status, checkedOutItems } = data.checkIn; this.snackbarStatus = status; diff --git a/aleksis/apps/plank/frontend/components/check_in/checkIn.graphql b/aleksis/apps/plank/frontend/components/check_in/checkIn.graphql index 590edfd..5fef1df 100644 --- a/aleksis/apps/plank/frontend/components/check_in/checkIn.graphql +++ b/aleksis/apps/plank/frontend/components/check_in/checkIn.graphql @@ -1,7 +1,32 @@ -mutation checkIn($idOrBarcode: ID!) { - checkIn(idOrBarcode: $idOrBarcode) { +query checkedOutItemsById($id: ID!) { + checkedOutItemsById(id: $id) { + id + count + process { + id + checkedOutAt + borrowingPerson { + id + fullName + } + } + item { + id + name + barcode + itemType { + id + name + } + } + } +} + +mutation checkIn($id: ID!) { + checkIn(id: $id) { checkedOutItems { id + count process { id checkInForm diff --git a/aleksis/apps/plank/frontend/components/check_out/CheckOutPage.vue b/aleksis/apps/plank/frontend/components/check_out/CheckOutPage.vue index 335cd91..adc92bb 100644 --- a/aleksis/apps/plank/frontend/components/check_out/CheckOutPage.vue +++ b/aleksis/apps/plank/frontend/components/check_out/CheckOutPage.vue @@ -41,7 +41,7 @@ <v-list> <template v-for="(item, index) in items"> <v-list-item :key="item.id"> - <v-list-item-icon class="mr-2"> + <v-list-item-icon class="mr-2" v-if="item.isAnonymous"> <v-text-field class="count-input" v-model.number="item.checkOutCount" diff --git a/aleksis/apps/plank/frontend/messages/en.json b/aleksis/apps/plank/frontend/messages/en.json index 33c3e53..d68701f 100644 --- a/aleksis/apps/plank/frontend/messages/en.json +++ b/aleksis/apps/plank/frontend/messages/en.json @@ -172,10 +172,13 @@ "download_form": "Download check in form", "show_process": "Show check out process", "messages": { - "not_found": "There is no item with this ID or barcode.", + "not_found": "There is no checked out item with this ID or barcode.", "checked_in": "The item has been successfully checked in.", "not_checked_out": "The item is not checked out." - } + }, + "select_item": "Select an item", + "select_item_description": "There are multiple check outs for this item (e. g. because it's anonymous). Please select the one you want to check in.", + "checked_out_to_at": "Checked out to {to} at {at} (#{id})" } }, "check_out_process": { diff --git a/aleksis/apps/plank/models.py b/aleksis/apps/plank/models.py index 66e80de..31ab9c0 100644 --- a/aleksis/apps/plank/models.py +++ b/aleksis/apps/plank/models.py @@ -1,7 +1,7 @@ from typing import Optional -from django.db import models -from django.db.models import Max +from django.db import models, transaction +from django.db.models import Max, QuerySet from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -308,10 +308,15 @@ class Item(ExtensibleModel): return self.count > 0 return not self.current_checked_out_item + @property + def current_checked_out_items(self) -> QuerySet["CheckedOutItem"]: + """Get current check outs for this item.""" + return self.checked_out_items.filter(checked_in=False) + @property def current_checked_out_item(self) -> Optional["CheckedOutItem"]: """Get current check out for this item (if checked out).""" - qs = self.checked_out_items.filter(checked_in=False) + qs = self.current_checked_out_items if qs.exists(): return qs.first() return None @@ -342,16 +347,6 @@ class Item(ExtensibleModel): except cls.DoesNotExist: return None - def check_in(self, person: Person) -> "CheckedOutItem": - """Check in item.""" - checked_out_item = self.current_checked_out_item - if checked_out_item: - checked_out_item.checked_in = True - checked_out_item.checked_in_at = timezone.now() - checked_out_item.checked_in_by = person - checked_out_item.save() - return checked_out_item - class CheckOutCondition(ExtensibleModel): """A condition to which an item is checked out.""" @@ -479,6 +474,17 @@ class CheckedOutItem(ExtensibleModel): null=True, ) + def check_in(self, person: Person): + """Check in item.""" + with transaction.atomic(): + if self.item.is_anonymous: + self.item.count += self.count + self.item.save() + self.checked_in = True + self.checked_in_at = timezone.now() + self.checked_in_by = person + self.save() + def save(self, *args, **kwargs): if self.item.is_anonymous: self.count = 1 diff --git a/aleksis/apps/plank/schema.py b/aleksis/apps/plank/schema.py index affee54..d7b326d 100644 --- a/aleksis/apps/plank/schema.py +++ b/aleksis/apps/plank/schema.py @@ -520,25 +520,25 @@ class CheckOutMutation(graphene.Mutation): class CheckInMutation(graphene.Mutation): class Arguments: - id_or_barcode = graphene.ID(required=True) + id = graphene.ID(required=True) checked_out_items = graphene.List(CheckedOutItemType) status = graphene.Field(graphene.String) @staticmethod - def mutate(root, info, id_or_barcode, **kwargs): # noqa - item = Item.get_by_id_or_barcode(id_or_barcode) - if not item: + def mutate(root, info, id, **kwargs): # noqa + checked_out_item = CheckedOutItem.objects.get(pk=id) + if not checked_out_item: return CheckInMutation(status="not_found") - if not info.context.user.has_perm("plank.check_in_item_rule", item): + if not info.context.user.has_perm("plank.check_in_item_rule", checked_out_item.item): return - if item.is_available: + if checked_out_item.checked_in: return CheckInMutation(status="not_checked_out") - checked_out_items = [item.current_checked_out_item] - item.check_in(person=info.context.user.person) - for contained_item in item.contained_items: - if not contained_item.is_available: + checked_out_items = [checked_out_item] + checked_out_item.check_in(person=info.context.user.person) + for contained_item in checked_out_item.item.contained_items: + if not contained_item.is_available and not contained_item.is_anonymous: checked_out_items.append(contained_item.current_checked_out_item) contained_item.check_in(person=info.context.user.person) @@ -563,6 +563,7 @@ class Query: items = graphene.List(ItemGrapheneType) item_by_id = graphene.Field(ItemGrapheneType, id=graphene.ID(required=True)) check_out_items_by_id = graphene.Field(CheckedOutItemsType, id=graphene.ID(required=True)) + checked_out_items_by_id = graphene.List(CheckedOutItemType, id=graphene.ID(required=True)) check_out_conditions = graphene.List(CheckOutConditionType) check_out_processes = graphene.List(CheckOutProcessType) check_out_process_by_id = graphene.Field(CheckOutProcessType, id=graphene.ID(required=True)) @@ -630,6 +631,13 @@ class Query: return CheckedOutItemsType(item=item, contained_items=item.contained_items) + def resolve_checked_out_items_by_id(self, info, id, **kwargs): # noqa + item = Item.get_by_id_or_barcode(id) + if not item or not info.context.user.has_perm("plank.view_item_rule", item): + return None + + return item.current_checked_out_items + def resolve_item_types(self, info, permission=None, **kwargs): if not info.context.user.has_perm("plank.view_itemtype"): return ItemType.objects.filter( -- GitLab