Bugzilla – Attachment 125202 Details for
Bug 28854
Add ability to create bundles of items for circulation
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 28854: Expose functionality to attach items to bundles
Bug-28854-Expose-functionality-to-attach-items-to-.patch (text/plain), 36.70 KB, created by
Martin Renvoize (ashimema)
on 2021-09-23 14:40:24 UTC
(
hide
)
Description:
Bug 28854: Expose functionality to attach items to bundles
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2021-09-23 14:40:24 UTC
Size:
36.70 KB
patch
obsolete
>From 9995cdf17a2e7432f7be54f3dfd00c84a12942aa Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@ptfs-europe.com> >Date: Thu, 23 Sep 2021 10:01:23 +0100 >Subject: [PATCH] Bug 28854: Expose functionality to attach items to bundles > >This patch adds methods the the Koha::Item object for managing item >bundling operations and then exposes those methods via the REST API. > >We include the new `BundleNotLoanValue` preference for setting not >for loan values when an item is added to a bundle. > >Finally, we expose bundle management via the catalogue details page. > >Test plan: >0) Apply patches up to this point >1) Creating a new bundle > * Add a new bib record > * Mark the bib record as a 'collection' type by setting leader > position 7 to 'c' > * Add a new item to this bib record > * You should see a new 'Manage bundle' button available in the > 'Actions' column of the Holdings table. > * Clicking 'Manage bundle' should expand the table to include a new > row directly beneath this one. > * Use the new 'Add to bundle' button that appears in this row to > trigger a modal that allows entering the barcode of items you wish > to add to the bundle > * Upon closing the modal, the bundle content table should reload and > contain your newly associated items. > * You can subsequently remove an item from a bundle using the new > 'Remove' button. >2) Not for loan > * Items that have been added into a bundle should now appear as 'Not > for loan' from their original biblio record and note which bundle > they belong to. >3) Error cases > * Try adding an item that already belongs to a bundle to another > bundle: Note an error is displayed in the modal form. > >TODO: Add Object Tests + API Tests > >https://bugs.koha-community.org/show_bug.cgi?id=24454 >--- > Koha/Item.pm | 165 ++++++++++++ > Koha/REST/V1/Items.pm | 121 +++++++++ > api/v1/swagger/definitions.json | 3 + > api/v1/swagger/definitions/bundle_link.json | 14 + > api/v1/swagger/definitions/item.json | 2 + > api/v1/swagger/paths.json | 9 + > api/v1/swagger/paths/items.json | 240 ++++++++++++++++++ > catalogue/detail.pl | 14 + > .../prog/css/src/staff-global.scss | 4 + > .../admin/preferences/cataloguing.pref | 4 + > .../prog/en/modules/catalogue/detail.tt | 228 ++++++++++++++++- > 11 files changed, 803 insertions(+), 1 deletion(-) > create mode 100644 api/v1/swagger/definitions/bundle_link.json > >diff --git a/Koha/Item.pm b/Koha/Item.pm >index 60d80a5c3eb..2a69ab13d00 100644 >--- a/Koha/Item.pm >+++ b/Koha/Item.pm >@@ -21,6 +21,7 @@ use Modern::Perl; > > use List::MoreUtils qw( any ); > use Data::Dumper qw( Dumper ); >+use Try::Tiny qw( catch try ); > > use Koha::Database; > use Koha::DateUtils qw( dt_from_string ); >@@ -1272,6 +1273,170 @@ sub move_to_biblio { > return $to_biblionumber; > } > >+=head3 bundle_items >+ >+ my $bundle_items = $item->bundle_items; >+ >+Returns the items associated with this bundle >+ >+=cut >+ >+sub bundle_items { >+ my ($self) = @_; >+ >+ if ( !$self->{_bundle_items_cached} ) { >+ my $bundle_items = Koha::Items->search( >+ { 'item_bundles_item.host' => $self->itemnumber }, >+ { join => 'item_bundles_item' } ); >+ $self->{_bundle_items} = $bundle_items; >+ $self->{_bundle_items_cached} = 1; >+ } >+ >+ return $self->{_bundle_items}; >+} >+ >+=head3 is_bundle >+ >+ my $is_bundle = $item->is_bundle; >+ >+Returns whether the item is a bundle or not >+ >+=cut >+ >+sub is_bundle { >+ my ($self) = @_; >+ return $self->bundle_items->count ? 1 : 0; >+} >+ >+=head3 bundle_host >+ >+ my $bundle = $item->bundle_host; >+ >+Returns the bundle item this item is attached to >+ >+=cut >+ >+sub bundle_host { >+ my ($self) = @_; >+ >+ if ( !$self->{_bundle_host_cached} ) { >+ my $bundle_item_rs = $self->_result->item_bundles_item; >+ $self->{_bundle_host} = >+ $bundle_item_rs >+ ? Koha::Item->_new_from_dbic($bundle_item_rs->host) >+ : undef; >+ $self->{_bundle_host_cached} = 1; >+ } >+ >+ return $self->{_bundle_host}; >+} >+ >+=head3 in_bundle >+ >+ my $in_bundle = $item->in_bundle; >+ >+Returns whether this item is currently in a bundle >+ >+=cut >+ >+sub in_bundle { >+ my ($self) = @_; >+ return $self->bundle_host ? 1 : 0; >+} >+ >+=head3 add_to_bundle >+ >+ my $link = $item->add_to_bundle($bundle_item); >+ >+Adds the bundle_item passed to this item >+ >+=cut >+ >+sub add_to_bundle { >+ my ( $self, $bundle_item ) = @_; >+ >+ my $schema = Koha::Database->new->schema; >+ >+ my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue'); >+ >+ try { >+ $schema->txn_do( >+ sub { >+ $self->_result->add_to_item_bundles_hosts( >+ { item => $bundle_item->itemnumber } ); >+ >+ $bundle_item->notforloan($BundleNotLoanValue)->store(); >+ } >+ ); >+ } >+ catch { >+ >+ # FIXME: See if we can move the below copy/paste from Koha::Object::store into it's own class and catch at a lower level in the Schema instantiation.. take inspiration fro DBIx::Error >+ if ( ref($_) eq 'DBIx::Class::Exception' ) { >+ warn $_->{msg}; >+ if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) { >+ # FK constraints >+ # FIXME: MySQL error, if we support more DB engines we should implement this for each >+ if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) { >+ Koha::Exceptions::Object::FKConstraint->throw( >+ error => 'Broken FK constraint', >+ broken_fk => $+{column} >+ ); >+ } >+ } >+ elsif ( >+ $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ ) >+ { >+ Koha::Exceptions::Object::DuplicateID->throw( >+ error => 'Duplicate ID', >+ duplicate_id => $+{key} >+ ); >+ } >+ elsif ( $_->{msg} =~ >+/Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/ >+ ) >+ { # The optional \W in the regex might be a quote or backtick >+ my $type = $+{type}; >+ my $value = $+{value}; >+ my $property = $+{property}; >+ $property =~ s/['`]//g; >+ Koha::Exceptions::Object::BadValue->throw( >+ type => $type, >+ value => $value, >+ property => $property =~ /(\w+\.\w+)$/ >+ ? $1 >+ : $property >+ , # results in table.column without quotes or backtics >+ ); >+ } >+ >+ # Catch-all for foreign key breakages. It will help find other use cases >+ $_->rethrow(); >+ } >+ else { >+ $_; >+ } >+ }; >+} >+ >+=head3 remove_from_bundle >+ >+Remove this item from any bundle it may have been attached to. >+ >+=cut >+ >+sub remove_from_bundle { >+ my ($self) = @_; >+ >+ my $bundle_item_rs = $self->_result->item_bundles_item; >+ if ( $bundle_item_rs ) { >+ $bundle_item_rs->delete; >+ $self->notforloan(0)->store(); >+ return 1; >+ } >+ return 0; >+} >+ > =head2 Internal methods > > =head3 _after_item_action_hooks >diff --git a/Koha/REST/V1/Items.pm b/Koha/REST/V1/Items.pm >index 1887bce5145..61150013e16 100644 >--- a/Koha/REST/V1/Items.pm >+++ b/Koha/REST/V1/Items.pm >@@ -152,4 +152,125 @@ sub pickup_locations { > }; > } > >+=head3 bundled_items >+ >+Controller function that handles bundled_items Koha::Item objects >+ >+=cut >+ >+sub bundled_items { >+ my $c = shift->openapi->valid_input or return; >+ >+ my $item_id = $c->validation->param('item_id'); >+ my $item = Koha::Items->find( $item_id ); >+ >+ unless ($item) { >+ return $c->render( >+ status => 404, >+ openapi => { error => "Item not found" } >+ ); >+ } >+ >+ return try { >+ my $items_set = $item->bundle_items; >+ my $items = $c->objects->search( $items_set ); >+ return $c->render( >+ status => 200, >+ openapi => $items >+ ); >+ } >+ catch { >+ $c->unhandled_exception($_); >+ }; >+} >+ >+=head3 add_to_bundle >+ >+Controller function that handles adding items to this bundle >+ >+=cut >+ >+sub add_to_bundle { >+ my $c = shift->openapi->valid_input or return; >+ >+ my $item_id = $c->validation->param('item_id'); >+ my $item = Koha::Items->find( $item_id ); >+ >+ unless ($item) { >+ return $c->render( >+ status => 404, >+ openapi => { error => "Item not found" } >+ ); >+ } >+ >+ >+ my $bundle_item_id = $c->validation->param('body')->{'external_id'}; >+ my $bundle_item = Koha::Items->find( { barcode => $bundle_item_id } ); >+ >+ unless ($bundle_item) { >+ return $c->render( >+ status => 404, >+ openapi => { error => "Bundle item not found" } >+ ); >+ } >+ >+ return try { >+ my $link = $item->add_to_bundle($bundle_item); >+ return $c->render( >+ status => 201, >+ openapi => $bundle_item >+ ); >+ } >+ catch { >+ if ( ref($_) eq 'Koha::Exceptions::Object::DuplicateID' ) { >+ return $c->render( >+ status => 409, >+ openapi => { >+ error => 'Item is already bundled', >+ key => $_->duplicate_id >+ } >+ ); >+ } >+ else { >+ $c->unhandled_exception($_); >+ } >+ }; >+} >+ >+=head3 remove_from_bundle >+ >+Controller function that handles removing items from this bundle >+ >+=cut >+ >+sub remove_from_bundle { >+ my $c = shift->openapi->valid_input or return; >+ >+ my $item_id = $c->validation->param('item_id'); >+ my $item = Koha::Items->find( $item_id ); >+ >+ unless ($item) { >+ return $c->render( >+ status => 404, >+ openapi => { error => "Item not found" } >+ ); >+ } >+ >+ my $bundle_item_id = $c->validation->param('bundled_item_id'); >+ my $bundle_item = Koha::Items->find( { itemnumber => $bundle_item_id } ); >+ >+ unless ($bundle_item) { >+ return $c->render( >+ status => 404, >+ openapi => { error => "Bundle item not found" } >+ ); >+ } >+ >+ $bundle_item->remove_from_bundle; >+ return $c->render( >+ status => 204, >+ openapi => q{} >+ ); >+} >+ > 1; >diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json >index 402eed6b256..ddd65a90e53 100644 >--- a/api/v1/swagger/definitions.json >+++ b/api/v1/swagger/definitions.json >@@ -5,6 +5,9 @@ > "basket": { > "$ref": "definitions/basket.json" > }, >+ "bundle_link": { >+ "$ref": "definitions/bundle_link.json" >+ }, > "cashup": { > "$ref": "definitions/cashup.json" > }, >diff --git a/api/v1/swagger/definitions/bundle_link.json b/api/v1/swagger/definitions/bundle_link.json >new file mode 100644 >index 00000000000..7b1274dfe4f >--- /dev/null >+++ b/api/v1/swagger/definitions/bundle_link.json >@@ -0,0 +1,14 @@ >+{ >+ "type": "object", >+ "properties": { >+ "item_id": { >+ "type": ["integer", "null"], >+ "description": "Internal item identifier" >+ }, >+ "external_id": { >+ "type": ["string", "null"], >+ "description": "Item barcode" >+ } >+ }, >+ "additionalProperties": false >+} >diff --git a/api/v1/swagger/definitions/item.json b/api/v1/swagger/definitions/item.json >index 14c110714e3..61de43a6f00 100644 >--- a/api/v1/swagger/definitions/item.json >+++ b/api/v1/swagger/definitions/item.json >@@ -9,6 +9,8 @@ > "type": "integer", > "description": "Internal identifier for the parent bibliographic record" > }, >+ "biblio": { >+ }, > "external_id": { > "type": ["string", "null"], > "description": "The item's barcode" >diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json >index 23f420cc05f..fa30c86624a 100644 >--- a/api/v1/swagger/paths.json >+++ b/api/v1/swagger/paths.json >@@ -83,6 +83,15 @@ > "/items/{item_id}": { > "$ref": "paths/items.json#/~1items~1{item_id}" > }, >+ "/items/{item_id}/bundled_items": { >+ "$ref": "paths/items.json#/~1items~1{item_id}~1bundled_items" >+ }, >+ "/items/{item_id}/bundled_items/item": { >+ "$ref": "paths/items.json#/~1items~1{item_id}~1bundled_items~1item" >+ }, >+ "/items/{item_id}/bundled_items/item/{bundled_item_id}": { >+ "$ref": "paths/items.json#/~1items~1{item_id}~1bundled_items~1item~1{bundled_item_id}" >+ }, > "/items/{item_id}/pickup_locations": { > "$ref": "paths/items.json#/~1items~1{item_id}~1pickup_locations" > }, >diff --git a/api/v1/swagger/paths/items.json b/api/v1/swagger/paths/items.json >index 7c18f625ee7..a0a2caf9d7b 100644 >--- a/api/v1/swagger/paths/items.json >+++ b/api/v1/swagger/paths/items.json >@@ -127,6 +127,246 @@ > } > } > }, >+ "/items/{item_id}/bundled_items": { >+ "get": { >+ "x-mojo-to": "Items#bundled_items", >+ "operationId": "bundledItems", >+ "tags": [ >+ "items" >+ ], >+ "summary": "List bundled items", >+ "parameters": [ >+ { >+ "$ref": "../parameters.json#/item_id_pp" >+ }, >+ { >+ "name": "external_id", >+ "in": "query", >+ "description": "Search on the item's barcode", >+ "required": false, >+ "type": "string" >+ }, >+ { >+ "$ref": "../parameters.json#/match" >+ }, >+ { >+ "$ref": "../parameters.json#/order_by" >+ }, >+ { >+ "$ref": "../parameters.json#/page" >+ }, >+ { >+ "$ref": "../parameters.json#/per_page" >+ }, >+ { >+ "$ref": "../parameters.json#/q_param" >+ }, >+ { >+ "$ref": "../parameters.json#/q_body" >+ }, >+ { >+ "$ref": "../parameters.json#/q_header" >+ } >+ ], >+ "consumes": [ >+ "application/json" >+ ], >+ "produces": [ >+ "application/json" >+ ], >+ "responses": { >+ "200": { >+ "description": "A list of item", >+ "schema": { >+ "type": "array", >+ "items": { >+ "$ref": "../definitions.json#/item" >+ } >+ } >+ }, >+ "401": { >+ "description": "Authentication required", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "403": { >+ "description": "Access forbidden", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "500": { >+ "description": "Internal server error", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "503": { >+ "description": "Under maintenance", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ } >+ }, >+ "x-koha-authorization": { >+ "permissions": { >+ "catalogue": "1" >+ } >+ }, >+ "x-koha-embed": [ >+ "biblio", >+ "checkout" >+ ] >+ } >+ }, >+ "/items/{item_id}/bundled_items/item": { >+ "post": { >+ "x-mojo-to": "Items#add_to_bundle", >+ "operationId": "addToBundle", >+ "tags": ["items"], >+ "summary": "Add item to bundle", >+ "parameters": [{ >+ "$ref": "../parameters.json#/item_id_pp" >+ }, >+ { >+ "name": "body", >+ "in": "body", >+ "description": "A JSON object containing information about the new bundle link", >+ "required": true, >+ "schema": { >+ "$ref": "../definitions.json#/bundle_link" >+ } >+ } >+ ], >+ "consumes": ["application/json"], >+ "produces": ["application/json"], >+ "responses": { >+ "201": { >+ "description": "A successfully created bundle link", >+ "schema": { >+ "items": { >+ "$ref": "../definitions.json#/item" >+ } >+ } >+ }, >+ "400": { >+ "description": "Bad parameter", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "401": { >+ "description": "Authentication required", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "403": { >+ "description": "Access forbidden", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "404": { >+ "description": "Resource not found", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "409": { >+ "description": "Conflict in creating resource", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "500": { >+ "description": "Internal server error", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "503": { >+ "description": "Under maintenance", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ } >+ }, >+ "x-koha-authorization": { >+ "permissions": { >+ "catalogue": 1 >+ } >+ } >+ } >+ }, >+ "/items/{item_id}/bundled_items/item/{bundled_item_id}": { >+ "delete": { >+ "x-mojo-to": "Items#remove_from_bundle", >+ "operationId": "removeFromBundle", >+ "tags": ["items"], >+ "summary": "Remove item from bundle", >+ "parameters": [{ >+ "$ref": "../parameters.json#/item_id_pp" >+ }, >+ { >+ "name": "bundled_item_id", >+ "in": "path", >+ "description": "Internal identifier for the bundled item", >+ "required": true, >+ "type": "string" >+ } >+ ], >+ "consumes": ["application/json"], >+ "produces": ["application/json"], >+ "responses": { >+ "204": { >+ "description": "Bundle link deleted" >+ }, >+ "400": { >+ "description": "Bad parameter", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "401": { >+ "description": "Authentication required", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "403": { >+ "description": "Access forbidden", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "404": { >+ "description": "Resource not found", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "500": { >+ "description": "Internal server error", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ }, >+ "503": { >+ "description": "Under maintenance", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ } >+ }, >+ "x-koha-authorization": { >+ "permissions": { >+ "catalogue": 1 >+ } >+ } >+ } >+ }, > "/items/{item_id}/pickup_locations": { > "get": { > "x-mojo-to": "Items#pickup_locations", >diff --git a/catalogue/detail.pl b/catalogue/detail.pl >index 0cb204ced9d..ce594fbb512 100755 >--- a/catalogue/detail.pl >+++ b/catalogue/detail.pl >@@ -203,6 +203,11 @@ if (@hostitems){ > > my $dat = &GetBiblioData($biblionumber); > >+#is biblio a collection >+my $leader = $record->leader(); >+$dat->{collection} = ( substr($leader,7,1) eq 'c' ) ? 1 : 0; >+ >+ > #coping with subscriptions > my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber); > my @subscriptions = SearchSubscriptions({ biblionumber => $biblionumber, orderby => 'title' }); >@@ -395,6 +400,15 @@ foreach my $item (@items) { > $item->{cover_images} = $item_object->cover_images; > } > >+ if ($item_object->is_bundle) { >+ $itemfields{bundles} = 1; >+ $item->{is_bundle} = 1; >+ } >+ >+ if ($item_object->in_bundle) { >+ $item->{bundle_host} = $item_object->bundle_host; >+ } >+ > if ($currentbranch and C4::Context->preference('SeparateHoldings')) { > if ($itembranchcode and $itembranchcode eq $currentbranch) { > push @itemloop, $item; >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >index 9d8f5e8069c..8ee0df0a57d 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -2293,6 +2293,10 @@ td { > display: block; > } > >+.bundled { >+ display: block; >+} >+ > .datedue { > color: #999; > display: block; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref >index 5ffd9a22f3e..e0029bab616 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref >@@ -158,6 +158,10 @@ Cataloging: > - and record's last modifier name in MARC subfield > - pref: MarcFieldForModifierName > - ". <br/><strong>NOTE:</strong> Use a dollar sign between field and subfield like 123$a." >+ - >+ - Use the NOT_LOAN authorised value >+ - pref: BundleNotLoanValue >+ - to represent 'added to bundle' when an item is attached to bundle. > Display: > - > - 'Separate main entry and subdivisions with ' >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >index 7c2b961d6ca..5c556335d5c 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >@@ -347,7 +347,7 @@ > </thead> > <tbody> > [% FOREACH item IN items %] >- <tr> >+ <tr id="item_[% item.itemnumber | html %]" data-itemnumber="[% item.itemnumber | html %]"> > [% IF (StaffDetailItemSelection) %] > <td style="text-align:center;vertical-align:middle"> > <input type="checkbox" value="[% item.itemnumber | html %]" name="itemnumber" /> >@@ -509,6 +509,10 @@ Note that permanent location is a code, and location may be an authval. > <span class="restricted">([% item.restrictedvalue | html %])</span> > [% END %] > >+ [% IF ( item.bundle_host ) %] >+ <span class="bundled">In bundle: [% INCLUDE 'biblio-title.inc' biblio = item.bundle_host.biblio link = 1 %]</span> >+ [% END %] >+ > </td> > <td class="datelastseen" data-order="[% item.datelastseen | html %]">[% item.datelastseen | $KohaDates %]</td> > <td class="dateaccessioned" data-order="[% item.dateaccessioned | html %]">[% item.dateaccessioned | $KohaDates %]</td> >@@ -593,6 +597,9 @@ Note that permanent location is a code, and location may be an authval. > <a class="btn btn-default btn-xs" href="/cgi-bin/koha/cataloguing/additem.pl?op=edititem&biblionumber=[% item.biblionumber | html %]&itemnumber=[% item.itemnumber | html %]#edititem"><i class="fa fa-pencil"></i> Edit</a> > [% END %] > [% END %] >+ [% IF collection %] >+ <button class="btn btn-default btn-xs details-control"><i class="fa fa-folder"></i> Manage bundle</button> >+ [% END %] > </td> > [% END %] > </tr> >@@ -1020,6 +1027,35 @@ Note that permanent location is a code, and location may be an authval. > > [% END %] > >+ <div class="modal" id="bundleItemsModal" tabindex="-1" role="dialog" aria-labelledby="bundleItemsLabel"> >+ <form id="bundleItemsForm" action=""> >+ <div class="modal-dialog" role="document"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">Ã</button> >+ <h3 id="bundleItemsLabel">Add to bundle</h3> >+ </div> >+ <div class="modal-body"> >+ <div id="result"></div> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label class="required" for="external_id">Item barcode: </label> >+ <input type="text" id="external_id" name="external_id" required="required"> >+ <span class="required">Required</span> >+ </li> >+ </ol> >+ </fieldset> >+ </div> >+ <div class="modal-footer"> >+ <button type="submit" class="btn btn-default">Submit</button> >+ <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button> >+ </div> >+ </div> >+ </div> >+ </form> >+ </div> >+ > [% MACRO jsinclude BLOCK %] > [% INCLUDE 'catalog-strings.inc' %] > [% Asset.js("js/catalog.js") | $raw %] >@@ -1315,6 +1351,7 @@ Note that permanent location is a code, and location may be an authval. > [% INCLUDE 'datatables.inc' %] > [% Asset.js("lib/jquery/plugins/jquery.dataTables.columnFilter.js") | $raw %] > [% INCLUDE 'columns_settings.inc' %] >+ [% INCLUDE 'js-date-format.inc' %] > [% Asset.js("js/browser.js") | $raw %] > [% Asset.js("js/table_filters.js") | $raw %] > <script> >@@ -1323,6 +1360,118 @@ Note that permanent location is a code, and location may be an authval. > browser.show(); > > $(document).ready(function() { >+ >+ function createChild ( row, itemnumber ) { >+ >+ // Toolbar >+ var bundle_toolbar = $('<div id="toolbar" class="btn-toolbar"><a class="btn btn-default" data-toggle="modal" data-target="#bundleItemsModal" data-item="' + itemnumber + '"><i class="fa fa-plus"></i> Add to bundle</a></div>'); >+ >+ // This is the table we'll convert into a DataTable >+ var bundles_table = $('<table class="display tbundle" data-itemnumber="'+itemnumber+'" id="bundle_table_'+itemnumber+'" width="100%"/>'); >+ >+ // Display it the child row >+ row.child( bundle_toolbar.add(bundles_table) ).show(); >+ >+ // Initialise as a DataTable >+ var bundle_table_url = "/api/v1/items/" + itemnumber + "/bundled_items?"; >+ var bundle_table = bundles_table.api({ >+ "ajax": { >+ "url": bundle_table_url >+ }, >+ "header_filter": false, >+ "embed": [ >+ "biblio" >+ ], >+ "order": [[ 1, "asc" ]], >+ "columns": [ >+ { >+ "data": "biblio.title:biblio.medium", >+ "title": "Title", >+ "searchable": true, >+ "orderable": true, >+ "render": function(data, type, row, meta) { >+ var title = ""; >+ if ( row.biblio.title ) { >+ title = title.concat('<span class="biblio-title">',row.biblio.title,'</span>'); >+ } >+ if ( row.biblio.medium ) { >+ title = title.concat('<span class="biblio-medium">',row.biblio.medium,'</span>'); >+ } >+ return title; >+ } >+ }, >+ { >+ "data": "biblio.author", >+ "title": "Author", >+ "searchable": true, >+ "orderable": true, >+ }, >+ { >+ "data": "collection_code", >+ "title": "Collection code", >+ "searchable": true, >+ "orderable": true, >+ }, >+ { >+ "data": "item_type", >+ "title": "Item Type", >+ "searchable": false, >+ "orderable": true, >+ }, >+ { >+ "data": "callnumber", >+ "title": "Callnumber", >+ "searchable": true, >+ "orderable": true, >+ }, >+ { >+ "data": "external_id", >+ "title": "Barcode", >+ "searchable": true, >+ "orderable": true, >+ }, >+ { >+ "data": "lost_status:last_seen_date", >+ "title": "Status", >+ "searchable": false, >+ "orderable": true, >+ "render": function(data, type, row, meta) { >+ if ( row.lost_status ) { >+ return "Lost: " + row.lost_status; >+ } >+ return "Available"; >+ } >+ }, >+ { >+ "data": function( row, type, val, meta ) { >+ var result = '<button class="btn btn-default btn-xs remove" role="button" data-itemnumber="'+row.item_id+'"><i class="fa fa-minus" aria-hidden="true"></i> '+_("Remove")+'</button>\n'; >+ return result; >+ }, >+ "title": "Actions", >+ "searchable": false, >+ "orderable": false >+ } >+ ] >+ }, [], 1); >+ >+ $(".tbundle").on("click", ".remove", function(){ >+ var bundle_table = $(this).closest('table'); >+ var host_itemnumber = bundle_table.data('itemnumber'); >+ var component_itemnumber = $(this).data('itemnumber'); >+ var unlink_item_url = "/api/v1/items/" + host_itemnumber + "/bundled_items/item/" + component_itemnumber; >+ $.ajax({ >+ type: "DELETE", >+ url: unlink_item_url, >+ success: function(){ >+ bundle_table.DataTable({ 'retrieve': true }).draw(false); >+ } >+ }); >+ }); >+ >+ return; >+ } >+ >+ > var ids = ['holdings_table', 'otherholdings_table']; > var columns_settings = [ [% TablesSettings.GetColumns('catalogue', 'detail','holdings_table','json') | $raw %], [% TablesSettings.GetColumns('catalogue', 'detail','otherholdings_table','json') | $raw %] ]; > var has_images = ["[% itemloop_has_images | html %]", "[% otheritemloop_has_images | html %]"]; >@@ -1339,6 +1488,26 @@ Note that permanent location is a code, and location may be an authval. > "sDom": 'C<"top pager"ilpfB><"#filter_c">tr<"bottom pager"ip>', > }; > var table = KohaTable(id, dt_parameters, columns_settings[i], 'with_filters'); >+ >+ // Add event listener for opening and closing details >+ $('#' + id + ' tbody').on('click', 'button.details-control', function () { >+ var tr = $(this).closest('tr'); >+ var dTable = $(this).closest('table').DataTable({ 'retrieve': true }); >+ >+ var itemnumber = tr.data('itemnumber'); >+ var row = dTable.row( tr ); >+ >+ if ( row.child.isShown() ) { >+ // This row is already open - close it >+ row.child.hide(); >+ tr.removeClass('shown'); >+ } >+ else { >+ // Open this row >+ createChild(row, itemnumber); >+ tr.addClass('shown'); >+ } >+ } ); > } > > [% IF Koha.Preference('AcquisitionDetails') %] >@@ -1360,6 +1529,63 @@ Note that permanent location is a code, and location may be an authval. > "sPaginationType": "full" > })); > [% END %] >+ >+ var bundle_changed; >+ var bundle_form_active; >+ $("#bundleItemsModal").on("shown.bs.modal", function(e){ >+ var button = $(e.relatedTarget); >+ var item_id = button.data('item'); >+ $("#result").replaceWith('<div id="result"></div>'); >+ $("#bundleItemsForm").attr('action', '/api/v1/items/' + item_id + '/bundled_items/item'); >+ $("#external_id").focus(); >+ bundle_changed = 0; >+ bundle_form_active = item_id; >+ }); >+ >+ $("#bundleItemsForm").submit(function(event) { >+ >+ /* stop form from submitting normally */ >+ event.preventDefault(); >+ >+ /* get the action attribute from the <form action=""> element */ >+ var $form = $(this), >+ url = $form.attr('action'); >+ >+ /* Send the data using post with external_id */ >+ var posting = $.post(url, JSON.stringify({ >+ external_id: $('#external_id').val() >+ }), null, "json"); >+ >+ /* Report the results */ >+ posting.done(function(data) { >+ var barcode = $('#external_id').val(); >+ $('#result').replaceWith('<div id="result" class="alert alert-success">Success: Added '+barcode+'</div>'); >+ $('#external_id').val('').focus(); >+ bundle_changed = 1; >+ }); >+ posting.fail(function(data) { >+ var barcode = $('#external_id').val(); >+ if ( data.status === 409 ) { >+ var response = data.responseJSON; >+ if ( response.key === "PRIMARY" ) { >+ $('#result').replaceWith('<div id="result" class="alert alert-warning">Warning: Item '+barcode+' already attached</div>'); >+ } else { >+ $('#result').replaceWith('<div id="result" class="alert alert-danger">Failure: Item '+barcode+' belongs to another bundle</div>'); >+ } >+ } else { >+ $('#result').replaceWith('<div id="result" class="alert alert-danger">Failure</div>'); >+ } >+ $('#external_id').val('').focus(); >+ }); >+ }); >+ >+ $("#bundleItemsModal").on("hidden.bs.modal", function(e){ >+ if ( bundle_changed ) { >+ $('#bundle_table_'+bundle_form_active).DataTable({ 'retrieve': true }).ajax.reload(); >+ } >+ bundle_form_active = 0; >+ bundle_changed = 0; >+ }); > }); > > $(document).ready(function() { >-- >2.20.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 28854
:
124319
|
124498
|
124616
|
125200
|
125201
|
125202
|
125203
|
125204
|
125205
|
125206
|
126103
|
126104
|
126105
|
126106
|
126107
|
126108
|
126109
|
126110
|
126148
|
126149
|
126150
|
126151
|
126152
|
126153
|
126154
|
126155
|
126166
|
126167
|
126168
|
126169
|
126170
|
126171
|
126172
|
126173
|
127498
|
127499
|
127500
|
127501
|
127502
|
127503
|
127504
|
127505
|
127659
|
127660
|
127661
|
127662
|
127663
|
127664
|
127665
|
127666
|
127829
|
127830
|
127831
|
127832
|
127833
|
127834
|
127835
|
127836
|
127837
|
127838
|
130313
|
130314
|
130315
|
130316
|
130317
|
130318
|
130319
|
130320
|
130321
|
130322
|
130323
|
130324
|
130325
|
130326
|
130327
|
130328
|
130329
|
130330
|
130331
|
130332
|
130456
|
132703
|
132704
|
132705
|
132706
|
132707
|
132708
|
132709
|
132710
|
132711
|
132712
|
132713
|
132714
|
132715
|
132716
|
132717
|
132718
|
132719
|
132720
|
132721
|
132722
|
132723
|
135019
|
135020
|
135021
|
135022
|
135023
|
135024
|
135025
|
135026
|
135027
|
135028
|
135029
|
135030
|
135031
|
135032
|
135033
|
135034
|
135035
|
135036
|
135037
|
136226
|
136227
|
136228
|
136229
|
136230
|
136231
|
136232
|
136233
|
136234
|
136235
|
136236
|
136237
|
136238
|
136239
|
136240
|
136241
|
136242
|
136243
|
136244
|
136249
|
136253
|
136254
|
136264
|
136265
|
136266
|
136267
|
136268
|
136269
|
136270
|
136271
|
136272
|
136273
|
136274
|
136275
|
136276
|
136277
|
136278
|
136279
|
136280
|
136281
|
136282
|
136283
|
136284
|
136285
|
136287
|
136340
|
136341
|
136342
|
136343
|
136344
|
136345
|
136346
|
136347
|
136348
|
136349
|
136350
|
136351
|
136352
|
136353
|
136354
|
136355
|
136356
|
136357
|
136358
|
136359
|
136360
|
136361
|
136362
|
136363
|
136364
|
136365
|
136366
|
136367
|
136368
|
136369
|
136370
|
136371
|
136498
|
136529
|
136530
|
136613
|
136614
|
136615
|
136616
|
136617
|
136618
|
136619
|
136620
|
136621
|
136622
|
136623
|
136624
|
136625
|
136626
|
136627
|
136628
|
136629
|
136630
|
136631
|
136632
|
136633
|
136634
|
136635
|
136636
|
136637
|
136638
|
136639
|
136640
|
136641
|
136642
|
136643
|
136644
|
136645
|
136646
|
136647
|
136648
|
136649
|
136650
|
136651
|
136652
|
136654
|
136655
|
136806
|
136809
|
136811
|
136814
|
136817
|
136821
|
136824
|
136826
|
136829
|
136832
|
136835
|
136838
|
136841
|
136843
|
136846
|
136849
|
136850
|
136851
|
136852
|
136853
|
136854
|
136855
|
136856
|
136857
|
136858
|
136859
|
136860
|
136861
|
136862
|
136863
|
136864
|
136865
|
136866
|
136867
|
136868
|
136869
|
136870
|
136871
|
136872
|
136873
|
136874
|
136875
|
136876
|
136877
|
136905
|
136906
|
136907
|
136908
|
136909
|
136910
|
136911
|
136912
|
136913
|
136914
|
136915
|
136916
|
136917
|
136918
|
136919
|
136920
|
136921
|
136922
|
136923
|
136924
|
136925
|
136926
|
136927
|
136928
|
136929
|
136930
|
136931
|
136932
|
136933
|
136934
|
136935
|
136936
|
136937
|
136938
|
136939
|
136940
|
136941
|
136942
|
136943
|
136944
|
136945
|
136946
|
136947
|
136949
|
136978
|
136979
|
136980
|
136981
|
136982
|
136985
|
136988
|
136989
|
136990
|
136991
|
136992
|
136993
|
136994
|
136995
|
136996
|
136997
|
136998
|
136999
|
137000
|
137001
|
137002
|
137003
|
137004
|
137005
|
137006
|
137007
|
137008
|
137009
|
137010
|
137011
|
137012
|
137013
|
137014
|
137015
|
137016
|
137017
|
137018
|
137019
|
137020
|
137021
|
137022
|
137461
|
137462
|
137463
|
137464
|
137465
|
137466
|
137467
|
137468
|
137469
|
137470
|
137471
|
137472
|
137473
|
137474
|
137475
|
137476
|
137477
|
137478
|
137479
|
137480
|
137481
|
137482
|
137483
|
137484
|
137485
|
137486
|
137487
|
137488
|
137489
|
137490
|
137491
|
137492
|
137493
|
137494
|
137495
|
137496
|
137497
|
137498
|
137499
|
137500
|
137501
|
137503
|
137506
|
137524
|
137525
|
137527
|
137528
|
137529
|
137530
|
137531
|
137532
|
137533
|
137534
|
137535
|
137536
|
137537
|
137538
|
137540
|
137541
|
137542
|
137543
|
137544
|
137545
|
137546
|
137547
|
137548
|
137549
|
137550
|
137551
|
137552
|
137553
|
137554
|
137555
|
137556
|
137557
|
137558
|
137559
|
137560
|
137561
|
137562
|
137563
|
137564
|
137565
|
137566
|
137567
|
137568
|
137569
|
137570
|
137571
|
137684
|
137685
|
139347