Bugzilla – Attachment 175371 Details for
Bug 38666
Closed stack requests
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 38666: Closed stack requests
Bug-38666-Closed-stack-requests.patch (text/plain), 128.51 KB, created by
Julian Maurice
on 2024-12-11 13:06:39 UTC
(
hide
)
Description:
Bug 38666: Closed stack requests
Filename:
MIME Type:
Creator:
Julian Maurice
Created:
2024-12-11 13:06:39 UTC
Size:
128.51 KB
patch
obsolete
>From 2125ee32a92c344dcfa7bd644e2d8d01cc448f76 Mon Sep 17 00:00:00 2001 >From: Julian Maurice <julian.maurice@biblibre.com> >Date: Thu, 18 Jan 2024 16:26:22 +0100 >Subject: [PATCH] Bug 38666: Closed stack requests >MIME-Version: 1.0 >Content-Type: text/plain; charset=UTF-8 >Content-Transfer-Encoding: 8bit > >A closed stack request is a special kind of hold that can only be placed >on a closed stack item. >A closed stack item is an item that is not available publicly in the >library but can be made available on demand. When a request is made, a >librarian will print a slip then will go retrieve the document and place >the slip in place of the document. > >Internally, a closed stack item is an item that is flagged as "closed >stack" (new column items.is_closed_stack). The flag can be modified on >the biblio "Items" tab. The new column can also be mapped to an item >MARC subfield. > >An item is available for a closed stack request only if: >- it is flagged as "closed stack" >- there is no holds on it >- it is not checked out >- it is not in transfer > >If any of the above conditions is false, it is not available for a >closed stack request but it can be available for a regular hold. >In particular this means that once a closed stack request has been made >on a particular item, this item can now be reserved using regular holds. > >Closed stack requests behave like regular holds, with the following >exceptions: >- They can only be placed on a specific item (item-level hold is forced) >- They can be placed on "not for loan" items >- They bypass the "onshelfholds" circulation rule >- They cannot be cancelled from OPAC > >There is a new page "Closed stack requests" accessible from the >circulation menu that lists all pending closed stack requests. >From there it is possible to cancel requests or to print a slip (letter >code CLOSED_STACK_SLIP in 'reserves' module) >Once the slip is printed, closed stack requests move to another tab in >the same page. > >The patch also adds a template plugin 'GD.Barcode' that allows to >include a barcode image inside a letter. Useful to be able to scan the >printed closed stack request slip. >Example usage: > >[% USE GD.Barcode %] ><img src="[% GD.Barcode.create_as_data_url('Code39', hold.item.barcode) | html %]"> > >Test plan: > 1. Apply patch, run updatedatabase.pl and update_dbix_class_files.pl, > and restart koha > 2. Create a biblio with two items. > 3. On the biblio detail page, click on the "Items" tab > 4. For one of the two items, change the "Closed stack" status from No > to Yes > 5. Notice there is now a "closed stack requests" tab on the left, and a > "closed stack request" button in the toolbar > 6. Click on "Place hold" and select a patron. Notice there is only one > item displayed in the table (the non-"closed stack" item) > 7. Go back to the biblio detail view and click on the "Closed stack > request" button. Select a patron. Notice there is only one item > displayed in the table (the "closed stack" item). > 8. Select this item and click on "Place hold". You should be redirected > to the "Closed stack requests" tab where you can see the request > that you just made > 9. Try to place a regular hold again. This time you should see both > items in the table. >10. Go to Tools » Notices and slips >11. Create a new notice in the Holds module with code CLOSED_STACK_SLIP. > Add content in the "Print" message. >12. Go to Circulation » Closed stack requests >13. You have two tabs: "Pending" and "Slip printed", you should have one > request under the pending tab, and zero under the other one. >14. Click on the "Print closed stack request slip" button, in the > "pending" table. A printer dialog should open, close it. > The "Closed stack requests" should have been automatically > refreshed, showing you an empty "pending" tab, while the "Slip > printed" tab now contains the previously pending request. >15. Cancel the closed stack request. >16. Login to OPAC, find your biblio record and place a closed stack > request. Like from the staff interface, only the closed stack item > should be in the items table, and it should not be possible to place > a biblio-level request >17. In your account page, verify that the closed stack request appear > under the new tab "Closed stack requests" and that there is no > "Cancel" button >18. Go back to the staff interface main page. You should see an alert at > the bottom: "Pending closed stack requests: 1". Clicking on it > should redirect you to the closed stack requests page seen earlier > (step 12) >19. Set the closed stack item as "notforloan", either by editing the > item or its item type. Verify that you can still do closed stack > requests >20. Change circulation rules to forbid on-shelf holds on the closed > stack item and verify that you can still do closed stack requests. > >Sponsored-by: Université de Lyon 3 >--- > C4/Reserves.pm | 69 ++-- > Koha/Biblio.pm | 34 ++ > Koha/CirculationRules.pm | 2 + > Koha/Hold.pm | 6 +- > Koha/Holds.pm | 78 +++- > Koha/Item.pm | 31 ++ > Koha/Items.pm | 32 +- > Koha/Template/Plugin/Biblio.pm | 2 +- > Koha/Template/Plugin/GD/Barcode.pm | 86 +++++ > admin/columns_settings.yml | 20 + > api/v1/swagger/definitions/item.yaml | 4 + > catalogue/detail.pl | 5 +- > catalogue/updateitem.pl | 4 + > circ/circulation.pl | 5 +- > circ/closed-stack-requests.pl | 120 ++++++ > .../atomicupdate/closed-stack-requests.pl | 47 +++ > installer/data/mysql/kohastructure.sql | 4 + > .../prog/en/includes/biblio-view-menu.inc | 11 +- > .../prog/en/includes/cat-toolbar.inc | 4 + > .../prog/en/includes/circ-nav.inc | 3 + > .../prog/en/includes/patron-detail-tabs.inc | 46 +++ > .../prog/en/modules/catalogue/detail.tt | 13 + > .../prog/en/modules/catalogue/moredetail.tt | 27 ++ > .../prog/en/modules/circ/circulation-home.tt | 3 + > .../en/modules/circ/closed-stack-requests.tt | 327 ++++++++++++++++ > .../prog/en/modules/intranet-main.tt | 9 +- > .../prog/en/modules/reserve/request.tt | 356 ++++++++++-------- > koha-tmpl/intranet-tmpl/prog/js/holds.js | 175 +++++++++ > .../bootstrap/en/includes/holds-table.inc | 3 +- > .../en/includes/opac-detail-sidebar.inc | 4 + > .../bootstrap/en/modules/opac-reserve.tt | 35 +- > .../bootstrap/en/modules/opac-user.tt | 16 +- > mainpage.pl | 5 + > members/moremember.pl | 32 +- > opac/opac-reserve.pl | 33 +- > opac/opac-user.pl | 10 +- > reserve/placerequest.pl | 6 +- > reserve/request.pl | 19 +- > svc/holds | 10 + > 39 files changed, 1444 insertions(+), 252 deletions(-) > create mode 100644 Koha/Template/Plugin/GD/Barcode.pm > create mode 100755 circ/closed-stack-requests.pl > create mode 100755 installer/data/mysql/atomicupdate/closed-stack-requests.pl > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/circ/closed-stack-requests.tt > >diff --git a/C4/Reserves.pm b/C4/Reserves.pm >index 046bfd0235..5cc6ba2f3f 100644 >--- a/C4/Reserves.pm >+++ b/C4/Reserves.pm >@@ -196,11 +196,11 @@ sub AddReserve { > > $resdate ||= dt_from_string; > >+ my $item = $checkitem ? Koha::Items->find($checkitem) : undef; >+ > # if we have an item selectionned, and the pickup branch is the same as the holdingbranch > # of the document, we force the value $priority and $found . >- if ( $checkitem and not C4::Context->preference('ReservesNeedReturns') ) { >- my $item = Koha::Items->find( $checkitem ); # FIXME Prevent bad calls >- >+ if ( $item and not C4::Context->preference('ReservesNeedReturns') ) { > if ( > # If item is already checked out, it cannot be set waiting > !$item->onloan >@@ -833,16 +833,17 @@ sub CheckReserves { > my @SkipHoldTrapOnNotForLoanValue = split( '\|', C4::Context->preference('SkipHoldTrapOnNotForLoanValue') ); > return if grep { $_ eq $item->notforloan } @SkipHoldTrapOnNotForLoanValue; > >- my $dont_trap = C4::Context->preference('TrapHoldsOnOrder') ? $item->notforloan > 0 : $item->notforloan; >- if ( !$dont_trap ) { >- my $item_type = $item->effective_itemtype; >- if ( $item_type ) { >- return if Koha::ItemTypes->find( $item_type )->notforloan; >+ unless ( $item->is_closed_stack ) { >+ my $dont_trap = C4::Context->preference('TrapHoldsOnOrder') ? $item->notforloan > 0 : $item->notforloan; >+ if ( !$dont_trap ) { >+ my $item_type = $item->effective_itemtype; >+ if ($item_type) { >+ return if Koha::ItemTypes->find($item_type)->notforloan; >+ } >+ } else { >+ return; > } > } >- else { >- return; >- } > > # Find this item in the reserves > my @reserves = _Findgroupreserve( $item->biblionumber, $item->itemnumber, $lookahead_days, $ignore_borrowers); >@@ -1316,10 +1317,15 @@ sub IsAvailableForItemLevelRequest { > unless defined $itemtype; > my $notforloan_per_itemtype = Koha::ItemTypes->find($itemtype)->notforloan; > >+ # Closed stack items can be requested even if marked as "not for loan" >+ unless ( $item->is_closed_stack ) { >+ return 0 >+ if $notforloan_per_itemtype >+ || $item->notforloan > 0; # item with negative or zero notforloan value is holdable >+ } >+ > return 0 if >- $notforloan_per_itemtype || > $item->itemlost || >- $item->notforloan > 0 || # item with negative or zero notforloan value is holdable > $item->withdrawn || > ($item->damaged && !C4::Context->preference('AllowHoldsOnDamagedItems')); > >@@ -1335,29 +1341,34 @@ sub IsAvailableForItemLevelRequest { > return 0 unless $branchitemrule->{hold_fulfillment_policy} ne 'holdgroup' || $home_library->validate_hold_sibling( {branchcode => $pickup_branchcode} ); > } > >- my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy( { item => $item, patron => $patron } ); >+ unless ( $item->is_closed_stack ) { >+ my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy( { item => $item, patron => $patron } ); > >- if ( $on_shelf_holds == 1 ) { >- return 1; >- } elsif ( $on_shelf_holds == 2 ) { >+ if ( $on_shelf_holds == 1 ) { >+ return 1; >+ } elsif ( $on_shelf_holds == 2 ) { > >- # These calculations work at the biblio level, and can be expensive >- # we use the in-memory cache to avoid calling once per item when looping items on a biblio >+ # These calculations work at the biblio level, and can be expensive >+ # we use the in-memory cache to avoid calling once per item when looping items on a biblio > >- my $memory_cache = Koha::Cache::Memory::Lite->get_instance(); >- my $cache_key = sprintf "ItemsAnyAvailableAndNotRestricted:%s:%s", $patron->id, $item->biblionumber; >+ my $memory_cache = Koha::Cache::Memory::Lite->get_instance(); >+ my $cache_key = sprintf "ItemsAnyAvailableAndNotRestricted:%s:%s", $patron->id, $item->biblionumber; > >- my $any_available = $memory_cache->get_from_cache($cache_key); >- return $any_available ? 0 : 1 if defined($any_available); >+ my $any_available = $memory_cache->get_from_cache($cache_key); >+ return $any_available ? 0 : 1 if defined($any_available); > >- $any_available = >- ItemsAnyAvailableAndNotRestricted( { biblionumber => $item->biblionumber, patron => $patron } ); >- $memory_cache->set_in_cache( $cache_key, $any_available ); >- return $any_available ? 0 : 1; >+ $any_available = >+ ItemsAnyAvailableAndNotRestricted( { biblionumber => $item->biblionumber, patron => $patron } ); >+ $memory_cache->set_in_cache( $cache_key, $any_available ); >+ return $any_available ? 0 : 1; > >- } else { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved) >- return $item->notforloan < 0 || $item->onloan || $item->holds->filter_by_found->count; >+ } else >+ { # on_shelf_holds == 0 "If any unavailable" (the description is rather cryptic and could still be improved) >+ return $item->notforloan < 0 || $item->onloan || $item->holds->filter_by_found->count; >+ } > } >+ >+ return 1; > } > > =head2 ItemsAnyAvailableAndNotRestricted >diff --git a/Koha/Biblio.pm b/Koha/Biblio.pm >index 927d7b898e..7658e82f93 100644 >--- a/Koha/Biblio.pm >+++ b/Koha/Biblio.pm >@@ -2295,6 +2295,40 @@ sub merge_with { > return \%results; > } > >+=head3 forced_hold_level >+ >+Returns forced hold level for a biblio >+ >+Returns 'item' if item-level hold should be forced, or undef >+ >+=cut >+ >+sub forced_hold_level { >+ my ($self) = @_; >+ >+ my $forced_hold_level; >+ >+ # If there is at least one closed stack item that is not checked out and >+ # not reserved, force item-level hold >+ my $available_closed_stack_items = $self->items->search( >+ { >+ is_closed_stack => 1, >+ onloan => undef, >+ 'reserves.reserve_id' => undef, >+ }, >+ { >+ join => 'reserves', >+ }, >+ ); >+ >+ if ( $available_closed_stack_items->count > 0 ) { >+ $forced_hold_level = 'item'; >+ } >+ >+ return $forced_hold_level; >+} >+ >+ > =head2 Internal methods > > =head3 type >diff --git a/Koha/CirculationRules.pm b/Koha/CirculationRules.pm >index d558c1e781..1c76eb0636 100644 >--- a/Koha/CirculationRules.pm >+++ b/Koha/CirculationRules.pm >@@ -563,6 +563,8 @@ sub get_opacitemholds_policy { > > return unless $item or $patron; > >+ return 'F' if $item->is_closed_stack; >+ > my $rule = Koha::CirculationRules->get_effective_rule( > { > categorycode => $patron->categorycode, >diff --git a/Koha/Hold.pm b/Koha/Hold.pm >index f5f8ada2b4..a5d8e3195f 100644 >--- a/Koha/Hold.pm >+++ b/Koha/Hold.pm >@@ -456,8 +456,10 @@ This is used from the OPAC. > sub is_cancelable_from_opac { > my ($self) = @_; > >- return 1 unless $self->is_found(); >- return 0; # if ->is_in_transit or if ->is_waiting or ->is_in_processing >+ return 0 if $self->is_found(); >+ return 0 if $self->item && $self->item->is_closed_stack; >+ >+ return 1; > } > > =head3 cancellation_requestable_from_opac >diff --git a/Koha/Holds.pm b/Koha/Holds.pm >index d4e0b5cdc6..1e687eb85c 100644 >--- a/Koha/Holds.pm >+++ b/Koha/Holds.pm >@@ -133,6 +133,7 @@ Items that are not: > widthdrawn > not for loan > not on loan >+ in closed stack > > =cut > >@@ -163,10 +164,11 @@ sub get_items_that_can_fill { > > return Koha::Items->search( > { >- -or => \@bibs_or_items, >- itemnumber => { -not_in => [ @branchtransfers, @waiting_holds ] }, >- onloan => undef, >- notforloan => 0, >+ -or => \@bibs_or_items, >+ itemnumber => { -not_in => [ @branchtransfers, @waiting_holds ] }, >+ onloan => undef, >+ notforloan => 0, >+ is_closed_stack => 0, > } > )->filter_by_for_hold(); > } >@@ -201,6 +203,74 @@ sub filter_out_has_cancellation_requests { > { join => 'cancellation_requests' } ); > } > >+=head3 filter_by_closed_stack_requests >+ >+Apply filter to keep only closed stack requests >+ >+ $holds = Koha::Holds->search($filter)->filter_by_closed_stack_requests(); >+ >+Returns a C<Koha::Holds> object >+ >+=cut >+ >+sub filter_by_closed_stack_requests { >+ my ($self) = @_; >+ >+ return $self->search( $self->_closed_stack_request_filter, { join => 'item' } ); >+} >+ >+=head3 filter_out_closed_stack_requests >+ >+Apply filter to exclude closed stack requests >+ >+ $holds = Koha::Holds->search($filter)->filter_out_closed_stack_requests(); >+ >+Returns a C<Koha::Holds> object >+ >+=cut >+ >+sub filter_out_closed_stack_requests { >+ my ($self) = @_; >+ >+ return $self->search( { -not_bool => $self->_closed_stack_request_filter }, { join => 'item' } ); >+} >+ >+sub _closed_stack_request_filter { >+ >+ # This query returns the top priority reserve's id for each item >+ my $reserve_id_subselect = q{ >+ select reserve_id from ( >+ select reserve_id, itemnumber, row_number() over (partition by itemnumber order by priority) rank >+ from reserves where itemnumber is not null >+ ) r >+ where rank = 1 >+ }; >+ >+ my %where = ( >+ 'me.reserve_id' => { -in => \$reserve_id_subselect }, >+ 'me.suspend' => 0, >+ 'me.found' => undef, >+ 'me.priority' => { '!=' => 0 }, >+ 'item.itemlost' => 0, >+ 'item.withdrawn' => 0, >+ 'item.onloan' => undef, >+ 'item.is_closed_stack' => 1, >+ 'item.itemnumber' => { >+ -not_in => \'SELECT itemnumber FROM branchtransfers WHERE datearrived IS NULL AND datecancelled IS NULL' >+ }, >+ ); >+ >+ if ( !C4::Context->preference('AllowHoldsOnDamagedItems') ) { >+ $where{'item.damaged'} = 0; >+ } >+ >+ if ( C4::Context->only_my_library() ) { >+ $where{'me.branchcode'} = C4::Context->userenv->{'branch'}; >+ } >+ >+ return \%where; >+} >+ > =head2 Internal methods > > =head3 _type >diff --git a/Koha/Item.pm b/Koha/Item.pm >index 07fc6ad5c8..8977ada6bc 100644 >--- a/Koha/Item.pm >+++ b/Koha/Item.pm >@@ -2524,6 +2524,37 @@ sub analytics_count { > return C4::Items::GetAnalyticsCount($self->itemnumber); > } > >+=head3 is_available_for_closed_stack_request >+ >+Returns 1 if item is available for a closed stack request, 0 otherwise >+ >+An item is available for a closed stack request if: >+ >+=over >+ >+=item * it is flagged as "closed stack" >+ >+=item * there is no holds on it >+ >+=item * it is not checked out >+ >+=item * it is not in transfer >+ >+=back >+ >+=cut >+ >+sub is_available_for_closed_stack_request { >+ my ($self) = @_; >+ >+ return 0 unless $self->is_closed_stack; >+ return 0 if $self->holds->count > 0; >+ return 0 if $self->checkout; >+ return 0 if $self->get_transfers->count > 0; >+ >+ return 1; >+} >+ > =head3 strings_map > > Returns a map of column name to string representations including the string, >diff --git a/Koha/Items.pm b/Koha/Items.pm >index 1cb1dce1b5..03858df54a 100644 >--- a/Koha/Items.pm >+++ b/Koha/Items.pm >@@ -103,8 +103,15 @@ sub filter_by_for_hold { > itemlost => 0, > withdrawn => 0, > notforloan => { '<=' => 0 }, # items with negative or zero notforloan value are holdable >- ( C4::Context->preference('AllowHoldsOnDamagedItems')? (): ( damaged => 0 ) ), >- ( C4::Context->only_my_library() ? ( homebranch => C4::Context::mybranch() ) : () ), >+ ( C4::Context->preference('AllowHoldsOnDamagedItems') ? () : ( damaged => 0 ) ), >+ ( C4::Context->only_my_library() ? ( homebranch => C4::Context::mybranch() ) : () ), >+ -or => [ >+ { is_closed_stack => 0 }, >+ { >+ is_closed_stack => 1, >+ 'reserves.reserve_id' => { '!=', undef }, >+ }, >+ ], > }; > > if ( C4::Context->preference("item-level_itypes") ) { >@@ -112,6 +119,9 @@ sub filter_by_for_hold { > { > %$params, > itype => { -not_in => \@hold_not_allowed_itypes }, >+ }, >+ { >+ join => 'reserves', > } > ); > } else { >@@ -121,7 +131,7 @@ sub filter_by_for_hold { > 'biblioitem.itemtype' => { -not_in => \@hold_not_allowed_itypes }, > }, > { >- join => 'biblioitem', >+ join => [ 'biblioitem', 'reserves' ], > } > ); > } >@@ -222,6 +232,22 @@ sub filter_by_bookable { > ); > } > >+=head3 filter_by_closed_stack >+ >+ my $filterd_items = $items->filter_by_closed_stack; >+ >+Returns a new resultset, containing only those items that are flagged as >+"closed stack". >+ >+=cut >+ >+sub filter_by_closed_stack { >+ my ($self) = @_; >+ >+ return $self->search( { is_closed_stack => 1 } ); >+} >+ >+ > =head3 move_to_biblio > > $items->move_to_biblio($to_biblio); >diff --git a/Koha/Template/Plugin/Biblio.pm b/Koha/Template/Plugin/Biblio.pm >index b16bb3e138..c1d4a9ce12 100644 >--- a/Koha/Template/Plugin/Biblio.pm >+++ b/Koha/Template/Plugin/Biblio.pm >@@ -32,7 +32,7 @@ sub HoldsCount { > > warn "HoldsCount is deprecated, you should use biblio.holds.count instead"; > >- my $holds = Koha::Holds->search( { biblionumber => $biblionumber } ); >+ my $holds = Koha::Holds->search( { 'me.biblionumber' => $biblionumber } )->filter_out_closed_stack_requests(); > > return $holds->count(); > } >diff --git a/Koha/Template/Plugin/GD/Barcode.pm b/Koha/Template/Plugin/GD/Barcode.pm >new file mode 100644 >index 0000000000..9090ef6321 >--- /dev/null >+++ b/Koha/Template/Plugin/GD/Barcode.pm >@@ -0,0 +1,86 @@ >+package Koha::Template::Plugin::GD::Barcode; >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+=head1 NAME >+ >+Koha::Template::Plugin::GD::Barcode >+ >+=head1 SYNOPSIS >+ >+ [% USE GD.Barcode %] >+ >+ <img src="[% GD.Barcode.create_as_data_url('Code39', item.barcode) | html %]"> >+ >+=head1 DESCRIPTION >+ >+This Template plugin allows to create barcode image as data URL >+ >+Mostly useful in notices and slips >+ >+=cut >+ >+use Modern::Perl; >+ >+use GD::Barcode; >+use MIME::Base64; >+ >+use base qw( Template::Plugin ); >+ >+=head1 METHODS >+ >+=head2 new >+ >+Creates a new instance of the plugin >+ >+=cut >+ >+sub new { >+ my ($class) = @_; >+ >+ my $self = {}; >+ >+ return bless $self, $class; >+} >+ >+=head2 create_as_data_url >+ >+Create a barcode image as a data URL >+ >+ [% GD.Barcode.create_as_data_url(type, barcode, args, plot_args) %] >+ >+C<type>, C<barcode> and C<args> are passed to C<GD::Barcode::new> >+ >+C<plot_args> is passed to C<GD::Barcode::plot> >+ >+See L<GD::Barcode/new> and L<GD::Barcode/plot> >+ >+It returns a data URL suited for use in an img src attribute >+ >+=cut >+ >+sub create_as_data_url { >+ my ( $self, $type, $barcode, $args, $plot_args ) = @_; >+ >+ $args //= {}; >+ $plot_args //= {}; >+ >+ my $data = GD::Barcode->new( $type, $barcode, $args )->plot(%$plot_args)->png; >+ >+ return 'data:image/png;base64,' . encode_base64($data); >+} >+ >+1; >diff --git a/admin/columns_settings.yml b/admin/columns_settings.yml >index b4f487d965..a32fe5d3fb 100644 >--- a/admin/columns_settings.yml >+++ b/admin/columns_settings.yml >@@ -1891,6 +1891,26 @@ modules: > - > columnname: print_slip > >+ closed-stack-requests: >+ default_display_length: 20 >+ default_sort_order: 0 >+ columns: >+ - columnname: patron >+ - columnname: title >+ - columnname: libraries >+ - columnname: barcodes >+ - columnname: call_numbers >+ - columnname: copy_numbers >+ - columnname: stocknumber >+ - columnname: enumeration >+ - columnname: itemtypes >+ - columnname: locations >+ - columnname: collection >+ - columnname: hold_date >+ - columnname: reserve_notes >+ - columnname: pickup_location >+ - columnname: action >+ > holdsratios: > holds-ratios: > default_display_length: 20 >diff --git a/api/v1/swagger/definitions/item.yaml b/api/v1/swagger/definitions/item.yaml >index b254eb2dc3..8fd81dee9c 100644 >--- a/api/v1/swagger/definitions/item.yaml >+++ b/api/v1/swagger/definitions/item.yaml >@@ -207,6 +207,10 @@ properties: > - string > - "null" > description: Itemtype defining the type for this item >+ is_closed_stack: >+ type: >+ - boolean >+ - "null" > effective_item_type_id: > type: > - string >diff --git a/catalogue/detail.pl b/catalogue/detail.pl >index 7af8010343..356e38c57f 100755 >--- a/catalogue/detail.pl >+++ b/catalogue/detail.pl >@@ -488,7 +488,10 @@ if (C4::Context->preference('TagsEnabled') and $tag_quantity = C4::Context->pref > > #we only need to pass the number of holds to the template > my $holds = $biblio->holds; >-$template->param( holdcount => $holds->count ); >+$template->param( >+ holdcount => $holds->filter_out_closed_stack_requests->count, >+ closed_stack_request_count => $holds->filter_by_closed_stack_requests->count, >+); > > # Check if there are any ILL requests connected to the biblio > my $illrequests = >diff --git a/catalogue/updateitem.pl b/catalogue/updateitem.pl >index cb92efbb16..209eff9744 100755 >--- a/catalogue/updateitem.pl >+++ b/catalogue/updateitem.pl >@@ -41,6 +41,8 @@ my $damaged=$cgi->param('damaged'); > my $exclude_from_local_holds_priority = $cgi->param('exclude_from_local_holds_priority'); > my $bookable = $cgi->param('bookable') // q{}; > >+my $is_closed_stack = $cgi->param('is_closed_stack'); >+ > my $confirm=$cgi->param('confirm'); > my $dbh = C4::Context->dbh; > >@@ -73,6 +75,8 @@ elsif ( $op eq "cud-set_public_note" ) { # i.e., itemnotes parameter passed from > $item->itemlost($itemlost); > } elsif ( $op eq "cud-set_withdrawn" && $withdrawn ne $item_data_hashref->{'withdrawn'}) { > $item->withdrawn($withdrawn); >+} elsif ( $op eq "cud-set_is_closed_stack" ) { >+ $item->is_closed_stack($is_closed_stack); > } elsif ( $op eq "cud-set_exclude_priority" && $exclude_from_local_holds_priority ne $item_data_hashref->{'exclude_from_local_holds_priority'}) { > $item->exclude_from_local_holds_priority($exclude_from_local_holds_priority); > $messages = "updated_exclude_from_local_holds_priority=$exclude_from_local_holds_priority&"; >diff --git a/circ/circulation.pl b/circ/circulation.pl >index 968470e6f1..7909a63c10 100755 >--- a/circ/circulation.pl >+++ b/circ/circulation.pl >@@ -594,8 +594,9 @@ if ($patron) { > my $holds = Koha::Holds->search( { borrowernumber => $borrowernumber } ); # FIXME must be Koha::Patron->holds > my $waiting_holds = $holds->waiting; > $template->param( >- holds_count => $holds->count(), >- WaitingHolds => $waiting_holds, >+ holds_count => $holds->filter_out_closed_stack_requests()->count(), >+ closed_stack_requests_count => $holds->filter_by_closed_stack_requests()->count(), >+ WaitingHolds => $waiting_holds, > ); > > if ( C4::Context->preference('UseRecalls') ) { >diff --git a/circ/closed-stack-requests.pl b/circ/closed-stack-requests.pl >new file mode 100755 >index 0000000000..1b52bc195e >--- /dev/null >+++ b/circ/closed-stack-requests.pl >@@ -0,0 +1,120 @@ >+#!/usr/bin/perl >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use CGI '-utf8'; >+ >+use C4::Output qw( output_html_with_http_headers ); >+use C4::Auth qw( get_template_and_user ); >+use Koha::Holds; >+use Koha::I18N; >+ >+my $cgi = CGI->new; >+ >+my $op = $cgi->param('op') || ''; >+if ( $op eq 'cud-print_slip' ) { >+ my $reserve_id = $cgi->param('reserve_id'); >+ >+ my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => 'circ/printslip.tt', >+ query => $cgi, >+ type => 'intranet', >+ flagsrequired => { circulate => 'circulate_remaining_permissions' }, >+ } >+ ); >+ >+ my $hold = Koha::Holds->find($reserve_id); >+ >+ my $letter = C4::Letters::GetPreparedLetter( >+ module => 'reserves', >+ letter_code => 'CLOSED_STACK_SLIP', >+ branchcode => $hold->branchcode, >+ lang => $hold->patron->lang, >+ message_transport_type => 'print', >+ tables => { >+ reserves => $hold->unblessed, >+ branches => $hold->branchcode, >+ borrowers => $hold->borrowernumber, >+ biblio => $hold->biblionumber, >+ biblioitems => $hold->biblionumber, >+ items => $hold->itemnumber, >+ }, >+ objects => { >+ hold => $hold, >+ } >+ ); >+ if ($letter) { >+ $template->param( >+ title => __('Closed stack request slip'), >+ slip => $letter->{content}, >+ plain => !$letter->{is_html}, >+ ); >+ $hold->closed_stack_request_slip_printed(1); >+ $hold->store(); >+ } >+ >+ output_html_with_http_headers $cgi, $cookie, $template->output; >+ exit; >+} >+ >+my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => "circ/closed-stack-requests.tt", >+ query => $cgi, >+ type => "intranet", >+ flagsrequired => { circulate => "circulate_remaining_permissions" }, >+ } >+); >+ >+my @messages; >+if ( $op eq 'cud-cancel_reserve' ) { >+ my $reserve_id = $cgi->param('reserve_id'); >+ my $hold = Koha::Holds->find($reserve_id); >+ if ($hold) { >+ my $cancellation_reason = $cgi->param('cancellation-reason'); >+ $hold->cancel( { cancellation_reason => $cancellation_reason } ); >+ push @messages, { type => 'message', code => 'hold_cancelled' }; >+ } >+} >+ >+my $holds = Koha::Holds->search()->filter_by_closed_stack_requests(); >+ >+my $branchcode = $cgi->param('branchcode') // C4::Context->userenv->{branch}; >+if ( $branchcode ne '' ) { >+ $holds = $holds->search( { branchcode => $branchcode } ); >+} >+$template->param( branchcode => $branchcode ); >+ >+my ( @pending_holds, @printed_slip_holds ); >+ >+foreach my $hold ( $holds->as_list ) { >+ if ( $hold->closed_stack_request_slip_printed ) { >+ push @printed_slip_holds, $hold; >+ } else { >+ push @pending_holds, $hold; >+ } >+} >+ >+$template->param( >+ pending_holds => \@pending_holds, >+ printed_slip_holds => \@printed_slip_holds, >+ messages => \@messages, >+); >+ >+output_html_with_http_headers $cgi, $cookie, $template->output; >diff --git a/installer/data/mysql/atomicupdate/closed-stack-requests.pl b/installer/data/mysql/atomicupdate/closed-stack-requests.pl >new file mode 100755 >index 0000000000..cb8a3aee10 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/closed-stack-requests.pl >@@ -0,0 +1,47 @@ >+use Modern::Perl; >+ >+return { >+ bug_number => "38666", >+ description => "Closed stack requests", >+ up => sub { >+ my ($args) = @_; >+ my ( $dbh, $out ) = @$args{qw(dbh out)}; >+ >+ unless ( column_exists( 'items', 'is_closed_stack' ) ) { >+ $dbh->do( >+ "ALTER TABLE `items` ADD COLUMN `is_closed_stack` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'if true, special rules apply for holds on this item' AFTER `itype`" >+ ); >+ say $out "Column items.is_closed_stack created"; >+ } else { >+ say $out "Column items.is_closed_stack not created (already exists)"; >+ } >+ >+ unless ( column_exists( 'deleteditems', 'is_closed_stack' ) ) { >+ $dbh->do( >+ "ALTER TABLE `deleteditems` ADD COLUMN `is_closed_stack` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'if true, special rules apply for holds on this item' AFTER `itype`" >+ ); >+ say $out "Column deleteditems.is_closed_stack created"; >+ } else { >+ say $out "Column deleteditems.is_closed_stack not created (already exists)"; >+ } >+ >+ unless ( column_exists( 'reserves', 'closed_stack_request_slip_printed' ) ) { >+ $dbh->do( >+ "ALTER TABLE `reserves` ADD COLUMN `closed_stack_request_slip_printed` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'boolean flag to track if closed stack request slip was printed (useful for display/filtering)' AFTER `non_priority`" >+ ); >+ say $out "Column reserves.closed_stack_request_slip_printed created"; >+ } else { >+ say $out "Column reserves.closed_stack_request_slip_printed not created (already exists)"; >+ } >+ >+ unless ( column_exists( 'old_reserves', 'closed_stack_request_slip_printed' ) ) { >+ $dbh->do( >+ "ALTER TABLE `old_reserves` ADD COLUMN `closed_stack_request_slip_printed` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'boolean flag to track if closed stack request slip was printed (useful for display/filtering)' AFTER `non_priority`" >+ ); >+ say $out "Column old_reserves.closed_stack_request_slip_printed created"; >+ } else { >+ say $out "Column old_reserves.closed_stack_request_slip_printed not created (already exists)"; >+ } >+ >+ }, >+}; >diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql >index d368659571..531fa92572 100644 >--- a/installer/data/mysql/kohastructure.sql >+++ b/installer/data/mysql/kohastructure.sql >@@ -2804,6 +2804,7 @@ CREATE TABLE `deleteditems` ( > `materials` mediumtext DEFAULT NULL COMMENT 'materials specified (MARC21 952$3)', > `uri` mediumtext DEFAULT NULL COMMENT 'URL for the item (MARC21 952$u)', > `itype` varchar(10) DEFAULT NULL COMMENT 'foreign key from the itemtypes table defining the type for this item (MARC21 952$y)', >+ `is_closed_stack` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'if true, special rules apply for holds on this item', > `more_subfields_xml` longtext DEFAULT NULL COMMENT 'additional 952 subfields in XML format', > `enumchron` mediumtext DEFAULT NULL COMMENT 'serial enumeration/chronology for the item (MARC21 952$h)', > `copynumber` varchar(32) DEFAULT NULL COMMENT 'copy number (MARC21 952$t)', >@@ -4064,6 +4065,7 @@ CREATE TABLE `items` ( > `materials` mediumtext DEFAULT NULL COMMENT 'materials specified (MARC21 952$3)', > `uri` mediumtext DEFAULT NULL COMMENT 'URL for the item (MARC21 952$u)', > `itype` varchar(10) DEFAULT NULL COMMENT 'foreign key from the itemtypes table defining the type for this item (MARC21 952$y)', >+ `is_closed_stack` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'if true, special rules apply for holds on this item', > `more_subfields_xml` longtext DEFAULT NULL COMMENT 'additional 952 subfields in XML format', > `enumchron` mediumtext DEFAULT NULL COMMENT 'serial enumeration/chronology for the item (MARC21 952$h)', > `copynumber` varchar(32) DEFAULT NULL COMMENT 'copy number (MARC21 952$t)', >@@ -5044,6 +5046,7 @@ CREATE TABLE `old_reserves` ( > `itemtype` varchar(10) DEFAULT NULL COMMENT 'If record level hold, the optional itemtype of the item the patron is requesting', > `item_level_hold` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Is the hold placed at item level', > `non_priority` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Is this a non priority hold', >+ `closed_stack_request_slip_printed` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'boolean flag to track if closed stack request slip was printed (useful for display/filtering)', > PRIMARY KEY (`reserve_id`), > KEY `old_reserves_borrowernumber` (`borrowernumber`), > KEY `old_reserves_biblionumber` (`biblionumber`), >@@ -5614,6 +5617,7 @@ CREATE TABLE `reserves` ( > `itemtype` varchar(10) DEFAULT NULL COMMENT 'If record level hold, the optional itemtype of the item the patron is requesting', > `item_level_hold` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Is the hold placed at item level', > `non_priority` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Is this a non priority hold', >+ `closed_stack_request_slip_printed` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'boolean flag to track if closed stack request slip was printed (useful for display/filtering)', > PRIMARY KEY (`reserve_id`), > KEY `priorityfoundidx` (`priority`,`found`), > KEY `borrowernumber` (`borrowernumber`), >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc >index 3e5625eff0..3c909fdbc7 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/biblio-view-menu.inc >@@ -61,14 +61,21 @@ > [% END %] > > [%- IF ( CAN_user_reserveforothers ) -%] >- [%- IF ( holdsview ) -%] >+ [%- IF ( holdsview && !closed_stack_request ) -%] > <li class="active"> > [%- ELSE -%] > <li> > [%- END -%] >- <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% biblio_object_id | url %]">Holds ([% biblio.holds.count | html %])</a> >+ <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% biblio_object_id | url %]">Holds ([% biblio.holds.filter_out_closed_stack_requests.count | html %])</a> > </li> > [%- END -%] >+ [%- IF ( holdsview && closed_stack_request ) -%] >+ <li class="active"> >+ [%- ELSE -%] >+ <li> >+ [%- END -%] >+ <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% biblio_object_id | url %]&closed_stack_request=1">Closed stack requests ([% biblio.holds.filter_by_closed_stack_requests.count | html %])</a> >+ </li> > > [%- IF ( EasyAnalyticalRecords ) -%] > [%- IF ( analyze ) -%] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc >index 36d924b952..9030fa2b04 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc >@@ -270,6 +270,10 @@ > <div class="btn-group"><a id="placehold" class="btn btn-default" href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% biblionumber | html %]" role="button"><i class="fa-solid fa-bookmark"></i> Place hold</a></div> > [% END %] > [% END %] >+ >+ [% IF items.filter_by_closed_stack.count %] >+ <div class="btn-group"><a class="btn btn-default" href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% biblionumber | html %]&closed_stack_request=1"><i class="fa-solid fa-bookmark"></i> Closed stack request</a></div> >+ [% END %] > [% END %] > > [% IF ( CAN_user_circulate_manage_bookings && biblio.items.filter_by_bookable.count ) %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/circ-nav.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/circ-nav.inc >index 86ca5d18ce..7ba80e8381 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/circ-nav.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/circ-nav.inc >@@ -54,6 +54,9 @@ > <li> > <a href="/cgi-bin/koha/circ/pendingreserves.pl">Holds to pull</a> > </li> >+ <li> >+ <a href="/cgi-bin/koha/circ/closed-stack-requests.pl">Closed stack requests</a> >+ </li> > <li> > <a href="/cgi-bin/koha/circ/waitingreserves.pl">Holds awaiting pickup</a> > </li> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-detail-tabs.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-detail-tabs.inc >index 68f5e54630..4cfed7ff39 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/patron-detail-tabs.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/patron-detail-tabs.inc >@@ -23,6 +23,11 @@ > [% WRAPPER tab_item tabname= "holds" %] > <span>Holds</span> ([% holds_count || 0 | html %]) > [% END %] >+ [% IF closed_stack_requests_count > 0 %] >+ [% WRAPPER tab_item tabname="closed-stack-requests" %] >+ <span>Closed stack requests</span> ([% closed_stack_requests_count | html %]) >+ [% END %] >+ [% END %] > [% WRAPPER tab_item tabname="bookings" %] > [% SET bookings_count = patron.bookings.filter_by_active.count %] > [% SET expired_bookings_count = patron.bookings.count - bookings_count %] >@@ -215,6 +220,47 @@ > [% END %] > [% END # /tab_panel#holds %] > >+ [% IF closed_stack_requests_count > 0 %] >+ [% WRAPPER tab_panel tabname="closed-stack-requests" %] >+ <div id="closed-stack-requests" role="tabpanel" class="tab-pane"> >+ <form action="/cgi-bin/koha/reserve/modrequest.pl" method="post"> >+ <input type="hidden" name="from" value="circ" /> >+ >+ <table id="closed-stack-requests-table"> >+ <thead> >+ <tr> >+ <th>Hold date</th> >+ <th>Title</th> >+ <th>Call number</th> >+ <th>Item type</th> >+ <th>Barcode</th> >+ <th>Expiration</th> >+ <th>Priority</th> >+ <th>Delete?</th> >+ <th>Status</th> >+ </tr> >+ </thead> >+ </table> >+ >+ <fieldset class="action"> >+ <input type="submit" class="cancel" name="submit" value="Cancel marked holds" /> >+ >+ [% SET hold_cancellation = AuthorisedValues.GetAuthValueDropbox('HOLD_CANCELLATION') %] >+ [% IF hold_cancellation.count %] >+ <label for="cancellation-reason">Cancellation reason:</label> >+ <select name="cancellation-reason"> >+ <option value="">No reason given</option> >+ [% FOREACH reason IN hold_cancellation %] >+ <option value="[% reason.authorised_value | html %]">[% reason.lib | html %]</option> >+ [% END %] >+ </select> >+ [% END %] >+ </fieldset> >+ </form> >+ </div> >+ [% END %] >+ [% END %] >+ > [% WRAPPER tab_panel tabname="bookings" %] > [% IF ( bookings_count ) %] > <fieldset class="action filters" style="cursor:pointer;"> >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 b82e221362..8e19963c45 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >@@ -198,6 +198,19 @@ > </span> > [% END %] > >+ [% IF ( closed_stack_request_count ) %] >+ <span class="results_summary"> >+ <span class="label">Closed stack requests:</span> >+ <span class="badge text-bg-info"> >+ [% IF CAN_user_reserveforothers_place_holds %] >+ <a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% biblionumber | uri %]&closed_stack_request=1">[% closed_stack_request_count | html %]</a> >+ [% ELSE %] >+ <span>[% closed_stack_request_count | html %]</span> >+ [% END %] >+ </span> >+ </span> >+ [% END %] >+ > [% IF illrequests.count %] > <span class="results_summary"> > <span class="label">ILL requests:</span> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt >index 2020f9fb9d..d2b85e9579 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt >@@ -315,6 +315,33 @@ > [% END %] > [% END %] > >+ <li> >+ <span class="label">Closed stack:</span> >+ [% IF ( CAN_user_circulate ) %] >+ <form action="updateitem.pl" method="post"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <input type="hidden" name="biblionumber" value="[% ITEM_DAT.biblionumber | html %]" /> >+ <input type="hidden" name="biblioitemnumber" value="[% ITEM_DAT.biblioitemnumber | html %]" /> >+ <input type="hidden" name="itemnumber" value="[% ITEM_DAT.itemnumber | html %]" /> >+ <select name="is_closed_stack" > >+ <option value="0">No</option> >+ [% IF ITEM_DAT.is_closed_stack %] >+ <option value="1" selected>Yes</option> >+ [% ELSE %] >+ <option value="1">Yes</option> >+ [% END %] >+ </select> >+ <input type="hidden" name="op" value="cud-set_is_closed_stack" /> >+ <input type="submit" name="submit" class="btn btn-primary btn-xs" value="Set status" /> >+ </form> >+ [% ELSE %] >+ [% IF ITEM_DAT.is_closed_stack %] >+ <span>Yes</span> >+ [% ELSE %] >+ <span>No</span> >+ [% END %] >+ [% END %] >+ </li> > </ol> <!-- /.bibliodetails --> > </div> <!-- /.rows --> > </div> <!-- /.listgroup --> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt >index 656dfd4d1f..0ca02770bf 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt >@@ -87,6 +87,9 @@ > <li> > <a class="circ-button" href="/cgi-bin/koha/circ/pendingreserves.pl"><i class="fa-solid fa-hand-back-fist"></i> Holds to pull</a> > </li> >+ <li> >+ <a class="circ-button" href="/cgi-bin/koha/circ/closed-stack-requests.pl"><i class="fa-solid fa-hand-back-fist"></i> Closed stack requests</a> >+ </li> > <li> > <a class="circ-button" href="/cgi-bin/koha/circ/waitingreserves.pl"><i class="fa-solid fa-calendar-days"></i> Holds awaiting pickup</a> > </li> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/closed-stack-requests.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/closed-stack-requests.tt >new file mode 100644 >index 0000000000..f7fd2999aa >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/closed-stack-requests.tt >@@ -0,0 +1,327 @@ >+[% USE raw %] >+[% USE Asset %] >+[% USE Branches %] >+[% USE Koha %] >+[% USE KohaDates %] >+[% USE TablesSettings %] >+[% USE AuthorisedValues %] >+[%- USE Branches -%] >+[%- USE ItemTypes -%] >+[% SET footerjs = 1 %] >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Closed stack requests › Circulation › Koha</title> >+[% INCLUDE 'doc-head-close.inc' %] >+</head> >+ >+<body id="circ_closed_stack_requests" class="circ"> >+[% WRAPPER 'header.inc' %] >+ [% INCLUDE 'circ-search.inc' %] >+[% END %] >+ >+[% WRAPPER 'sub-header.inc' %] >+ [% WRAPPER breadcrumbs %] >+ [% WRAPPER breadcrumb_item %] >+ <a href="/cgi-bin/koha/circ/circulation-home.pl">Circulation</a> >+ [% END %] >+ [% WRAPPER breadcrumb_item bc_active= 1 %] >+ <span>Closed stack requests</span> >+ [% END %] >+ [% END #/ WRAPPER breadcrumbs %] >+[% END #/ WRAPPER sub-header.inc %] >+ >+<div class="main container-fluid"> >+ <div class="row"> >+ <div class="col-md-10 order-md-2 order-sm-1"> >+ <main> >+ >+ [% FOR m IN messages %] >+ <div class="dialog [% m.type | html %]"> >+ [% SWITCH m.code %] >+ [% CASE 'hold_cancelled' %] >+ <span>The hold has been correctly cancelled.</span> >+ [% CASE %] >+ [% m.code | html %] >+ [% END %] >+ </div> >+ [% END %] >+ >+<h1>Closed stack requests</h1> >+ >+[% BLOCK holds_table %] >+ [% IF holds %] >+ <table id="[% id | html %]"> >+ <thead> >+ <tr> >+ <th>Patron</th> >+ <th class="anti-the">Title</th> >+ <th class="string-sort">Library</th> >+ <th>Barcode</th> >+ <th>Call number</th> >+ <th>Copy number</th> >+ <th>Stock number</th> >+ <th>Enumeration</th> >+ <th class="string-sort">Item type</th> >+ <th class="string-sort">Location</th> >+ <th class="string-sort">Collection</th> >+ <th>Hold date</th> >+ <th>Hold notes</th> >+ <th class="string-sort">Pickup location</th> >+ <th>Action</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH hold IN holds %] >+ <tr> >+ [% SET patron = hold.patron %] >+ [% SET item = hold.item %] >+ [% SET biblio = hold.biblio %] >+ >+ <td><a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% patron.borrowernumber | uri %]">[% patron.firstname | html %] [% patron.surname | html %]</a></td> >+ >+ <td> >+ <p> >+ [% INCLUDE 'biblio-title.inc' biblio=biblio link = 1 %] >+ </p> >+ [% IF ( biblio.author ) %]<p> by [% biblio.author | html %]</p>[% END %] >+ [% IF ( biblio.biblioitem.editionstatement ) %]<p>[% biblio.biblioitem.editionstatement | html %]</p>[% END %] >+ [% IF ( Koha.Preference('marcflavour') == 'MARC21' ) %] >+ [% IF ( biblio.copyrightdate ) %]<p>[% biblio.copyrightdate | html %]</p>[% END %] >+ [% ELSE %] >+ [% IF ( biblio.biblioitem.publicationyear ) %]<p>[% biblio.biblioitem.publicationyear | html %]</p>[% END %] >+ [% END %] >+ </td> >+ >+ <td>[% Branches.GetName(item.holdingbranch) | html %]</td> >+ >+ <td>[% item.barcode | html %]</td> >+ >+ <td>[% item.itemcallnumber | html %]</td> >+ >+ <td>[% item.copynumber | html %]</td> >+ >+ <td>[% item.stocknumber | html %]</td> >+ >+ <td>[% item.enumchron | html %]</td> >+ >+ <td>[% ItemTypes.GetDescription(item.itype) | html %]</td> >+ >+ <td>[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.location', authorised_value => item.location ) | html %] >+ >+ <td>[% AuthorisedValues.GetDescriptionByKohaField( kohafield => 'items.ccode', authorised_value => item.ccode ) | html %]</td> >+ >+ <td data-order="[% hold.reservedate | html %]"> >+ [% hold.reservedate | $KohaDates %] in [% Branches.GetName ( hold.branchcode ) | html %] >+ </td> >+ >+ <td>[% hold.reservenotes | html %]</td> >+ >+ <td>[% Branches.GetName ( hold.branchcode ) | html %]</td> >+ >+ <td> >+ <form method="post" id="print-closed-stack-request-slip-[% hold.reserve_id | html %]" target="_blank"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <input type="hidden" name="op" value="cud-print_slip"> >+ <input type="hidden" name="reserve_id" value="[% hold.reserve_id | html %]" /> >+ <button class="btn btn-default btn-sm print-closed-stack-request-slip" data-reserve-id="[% hold.reserve_id | html %]">Print closed stack request slip</button> >+ </form> >+ >+ <form name="cancelReserve" action="/cgi-bin/koha/circ/closed-stack-requests.pl" method="post"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <input type="hidden" name="op" value="cud-cancel_reserve" /> >+ <input type="hidden" name="reserve_id" value="[% hold.reserve_id | html %]" /> >+ >+ [% SET hold_cancellation = AuthorisedValues.GetAuthValueDropbox('HOLD_CANCELLATION') %] >+ [% IF hold_cancellation.count %] >+ <div class="form-group"> >+ <label for="cancellation-reason">Cancellation reason:</label> >+ <select class="cancellation-reason" name="cancellation-reason" id="cancellation-reason"> >+ <option value="">No reason given</option> >+ [% FOREACH reason IN hold_cancellation %] >+ <option value="[% reason.authorised_value | html %]">[% reason.lib | html %]</option> >+ [% END %] >+ </select> >+ </div> >+ [% END %] >+ >+ [% IF item.holdingbranch != item.homebranch %] >+ <button class="btn btn-default btn-sm" type="submit">Cancel hold and return to : [% Branches.GetName( item.homebranch ) | html %]</button> >+ [% ELSE %] >+ <button class="btn btn-default btn-sm" type="submit">Cancel hold</button> >+ [% END %] >+ </form> >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ >+ <tfoot> >+ <tr> >+ <td><input type="text" class="filter" data-column_num="0" placeholder="Patron name" style="width:95%"/></td> >+ <td><input type="text" class="filter" data-column_num="1" placeholder="Title" style="width:95%"/></td> >+ <td class="homebranchfilter"></td> >+ <td></td> >+ <td><input type="text" class="filter" data-column_num="4" placeholder="Call number" style="width:95%"/></td> >+ <td><input type="text" class="filter" data-column_num="5" placeholder="Copy number" style="width:95%"/></td> >+ <td><input type="text" class="filter" data-column_num="6" placeholder="Stocknumber" style="width:95%"/></td> >+ <td><input type="text" class="filter" data-column_num="7" placeholder="Available enumeration" style="width:95%"/></td> >+ <td class="itemtype-filter"></td> >+ <td class="locationfilter"></td> >+ <td></td> >+ <td></td> >+ <td></td> >+ <td class="pickup-location"></td> >+ <td></td> >+ </tr> >+ </tfoot> >+ </table> >+ [% ELSE %] >+ <strong>No items found.</strong> >+ [% END %] >+[% END %] >+ >+[% PROCESS 'html_helpers.inc' %] >+[% WRAPPER tabs %] >+ [% WRAPPER tabs_nav %] >+ [% WRAPPER tab_item tabname="pending" bt_active=1 %]<span>Pending</span> ([% pending_holds.size || 0 | html %])[% END %] >+ [% WRAPPER tab_item tabname="printed" %]<span>Slip printed</span> ([% printed_slip_holds.size || 0 | html %])[% END %] >+ [% END %] >+ [% WRAPPER tab_panels %] >+ [% WRAPPER tab_panel tabname="pending" bt_active=1 %] >+ <p>The following holds have not been filled. Please retrieve them and check them in.</p> >+ >+ [% INCLUDE holds_table id="holdst" holds=pending_holds %] >+ [% END %] >+ >+ [% WRAPPER tab_panel tabname="printed" %] >+ [% INCLUDE holds_table id="printed_slip_holds" holds=printed_slip_holds %] >+ [% END %] >+ [% END %] >+[% END %] >+ >+ >+ </main> >+ </div> <!-- /.col-sm-10.col-sm-push-2 --> >+ <div class="col-sm-2 col-sm-pull-10"> >+ <aside> >+ <form> >+ <fieldset class="brief"> >+ <h4>Filters</h4> >+ <label for="library">Library</label> >+ <select id="library" name="branchcode"> >+ <option value="">All</option> >+ [% FOREACH library IN Branches.all() %] >+ [% IF branchcode && branchcode == library.branchcode %] >+ <option value="[% library.branchcode | html %]" selected>[% library.branchname | html %]</option> >+ [% ELSE %] >+ <option value="[% library.branchcode | html %]">[% library.branchname | html %]</option> >+ [% END %] >+ [% END %] >+ </select> >+ </fieldset> >+ <fieldset class="action"> >+ <button class="btn btn-primary" type="submit">Filter</button> >+ </fieldset> >+ </form> >+ </aside> >+ [% IF Koha.Preference('CircSidebar') %] >+ <aside> >+ [% INCLUDE 'circ-nav.inc' %] >+ </aside> >+ [% END %] >+ </div> <!-- /.col-sm-2.col-sm-pull-10 --> >+ >+ </div> <!-- /.row --> >+ >+[% MACRO jsinclude BLOCK %] >+ [% INCLUDE 'calendar.inc' %] >+ [% INCLUDE 'datatables.inc' %] >+ [% INCLUDE 'columns_settings.inc' %] >+ <script> >+ function separateData ( ColumnData ){ >+ var cD = ColumnData; >+ var new_array = new Array(); >+ for ( j=0 ; j<cD.length ; j++ ) { >+ var split_array = cD[j].split(/\n/gi); >+ for ( k=0 ; k<split_array.length ; k++ ){ >+ var str = $.trim(split_array[k].replace(/[\n\r]/g, '')); >+ if ($.inArray(str, new_array) == -1 && str.length > 0 ) { >+ new_array.push(str); >+ } >+ } >+ } >+ new_array.sort(); >+ return new_array; >+ } >+ >+ function createSelect( data ) { >+ data = separateData(data); >+ var r='<select style="width:99%"><option value="">' + _("None") + '</option>', i, len=data.length; >+ var regex = /(<([^>]+)>)/ig; // Remove html tags >+ for ( i=0 ; i<len ; i++ ) { >+ var cell_val = data[i].replace(regex, ''); >+ if ( cell_val.length < 1 ) continue; >+ r += '<option value="'+cell_val+'">'+cell_val+'</option>'; >+ } >+ return r+'</select>'; >+ } >+ >+ $(document).ready(function() { >+ var table_settings = [% TablesSettings.GetTableSettings('circ', 'holds', 'closed-stack-requests', 'json') | $raw %]; >+ $('#holdst, #printed_slip_holds').each(function (i, el) { >+ const holdst = KohaTable(el.id, { >+ "sPaginationType": "full_numbers", >+ autoWidth: false, >+ }, table_settings); >+ holdst.fnAddFilters("filter"); >+ >+ $(".homebranchfilter", el).each( function () { >+ $(this).html( createSelect( holdst.fnGetColumnData(2) ) ); >+ $('select', this).change( function () { >+ holdst.fnFilter( $(this).val(), 2 ); >+ }); >+ }); >+ $(".itemtype-filter", el).each( function () { >+ $(this).html( createSelect( holdst.fnGetColumnData(8) ) ); >+ $('select', this).change( function () { >+ holdst.fnFilter( $(this).val(), 7 ); >+ }); >+ }); >+ $(".locationfilter", el).each( function () { >+ $(this).html( createSelect( holdst.fnGetColumnData(9) ) ); >+ $('select', this).change( function () { >+ holdst.fnFilter( $(this).val(), 8 ); >+ }); >+ }); >+ $(".pickup-location", el).each( function () { >+ $(this).html( createSelect( holdst.fnGetColumnData(13) ) ); >+ $('select', this).change( function () { >+ holdst.fnFilter( $(this).val(), 12 ); >+ }); >+ }); >+ }); >+ }); >+ </script> >+ <script> >+ $(document).ready(function() { >+ // Printing slip will change the reserve's status >+ // Reload the page to make the status change visible >+ $('#holdst .print-closed-stack-request-slip').on('click', function (ev) { >+ const reserve_id = $(this).data('reserve-id'); >+ const form = document.getElementById('print-closed-stack-request-slip-' + reserve_id); >+ if (form) { >+ form.submit(); >+ setTimeout(() => { location.reload() }, 1000); >+ } >+ }); >+ $('#holdst .set-waiting').on('click', function (ev) { >+ const reserve_id = $(this).data('reserve-id'); >+ const form = document.getElementById('set-waiting-' + reserve_id); >+ if (form) { >+ form.submit(); >+ } >+ }); >+ }); >+ </script> >+[% END %] >+ >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt >index c32de6ab9b..3bb75120c4 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt >@@ -180,7 +180,7 @@ > <div class="row"> > <div class="col-sm-12"> > [%# Following statement must be in one line for translatability %] >- [% IF ( CAN_user_tools_moderate_comments && pendingcomments ) || ( CAN_user_tools_moderate_tags && pendingtags ) || ( CAN_user_borrowers_edit_borrowers && pending_borrower_modifications ) || ( CAN_user_suggestions_suggestions_manage && ( pendingsuggestions || all_pendingsuggestions )) || ( CAN_user_borrowers_edit_borrowers && pending_discharge_requests ) || pending_article_requests || ( Koha.Preference('AllowCheckoutNotes') && CAN_user_circulate_manage_checkout_notes && pending_checkout_notes.count ) || ( ( Koha.Preference('OpacCatalogConcerns') || Koha.Preference('CatalogConcerns') ) && pending_biblio_tickets && CAN_user_editcatalogue_edit_catalogue ) || ( Koha.Preference('OPACReportProblem') && CAN_user_problem_reports && pending_problem_reports.count ) || already_ran_jobs || new_curbside_pickups.count || ( holds_with_cancellation_requests && CAN_user_circulate_circulate_remaining_permissions ) %] >+ [% IF ( CAN_user_tools_moderate_comments && pendingcomments ) || ( CAN_user_tools_moderate_tags && pendingtags ) || ( CAN_user_borrowers_edit_borrowers && pending_borrower_modifications ) || ( CAN_user_suggestions_suggestions_manage && ( pendingsuggestions || all_pendingsuggestions )) || ( CAN_user_borrowers_edit_borrowers && pending_discharge_requests ) || pending_article_requests || ( Koha.Preference('AllowCheckoutNotes') && CAN_user_circulate_manage_checkout_notes && pending_checkout_notes.count ) || ( ( Koha.Preference('OpacCatalogConcerns') || Koha.Preference('CatalogConcerns') ) && pending_biblio_tickets && CAN_user_editcatalogue_edit_catalogue ) || ( Koha.Preference('OPACReportProblem') && CAN_user_problem_reports && pending_problem_reports.count ) || already_ran_jobs || new_curbside_pickups.count || ( holds_with_cancellation_requests && CAN_user_circulate_circulate_remaining_permissions ) || pending_closed_stack_requests.count %] > <div id="area-pending" class="page-section"> > [% IF pending_article_requests %] > <div class="pending-info" id="article_requests_pending"> >@@ -275,6 +275,13 @@ > <span class="pending-number-link">[% holds_with_cancellation_requests | html %]</span> > </div> > [% END %] >+ >+ [% IF pending_closed_stack_requests.count %] >+ <div class="pending-info" id="pending_closed_stack_requests"> >+ <a href="/cgi-bin/koha/circ/closed-stack-requests.pl">Pending closed stack requests</a>: >+ <span class="pending-number-link">[% pending_closed_stack_requests.count | html %]</span> >+ </div> >+ [% END %] > </div> > > [% END %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt >index 2fcc09479f..9a65b0fff4 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt >@@ -122,7 +122,11 @@ > [% INCLUDE 'biblio-title.inc' link =1 %] > [% END %] > [% WRAPPER breadcrumb_item bc_active= 1 %] >- <span>Place a hold</span> >+ [% IF closed_stack_request %] >+ <span>Closed stack request</span> >+ [% ELSE %] >+ <span>Place a hold</span> >+ [% END %] > [% END %] > [% ELSE %] > [% IF ( patron ) %] >@@ -226,7 +230,11 @@ > [% END %] > > [% UNLESS ( multi_hold ) %] >- <h2>Place a hold on [% INCLUDE 'biblio-title.inc' link = 1 %] [% IF biblio.author %] by [% biblio.author | html %][% END %]</h2> >+ [% IF closed_stack_request %] >+ <h2>Closed stack request on [% INCLUDE 'biblio-title.inc' link = 1 %] [% IF biblio.author %] by [% biblio.author | html %][% END %]</h2> >+ [% ELSE %] >+ <h2>Place a hold on [% INCLUDE 'biblio-title.inc' link = 1 %] [% IF biblio.author %] by [% biblio.author | html %][% END %]</h2> >+ [% END %] > [% ELSE %] > <h2> > [% IF ( patron ) %] >@@ -562,6 +570,10 @@ > <input type="hidden" name="title" value="[% biblio.title | html %]" /> > <input type="hidden" name="rank-request" value="[% fixedRank | html %]" /> > >+ [% IF closed_stack_request %] >+ <input type="hidden" name="closed_stack_request" value="1"> >+ [% END %] >+ > <ol> > <li> > <span class="label">Patron:</span> >@@ -613,184 +625,189 @@ > </li> > </ol> > </fieldset> >- <fieldset class="rows any_specific"> >- <legend> >- [% IF force_hold_level == 'item' || force_hold_level == 'item_group' %] >- <input type="radio" id="requestany" name="request" disabled="true" /> >- [% ELSIF force_hold_level == 'record' %] >- <input type="radio" id="requestany" checked="checked" value="Any" disabled="true"/> >- <input type="hidden" name="request" value="Any"/> >- <span class="error"><i>(Required)</i></span> >- [% ELSE %] >- <input type="radio" id="requestany" name="request" checked="checked" value="Any" /> >- [% END %] >- <label for="requestany" class="inline"> >- Hold next available item >- </label> >- </legend> >- <input type="hidden" name="alreadyreserved" value="[% alreadyreserved | html %]" /> >- <fieldset class="enable_request_any disable_request_group disable_request_specific"> >- [% IF force_hold_level == 'item' # Patron has placed a item level hold previously for this record %] >- <span class="error"> >- <i class="fa fa-times fa-lg" title="Cannot be put on hold"></i> >- Hold must be item level >- </span> >- [% ELSIF force_hold_level == 'item_group' # Patron has placed an item group level hold previously for this record %] >- <span class="error"> >- <i class="fa fa-times fa-lg" title="Cannot be put on hold"></i> >- Hold must be item group level >- </span> >- [% ELSE %] >- <ol> >- >- <li> >- <label for="pickup">Pickup at:</label> >- <select name="pickup" id="pickup-next-avail" >- data-biblio-id="[% biblio.biblionumber | html %]" >- data-patron-id="[% patron.borrowernumber | html %]" >- data-pickup-location-source="biblio"> >- [% PROCESS options_for_libraries libraries => Branches.pickup_locations({ search_params => { biblio => biblionumber, patron => patron }, selected => pickup }) %] >- </select> >- </li> > >- [% IF Koha.Preference('AllowHoldItemTypeSelection') %] >- <li> >- <label for="itemtype">Request specific item type:</label> >- <select name="itemtype" id="itemtype"> >- <option value="">Any item type</option> >- [%- FOREACH itemtype IN available_itemtypes %] >- <option value="[% itemtype | html %]">[% ItemTypes.GetDescription( itemtype ) | html %]</option> >- [%- END %] >- </select> >- </li> >- [% END %] >- [% UNLESS remaining_holds_for_record == 1 %] >- <li> >- <label for="holds_to_place_count">Holds to place (count)</label> >- <input type="text" inputmode="numeric" pattern="[0-9]*" id="holds_to_place_count" name="holds_to_place_count" value="1" /> >- </li> >+ [% biblio_info = biblioloop.0 %] >+ >+ [% UNLESS closed_stack_request %] >+ <fieldset class="rows any_specific"> >+ <legend> >+ [% IF force_hold_level == 'item' || force_hold_level == 'item_group' %] >+ <input type="radio" id="requestany" name="request" disabled="true" /> >+ [% ELSIF force_hold_level == 'record' %] >+ <input type="radio" id="requestany" checked="checked" value="Any" disabled="true"/> >+ <input type="hidden" name="request" value="Any"/> >+ <span class="error"><i>(Required)</i></span> > [% ELSE %] >- <input type="hidden" name="holds_to_place_count" value="1" /> >+ <input type="radio" id="requestany" name="request" checked="checked" value="Any" /> > [% END %] >- </ol> >- [% END %] >+ <label for="requestany" class="inline"> >+ Hold next available item >+ </label> >+ </legend> >+ <input type="hidden" name="alreadyreserved" value="[% alreadyreserved | html %]" /> >+ <fieldset class="enable_request_any disable_request_group disable_request_specific"> >+ [% IF force_hold_level == 'item' # Patron has placed a item level hold previously for this record %] >+ <span class="error"> >+ <i class="fa fa-times fa-lg" title="Cannot be put on hold"></i> >+ Hold must be item level >+ </span> >+ [% ELSIF force_hold_level == 'item_group' # Patron has placed an item group level hold previously for this record %] >+ <span class="error"> >+ <i class="fa fa-times fa-lg" title="Cannot be put on hold"></i> >+ Hold must be item group level >+ </span> >+ [% ELSE %] >+ <ol> > >+ <li> >+ <label for="pickup">Pickup at:</label> >+ <select name="pickup" id="pickup-next-avail" >+ data-biblio-id="[% biblio.biblionumber | html %]" >+ data-patron-id="[% patron.borrowernumber | html %]" >+ data-pickup-location-source="biblio"> >+ [% PROCESS options_for_libraries libraries => Branches.pickup_locations({ search_params => { biblio => biblionumber, patron => patron }, selected => pickup }) %] >+ </select> >+ </li> > >- <fieldset class="action"> >- [% IF ( patron.borrowernumber ) %] >- [% IF ( override_required ) %] >- <button type="submit" id="hold_grp_btn" class="btn btn-primary warning"><i class="fa fa-exclamation-triangle "></i> Place hold</button> >- [% ELSIF ( none_available ) %] >- <button type="submit" id="hold_grp_btn" disabled="disabled" class="btn btn-primary btn-disabled">Place hold</button> >- [% ELSE %] >- <button type="submit" id="hold_grp_btn" class="btn btn-primary">Place hold</button> >+ [% IF Koha.Preference('AllowHoldItemTypeSelection') %] >+ <li> >+ <label for="itemtype">Request specific item type:</label> >+ <select name="itemtype" id="itemtype"> >+ <option value="">Any item type</option> >+ [%- FOREACH itemtype IN available_itemtypes %] >+ <option value="[% itemtype | html %]">[% ItemTypes.GetDescription( itemtype ) | html %]</option> >+ [%- END %] >+ </select> >+ </li> >+ [% END %] >+ [% UNLESS remaining_holds_for_record == 1 %] >+ <li> >+ <label for="holds_to_place_count">Holds to place (count)</label> >+ <input type="text" inputmode="numeric" pattern="[0-9]*" id="holds_to_place_count" name="holds_to_place_count" value="1" /> >+ </li> >+ [% ELSE %] >+ <input type="hidden" name="holds_to_place_count" value="1" /> >+ [% END %] >+ </ol> > [% END %] >- [% END %] >- </fieldset> >- </fieldset> >- </fieldset> >- >- <hr/> > >- [% biblio_info = biblioloop.0 %] >- <!-- ItemGroup level holds --> >- [% IF Koha.Preference('EnableItemGroupHolds') && biblio_info.object.item_groups.count %] >- <fieldset class="rows any_specific"> >- <legend> >- [% IF force_hold_level == 'item_group' %] >- <input type="radio" class="requestgrp" id="requestgrp" name="request" checked="checked" disabled="true" /> >- <span class="error"><i>(Required)</i></span> >- [% ELSIF force_hold_level == 'item' || force_hold_level == 'record' %] >- <input type="radio" class="requestgrp" id="requestgrp" name="request" disabled="true" /> >- [% ELSE %] >- <input type="radio" class="requestgrp" id="requestgrp" name="request" /> >- [% END %] >- <label for="requestgrp" class="inline"> >- Hold next available item from an item group >- </label> >- </legend> > >- <fieldset class="enable_request_group disable_request_any disable_request_specific"> >- [% IF force_hold_level == 'record' # Patron has placed a record level hold previously for this record %] >- <span class="error"> >- <i class="fa fa-times fa-lg" title="Cannot be put on hold"></i> >- Hold must be record level >- </span> >- [% ELSIF force_hold_level == 'item' # Patron has placed an item level hold previously for this record %] >- <span class="error"> >- <i class="fa fa-times fa-lg" title="Cannot be put on hold"></i> >- Hold must be item level >- </span> >- [% ELSE %] >- <ul> >- <li> >- <label for="pickup">Pickup at:</label> >- <select name="pickup" id="pickup-item-group" >- data-biblio-id="[% biblio.biblionumber | html %]" >- data-patron-id="[% patron.borrowernumber | html %]" >- data-pickup-location-source="biblio"> >- [% PROCESS options_for_libraries libraries => Branches.pickup_locations({ search_params => { biblio => biblionumber, patron => patron }, selected => pickup }) %] >- </select> >- </li> >- <li> >- <table id="requestgroup"> >- <thead> >- <tr> >- <th>Hold</th> >- <th>Item group</th> >- <th>Holdable items</th> >- </tr> >- </thead> >- <tbody> >- [% FOREACH g IN biblio_info.object.item_groups.search({}, { order_by => ['display_order'] }) %] >- [% IF g.items.count %] >- <tr> >- <td> >- <input id="item_group_id_[% g.id | html %]" class="requestgrp" type="radio" name="item_group_id" value="[% g.id | html %]" /> >- </td> >- <td> >- <label for="item_group_id_[% g.id | html %]">[% g.description | html %]</label> >- </td> >- <td> >- [% FOREACH i IN g.items %] >- <div><a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=[% i.biblionumber | uri %]#item[% i.itemnumber | uri %]">[% i.barcode | html %]</a></div> >- [% END %] >- </td> >- </tr> >- [% ELSE %] >- <tr> >- <td> >- <input id="item_group_id_[% g.id | html %]" class="requestgrp" type="radio" name="item_group_id" value="[% g.id | html %]" disabled="disabled" /> >- </td> >- <td> >- <label for="item_group_id_[% g.id | html %]">[% g.description | html %]</label> >- </td> >- <td> >- <div class="error">No holdable items in this item group.</div> >- </td> >- </tr> >- [% END %] >+ <fieldset class="action"> >+ [% IF ( patron.borrowernumber ) %] >+ [% IF ( override_required ) %] >+ <button type="submit" id="hold_grp_btn" class="btn btn-primary warning"><i class="fa fa-exclamation-triangle "></i> Place hold</button> >+ [% ELSIF ( none_available ) %] >+ <button type="submit" id="hold_grp_btn" disabled="disabled" class="btn btn-primary btn-disabled">Place hold</button> >+ [% ELSE %] >+ <button type="submit" id="hold_grp_btn" class="btn btn-primary">Place hold</button> > [% END %] >- </tbody> >- </table> >- </li> >- </ul> >- [% END %] >- <fieldset class="action"> >- [% IF ( patron.borrowernumber ) %] >- [% IF ( override_required ) %] >- <button type="submit" id="hold_any_btn" class="btn btn-primary warning"><i class="fa fa-exclamation-triangle "></i> Place hold</button> >- [% ELSIF ( none_available ) %] >- <button type="submit" id="hold_any_btn" disabled="disabled" class="btn btn-primary btn-disabled">Place hold</button> >+ [% END %] >+ </fieldset> >+ </fieldset> >+ </fieldset> >+ >+ <hr/> >+ >+ <!-- ItemGroup level holds --> >+ [% IF Koha.Preference('EnableItemGroupHolds') && biblio_info.object.item_groups.count %] >+ <fieldset class="rows any_specific"> >+ <legend> >+ [% IF force_hold_level == 'item_group' %] >+ <input type="radio" class="requestgrp" id="requestgrp" name="request" checked="checked" disabled="true" /> >+ <span class="error"><i>(Required)</i></span> >+ [% ELSIF force_hold_level == 'item' || force_hold_level == 'record' %] >+ <input type="radio" class="requestgrp" id="requestgrp" name="request" disabled="true" /> > [% ELSE %] >- <button type="submit" id="hold_any_btn" class="btn btn-primary">Place hold</button> >+ <input type="radio" class="requestgrp" id="requestgrp" name="request" /> > [% END %] >+ <label for="requestgrp" class="inline"> >+ Hold next available item from an item group >+ </label> >+ </legend> >+ >+ <fieldset class="enable_request_group disable_request_any disable_request_specific"> >+ [% IF force_hold_level == 'record' # Patron has placed a record level hold previously for this record %] >+ <span class="error"> >+ <i class="fa fa-times fa-lg" title="Cannot be put on hold"></i> >+ Hold must be record level >+ </span> >+ [% ELSIF force_hold_level == 'item' # Patron has placed an item level hold previously for this record %] >+ <span class="error"> >+ <i class="fa fa-times fa-lg" title="Cannot be put on hold"></i> >+ Hold must be item level >+ </span> >+ [% ELSE %] >+ <ul> >+ <li> >+ <label for="pickup">Pickup at:</label> >+ <select name="pickup" id="pickup-item-group" >+ data-biblio-id="[% biblio.biblionumber | html %]" >+ data-patron-id="[% patron.borrowernumber | html %]" >+ data-pickup-location-source="biblio"> >+ [% PROCESS options_for_libraries libraries => Branches.pickup_locations({ search_params => { biblio => biblionumber, patron => patron }, selected => pickup }) %] >+ </select> >+ </li> >+ <li> >+ <table id="requestgroup"> >+ <thead> >+ <tr> >+ <th>Hold</th> >+ <th>Item group</th> >+ <th>Holdable items</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH g IN biblio_info.object.item_groups.search({}, { order_by => ['display_order'] }) %] >+ [% IF g.items.count %] >+ <tr> >+ <td> >+ <input id="item_group_id_[% g.id | html %]" class="requestgrp" type="radio" name="item_group_id" value="[% g.id | html %]" /> >+ </td> >+ <td> >+ <label for="item_group_id_[% g.id | html %]">[% g.description | html %]</label> >+ </td> >+ <td> >+ [% FOREACH i IN g.items %] >+ <div><a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=[% i.biblionumber | uri %]#item[% i.itemnumber | uri %]">[% i.barcode | html %]</a></div> >+ [% END %] >+ </td> >+ </tr> >+ [% ELSE %] >+ <tr> >+ <td> >+ <input id="item_group_id_[% g.id | html %]" class="requestgrp" type="radio" name="item_group_id" value="[% g.id | html %]" disabled="disabled" /> >+ </td> >+ <td> >+ <label for="item_group_id_[% g.id | html %]">[% g.description | html %]</label> >+ </td> >+ <td> >+ <div class="error">No holdable items in this item group.</div> >+ </td> >+ </tr> >+ [% END %] >+ [% END %] >+ </tbody> >+ </table> >+ </li> >+ </ul> > [% END %] >+ >+ <fieldset class="action"> >+ [% IF ( patron.borrowernumber ) %] >+ [% IF ( override_required ) %] >+ <button type="submit" id="hold_any_btn" class="btn btn-primary warning"><i class="fa fa-exclamation-triangle "></i> Place hold</button> >+ [% ELSIF ( none_available ) %] >+ <button type="submit" id="hold_any_btn" disabled="disabled" class="btn btn-primary btn-disabled">Place hold</button> >+ [% ELSE %] >+ <button type="submit" id="hold_any_btn" class="btn btn-primary">Place hold</button> >+ [% END %] >+ [% END %] >+ </fieldset> >+ </fieldset> > </fieldset> >- </fieldset> >- </fieldset> >+ [% END # UNLESS closed_stack_request %] >+ <!-- /ItemGroup level holds --> > [% END %] >- <!-- /ItemGroup level holds --> > > <fieldset class="rows any_specific"> > <legend> >@@ -1188,6 +1205,10 @@ > </ul> > [% END %] > [% END %] >+ >+ [% IF itemloo.is_closed_stack %] >+ <br><span>Closed stack</span> >+ [% END %] > </td> > </tr> > [% END # /FOREACH biblioloo %] >@@ -1490,6 +1511,9 @@ > [% IF multi_hold %] > [% SET url_biblio_params = url_biblio_params _ "&multi_hold=1" %] > [% END %] >+ [% IF closed_stack_request %] >+ [% SET url_biblio_params = url_biblio_params _ "&closed_stack_request=1" %] >+ [% END %] > > <script> > $(document).ready(function () { >diff --git a/koha-tmpl/intranet-tmpl/prog/js/holds.js b/koha-tmpl/intranet-tmpl/prog/js/holds.js >index d18d394787..c6d35b9a96 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/holds.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/holds.js >@@ -326,6 +326,7 @@ $(document).ready(function() { > "url": '/cgi-bin/koha/svc/holds', > "data": function ( d ) { > d.borrowernumber = borrowernumber; >+ d.closed_stack_request = 0; > } > }, > }, table_settings_holds_table ); >@@ -497,4 +498,178 @@ $(document).ready(function() { > return toggle_suspend(this, inputs); > }); > >+ // Don't load holds table unless it is clicked on >+ $("#closed-stack-requests-tab").on( "click", function(){ load_closed_stack_requests_table() } ); >+ >+ // If the holds tab is preselected on load, we need to load the table >+ if ( $("#closed-stack-requests-tab").parent().hasClass('active') ) { load_closed_stack_requests_table() } >+ >+ function load_closed_stack_requests_table() { >+ >+ var holds = new Array(); >+ if ( ! $.fn.DataTable.isDataTable($('#closed-stack-requests-table')) ) { >+ var title; >+ const table = $("#closed-stack-requests-table").dataTable($.extend(true, {}, dataTablesDefaults, { >+ "bAutoWidth": false, >+ "sDom": "rt", >+ "columns": [ >+ { >+ "data": { _: "reservedate_formatted", "sort": "reservedate" } >+ }, >+ { >+ "mDataProp": function ( oObj ) { >+ title = "<a href='/cgi-bin/koha/reserve/request.pl?biblionumber=" >+ + oObj.biblionumber >+ + "'>" >+ + (oObj.title ? oObj.title.escapeHtml() : ''); >+ >+ $.each(oObj.subtitle, function( index, value ) { >+ title += " " + value.escapeHtml(); >+ }); >+ >+ title += " " + oObj.part_number + " " + oObj.part_name; >+ >+ if ( oObj.enumchron ) { >+ title += " (" + oObj.enumchron.escapeHtml() + ")"; >+ } >+ >+ title += "</a>"; >+ >+ if ( oObj.author ) { >+ title += " " + __("by _AUTHOR_").replace("_AUTHOR_", oObj.author.escapeHtml()); >+ } >+ >+ if ( oObj.itemnotes ) { >+ var span_class = ""; >+ if ( flatpickr.formatDate( new Date(oObj.issuedate), "Y-m-d" ) == ymd ){ >+ span_class = "circ-hlt"; >+ } >+ title += " - <span class='" + span_class + "'>" + oObj.itemnotes.escapeHtml() + "</span>" >+ } >+ >+ return title; >+ } >+ }, >+ { >+ "mDataProp": function( oObj ) { >+ return oObj.itemcallnumber && oObj.itemcallnumber.escapeHtml() || ""; >+ } >+ }, >+ { >+ "mDataProp": function( oObj ) { >+ var data = ""; >+ if ( oObj.itemtype ) { >+ data += oObj.itemtype_description; >+ } >+ return data; >+ } >+ }, >+ { >+ "mDataProp": function( oObj ) { >+ var data = ""; >+ if ( oObj.barcode ) { >+ data += " <a href='/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=" >+ + oObj.biblionumber >+ + "&itemnumber=" >+ + oObj.itemnumber >+ + "#item" >+ + oObj.itemnumber >+ + "'>" >+ + oObj.barcode.escapeHtml() >+ + "</a>"; >+ } >+ return data; >+ } >+ }, >+ { "data": { _: "expirationdate_formatted", "sort": "expirationdate" } }, >+ { >+ "mDataProp": function( oObj ) { >+ if ( oObj.priority && parseInt( oObj.priority ) && parseInt( oObj.priority ) > 0 ) { >+ return oObj.priority; >+ } else { >+ return ""; >+ } >+ } >+ }, >+ { >+ "bSortable": false, >+ "mDataProp": function( oObj ) { >+ return "<select name='rank-request'>" >+ +"<option value='n'>" + __("No") + "</option>" >+ +"<option value='del'>" + __("Yes") + "</option>" >+ + "</select>" >+ + "<input type='hidden' name='biblionumber' value='" + oObj.biblionumber + "'>" >+ + "<input type='hidden' name='borrowernumber' value='" + borrowernumber + "'>" >+ + "<input type='hidden' name='reserve_id' value='" + oObj.reserve_id + "'>"; >+ } >+ }, >+ { >+ "mDataProp": function( oObj ) { >+ var data = ""; >+ >+ if ( oObj.suspend == 1 ) { >+ data += "<p>" + __("Hold is <strong>suspended</strong>"); >+ if ( oObj.suspend_until ) { >+ data += " " + __("until %s").format(oObj.suspend_until_formatted); >+ } >+ data += "</p>"; >+ } >+ >+ if ( oObj.itemtype_limit ) { >+ data += __("Next available %s item").format(oObj.itemtype_limit); >+ } >+ >+ if ( oObj.item_group_id ) { >+ data += __("Next available item group <strong>%s</strong> item").format( oObj.item_group_description ); >+ } >+ >+ if ( oObj.barcode ) { >+ data += "<em>"; >+ if ( oObj.found == "W" ) { >+ >+ if ( oObj.waiting_here ) { >+ data += __("Item is <strong>waiting here</strong>"); >+ if (oObj.desk_name) { >+ data += ", " + __("at %s").format(oObj.desk_name.escapeHtml()); >+ } >+ } else { >+ data += __("Item is <strong>waiting</strong>"); >+ data += " " + __("at %s").format(oObj.waiting_at); >+ if (oObj.desk_name) { >+ data += ", " + __("at %s").format(oObj.desk_name.escapeHtml()); >+ } >+ >+ } >+ >+ } else if ( oObj.transferred ) { >+ data += __("Item is <strong>in transit</strong> from %s since %s").format(oObj.from_branch, oObj.date_sent); >+ } else if ( oObj.not_transferred ) { >+ data += __("Item hasn't been transferred yet from %s").format(oObj.not_transferred_by); >+ } >+ data += "</em>"; >+ } >+ return data; >+ } >+ } >+ ], >+ "bPaginate": false, >+ "bProcessing": true, >+ "bServerSide": false, >+ "ajax": { >+ "url": '/cgi-bin/koha/svc/holds', >+ "data": function ( d ) { >+ d.borrowernumber = borrowernumber; >+ d.closed_stack_request = 1; >+ } >+ }, >+ })); >+ >+ if ( $("#closed-stack-requests-table").length ) { >+ $("#closed-stack-requests-table_processing").position({ >+ of: $( "#closed-stack-requests-table" ), >+ collision: "none" >+ }); >+ } >+ } >+ } > }); >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/holds-table.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/holds-table.inc >index a92c77b540..adc852492a 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/holds-table.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/holds-table.inc >@@ -3,7 +3,8 @@ > [% USE KohaDates %] > [% PROCESS 'i18n.inc' %] > >-<table id="holdst" class="table table-bordered table-striped"> >+[% DEFAULT table_id = 'holdst' %] >+<table id="[% table_id | html %]" class="table table-bordered table-striped"> > <caption>Holds <span class="count">([% HOLDS.count | html %] total)</span></caption> > <!-- HOLDS TABLE ROWS --> > <thead> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-detail-sidebar.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-detail-sidebar.inc >index a0ddc377be..ed256c6647 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-detail-sidebar.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-detail-sidebar.inc >@@ -8,6 +8,10 @@ > [% IF ( ReservableItems ) %] > <li><a class="reserve btn btn-link btn-lg" href="/cgi-bin/koha/opac-reserve.pl?biblionumber=[% biblio.biblionumber | html %]"><i class="fa fa-fw fa-bookmark" aria-hidden="true"></i> Place hold</a></li> > [% END %] >+ >+ [% IF biblio.items.filter_by_closed_stack.count > 0 %] >+ <li><a class="btn btn-link btn-lg" href="/cgi-bin/koha/opac-reserve.pl?biblionumber=[% biblio.biblionumber | html %]&closed_stack_request=1"><i class="fa fa-fw fa-bookmark" aria-hidden="true"></i> Closed stack request</a></li> >+ [% END %] > [% END %] > [% END %] > >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reserve.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reserve.tt >index 78db93ddbb..65342096d4 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reserve.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-reserve.tt >@@ -10,7 +10,11 @@ > [% SET reserve_input_type = 'checkbox' %] > [% END %] > [% INCLUDE 'doc-head-open.inc' %] >-<title>Placing a hold › [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</title> >+[% IF closed_stack_request %] >+ <title>Closed stack request › [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</title> >+[% ELSE %] >+ <title>Placing a hold › [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</title> >+[% END %] > [% INCLUDE 'doc-head-close.inc' %] > [% BLOCK cssinclude %][% END %] > </head> >@@ -21,13 +25,21 @@ > <div class="main"> > [% WRAPPER breadcrumbs %] > [% WRAPPER breadcrumb_item bc_active= 1 %] >- <span>Placing a hold</span> >+ [% IF closed_stack_request %] >+ <span>Closed stack request</span> >+ [% ELSE %] >+ <span>Placing a hold</span> >+ [% END %] > [% END %] > [% END #/ WRAPPER breadcrumbs %] > > <div class="container"> > <div id="holds" class="maincontent"> >- <h1>Placing a hold</h1> >+ [% IF closed_stack_request %] >+ <h1>Closed stack request</h1> >+ [% ELSE %] >+ <h1>Placing a hold</h1> >+ [% END %] > [% IF ( message ) %] > <div id="holdmessages" class="alert"> > <p>Sorry, you cannot place holds.</p> >@@ -150,7 +162,11 @@ > > [% UNLESS ( message ) %] > [% UNLESS ( none_available ) %] >- <h2>Confirm holds for:[% INCLUDE 'patron-title.inc' patron = logged_in_user %] ([% logged_in_user.cardnumber | html %])</h2> >+ [% IF closed_stack_request %] >+ <h2>Confirm closed stack request for:[% INCLUDE 'patron-title.inc' patron = logged_in_user %] ([% logged_in_user.cardnumber | html %])</h2> >+ [% ELSE %] >+ <h2>Confirm holds for:[% INCLUDE 'patron-title.inc' patron = logged_in_user %] ([% logged_in_user.cardnumber | html %])</h2> >+ [% END %] > [% END # / UNLESS none_available %] > > [% IF ( new_reserves_allowed ) %] >@@ -163,6 +179,9 @@ > [% INCLUDE 'csrf-token.inc' %] > <legend class="sr-only">Hold requests</legend> > <input type="hidden" name="op" value="cud-place_reserve" /> >+ [% IF closed_stack_request %] >+ <input type="hidden" name="closed_stack_request" value="1"/> >+ [% END %] > <!-- These values are set dynamically by js --> > <input type="hidden" name="biblionumbers" id="biblionumbers"/> > <input type="hidden" name="selecteditems" id="selections"/> >@@ -173,11 +192,9 @@ > [% IF bibitemloo.forced_hold_level %] > <div class="alert alert-info forced_hold_level"> > [% IF bibitemloo.forced_hold_level == 'item' %] >- <span>You already have at least one item level hold on this title. >- All further holds must be item level.</span> >+ <span>Hold policy requires holds to be item level.</span> > [% ELSE %] >- <span>You already have at least one record level hold on this title. >- All further holds must be record level.</span> >+ <span>Hold policy requires holds to be record level.</span> > [% END %] > </div> > [% END %] >@@ -264,7 +281,7 @@ > </li> > [% END %] > >- [% UNLESS ( singleBranchMode ) %] >+ [% UNLESS ( singleBranchMode || closed_stack_request ) %] > [% IF ( bibitemloo.holdable && Koha.Preference('OPACAllowUserToChooseBranch')) %] > <li class="branch"> > <label for="branch_[% bibitemloo.biblionumber | html %]">Pick up location:</label> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt >index 12163f0e4f..6860276263 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt >@@ -299,6 +299,11 @@ > <span>Holds ([% RESERVES.count | html %])</span> > [% END %] > [% END %] >+ [% IF ( closed_stack_requests.count ) %] >+ [% WRAPPER tab_item tabname="opac-user-closed-stack-requests" %] >+ <span>Closed stack requests ([% closed_stack_requests.count | html %])</span> >+ [% END %] >+ [% END %] > [% IF Koha.Preference('UseRecalls') && RECALLS.count %] > [% WRAPPER tab_item tabname= "opac-user-recalls" %] > <span>Recalls ([% RECALLS.count | html %])</span> >@@ -844,6 +849,12 @@ > [% END # /tab_panel#opac-user-holds %] > [% END # / #RESERVES.count %] > >+ [% IF ( closed_stack_requests.count ) %] >+ [% WRAPPER tab_panel tabname="opac-user-closed-stack-requests" %] >+ [% PROCESS 'holds-table.inc' HOLDS = closed_stack_requests, SuspendHoldsOpac = SuspendHoldsOpac, showpriority = showpriority, AutoResumeSuspendedHolds = AutoResumeSuspendedHolds, table_id = 'closed-stack-requests-table' %] >+ [% END %] >+ [% END %] >+ > [% IF Koha.Preference('UseRecalls') && RECALLS.count %] > [% WRAPPER tab_panel tabname="opac-user-recalls" %] > <table id="recalls-table" class="table table-bordered table-striped"> >@@ -1107,6 +1118,9 @@ > [% IF ( opac_user_holds ) %] > $("#opac-user-views a[href='#opac-user-holds_panel']").tab("show"); > [% END %] >+ [% IF ( opac_user_closed_stack_requests ) %] >+ $("#opac-user-views a[href='#opac-user-closed-stack-requests_panel']").tab("show"); >+ [% END %] > [% IF ( opac_user_article_requests ) %] > $("#opac-user-views a[href='#opac-user-article-requests_panel']").tab("show"); > [% END %] >@@ -1218,7 +1232,7 @@ > ); > }); > >- var dTables = $("#checkoutst,#holdst,#overduest,#opac-user-relative-issues-table"); >+ var dTables = $("#checkoutst,#holdst,#closed-stack-requests-table,#overduest,#opac-user-relative-issues-table"); > dTables.each(function(){ > var thIndex = $(this).find("th.psort").index(); > $(this).on("init.dt", function() { >diff --git a/mainpage.pl b/mainpage.pl >index d84408538e..65ed400839 100755 >--- a/mainpage.pl >+++ b/mainpage.pl >@@ -138,6 +138,10 @@ if ( C4::Context->preference('CurbsidePickup') ) { > ); > } > >+my $pending_closed_stack_requests = >+ Koha::Holds->search( { branchcode => C4::Context->userenv->{branch} } )->filter_by_closed_stack_requests() >+ ->search( { closed_stack_request_slip_printed => 0 } ); >+ > $template->param( > pendingcomments => $pendingcomments, > pendingtags => $pendingtags, >@@ -145,6 +149,7 @@ $template->param( > pending_discharge_requests => $pending_discharge_requests, > pending_article_requests => $pending_article_requests, > pending_problem_reports => $pending_problem_reports, >+ pending_closed_stack_requests => $pending_closed_stack_requests, > ); > > output_html_with_http_headers $query, $cookie, $template->output; >diff --git a/members/moremember.pl b/members/moremember.pl >index 773bc4190d..c9ccb1c046 100755 >--- a/members/moremember.pl >+++ b/members/moremember.pl >@@ -209,7 +209,6 @@ if ( $patron->is_expired || $patron->is_going_to_expire ) { > my $holds = Koha::Holds->search( { borrowernumber => $borrowernumber } ); # FIXME must be Koha::Patron->holds > my $waiting_holds = $holds->waiting; > $template->param( >- holds_count => $holds->count(), > WaitingHolds => $waiting_holds, > ); > >@@ -288,21 +287,22 @@ my $has_modifications = Koha::Patron::Modifications->search( { borrowernumber => > my $patron_lists_count = $patron->get_lists_with_patron->count(); > > $template->param( >- patron => $patron, >- issuecount => $patron->checkouts->count, >- holds_count => $patron->holds->count, >- fines => $patron->account->balance, >- translated_language => $translated_language, >- detailview => 1, >- was_renewed => scalar $input->param('was_renewed') ? 1 : 0, >- $category_type => 1, # [% IF ( I ) %] = institutional/organisation >- housebound_role => scalar $patron->housebound_role, >- relatives_issues_count => $relatives_issues_count, >- relatives_borrowernumbers => \@relatives, >- logged_in_user => $logged_in_user, >- files => Koha::Patron::Files->new( borrowernumber => $borrowernumber ) ->GetFilesInfo(), >- has_modifications => $has_modifications, >- patron_lists_count => $patron_lists_count, >+ patron => $patron, >+ issuecount => $patron->checkouts->count, >+ holds_count => $patron->holds->filter_out_closed_stack_requests()->count, >+ closed_stack_requests_count => $patron->holds->filter_by_closed_stack_requests()->count, >+ fines => $patron->account->balance, >+ translated_language => $translated_language, >+ detailview => 1, >+ was_renewed => scalar $input->param('was_renewed') ? 1 : 0, >+ $category_type => 1, # [% IF ( I ) %] = institutional/organisation >+ housebound_role => scalar $patron->housebound_role, >+ relatives_issues_count => $relatives_issues_count, >+ relatives_borrowernumbers => \@relatives, >+ logged_in_user => $logged_in_user, >+ files => Koha::Patron::Files->new( borrowernumber => $borrowernumber )->GetFilesInfo(), >+ has_modifications => $has_modifications, >+ patron_lists_count => $patron_lists_count, > ); > > if ( C4::Context->preference('UseRecalls') ) { >diff --git a/opac/opac-reserve.pl b/opac/opac-reserve.pl >index 002a7b13bf..82102dfedc 100755 >--- a/opac/opac-reserve.pl >+++ b/opac/opac-reserve.pl >@@ -88,6 +88,9 @@ if (! $biblionumbers) { > $biblionumbers = $query->param('biblionumber'); > } > >+my $closed_stack_request = $query->param('closed_stack_request'); >+$template->param( closed_stack_request => $closed_stack_request ); >+ > if ( !$biblionumbers && $op ne 'cud-place_reserve' ) { > $template->param( message => 1, no_biblionumber => 1 ); > output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 }; >@@ -246,6 +249,10 @@ if ( $op eq 'cud-place_reserve' ) { > } > } > >+ if ( $closed_stack_request && $item ) { >+ $branch = $item->holdingbranch; >+ } >+ > # if we have an item, we are placing the hold on the item's bib, in case of analytics > if ( $item ) { > $biblioNum = $item->biblionumber; >@@ -332,7 +339,10 @@ if ( $op eq 'cud-place_reserve' ) { > } > } > >- print $query->redirect("/cgi-bin/koha/opac-user.pl?" . ( @failed_holds ? "failed_holds=" . join('|',@failed_holds) : q|| ) . "&opac-user-holds=1"); >+ my $param = $closed_stack_request ? 'opac-user-closed-stack-requests' : 'opac-user-holds'; >+ print $query->redirect( "/cgi-bin/koha/opac-user.pl?" >+ . ( @failed_holds ? "failed_holds=" . join( '|', @failed_holds ) : q|| ) >+ . "&$param=1" ); > exit; > } > >@@ -445,6 +455,7 @@ foreach my $biblioNum (@biblionumbers) { > # it's complicated logic to analyse. > # (before this loop was inside that sub loop so it was O(n^2) ) > foreach my $item (@{$biblioData->{items}}) { >+ next if ($closed_stack_request xor $item->is_available_for_closed_stack_request); > > my $item_info = $item->unblessed; > $item_info->{holding_branch} = $item->holding_branch; >@@ -573,13 +584,19 @@ foreach my $biblioNum (@biblionumbers) { > # patron placed a record level hold, all the holds the patron places must > # be record level. If the patron placed an item level hold, all holds > # the patron places must be item level >- my $forced_hold_level = Koha::Holds->search( >- { >- borrowernumber => $borrowernumber, >- biblionumber => $biblioNum, >- found => undef, >- } >- )->forced_hold_level(); >+ # Unless the biblio itself forces a specific hold level which >+ # supersedes the above rules >+ my $forced_hold_level = $biblio->forced_hold_level; >+ unless ($forced_hold_level) { >+ $forced_hold_level = Koha::Holds->search( >+ { >+ borrowernumber => $borrowernumber, >+ biblionumber => $biblioNum, >+ found => undef, >+ } >+ )->forced_hold_level(); >+ } >+ > if ($forced_hold_level) { > $biblioLoopIter{force_hold} = 1 if $forced_hold_level eq 'item'; > $biblioLoopIter{force_hold} = 0 if $forced_hold_level eq 'item_group'; >diff --git a/opac/opac-user.pl b/opac/opac-user.pl >index 3f1ca4ad59..2cea5411e9 100755 >--- a/opac/opac-user.pl >+++ b/opac/opac-user.pl >@@ -333,11 +333,13 @@ if ($show_barcode) { > $template->param( show_barcode => 1 ) if $show_barcode; > > # now the reserved items.... >-my $reserves = $patron->holds->filter_out_has_cancellation_requests; >+my $reserves = $patron->holds->filter_out_has_cancellation_requests->filter_out_closed_stack_requests; >+my $closed_stack_requests = $patron->holds->filter_out_has_cancellation_requests->filter_by_closed_stack_requests; > > $template->param( >- RESERVES => $reserves, >- showpriority => $show_priority, >+ RESERVES => $reserves, >+ closed_stack_requests => $closed_stack_requests, >+ showpriority => $show_priority, > ); > > if ( C4::Context->preference('UseRecalls') ) { >@@ -418,6 +420,8 @@ $template->param( > failed_holds => scalar $query->param('failed_holds'), > opac_user_holds => scalar $query->param('opac-user-holds') || 0, > opac_user_article_requests => scalar $query->param('opac-user-article-requests') || 0, >+ >+ opac_user_closed_stack_requests => scalar $query->param('opac-user-closed-stack-requests') || 0, > ); > > # if not an empty string this indicates to return >diff --git a/reserve/placerequest.pl b/reserve/placerequest.pl >index 11aa387672..5c2eacee8c 100755 >--- a/reserve/placerequest.pl >+++ b/reserve/placerequest.pl >@@ -167,7 +167,11 @@ if ( $op eq 'cud-placerequest' && $patron ) { > foreach my $msg ( keys %failed_holds ) { > push( @failed_hold_msgs, $msg ); > } >- $redirect_url->query_form( biblionumber => [@biblionumbers], failed_holds => \@failed_hold_msgs ); >+ my %params = ( biblionumber => [@biblionumbers], failed_holds => \@failed_hold_msgs ); >+ if ( $input->param('closed_stack_request') ) { >+ $params{closed_stack_request} = 1; >+ } >+ $redirect_url->query_form(%params); > print $input->redirect($redirect_url); > } elsif ( $borrowernumber eq '' ) { > print $input->header(); >diff --git a/reserve/request.pl b/reserve/request.pl >index b19fa3b868..d373679281 100755 >--- a/reserve/request.pl >+++ b/reserve/request.pl >@@ -66,6 +66,7 @@ my ( $template, $borrowernumber, $cookie, $flags ) = get_template_and_user( > > my $showallitems = $input->param('showallitems'); > my $pickup = $input->param('pickup'); >+my $closed_stack_request = $input->param('closed_stack_request'); > > my $itemtypes = { > map { >@@ -353,6 +354,8 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold ) > # patron placed a record level hold, all the holds the patron places must > # be record level. If the patron placed an item level hold, all holds > # the patron places must be item level >+ # Unless the biblio itself forces a specific hold level which >+ # supersedes the above rules > my $holds = Koha::Holds->search( > { > borrowernumber => $patron->borrowernumber, >@@ -360,7 +363,9 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold ) > found => undef, > } > ); >- $template->param( force_hold_level => $holds->forced_hold_level() ); >+ >+ my $forced_hold_level = $biblio->forced_hold_level // $holds->forced_hold_level(); >+ $template->param( force_hold_level => $forced_hold_level ); > > # For a librarian to be able to place multiple record holds for a patron for a record, > # we must find out what the maximum number of holds they can place for the patron is >@@ -400,6 +405,8 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold ) > # (before this loop was inside that sub loop so it was O(n^2) ) > > for my $item_object ( @items ) { >+ next if ( $closed_stack_request xor $item_object->is_available_for_closed_stack_request ); >+ > my $do_check; > my $item = $item_object->unblessed; > $item->{object} = $item_object; >@@ -610,7 +617,13 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold ) > )->unblessed > } > }; >- my @reserves = Koha::Holds->search( { biblionumber => $biblionumber }, { order_by => 'priority' } )->as_list; >+ my $holds_rs = Koha::Holds->search( { 'me.biblionumber' => $biblionumber }, { order_by => 'priority' } ); >+ if ($closed_stack_request) { >+ $holds_rs = $holds_rs->filter_by_closed_stack_requests(); >+ } else { >+ $holds_rs = $holds_rs->filter_out_closed_stack_requests(); >+ } >+ my @reserves = $holds_rs->as_list; > foreach my $res ( > sort { > my $a_found = $a->found() || ''; >@@ -730,6 +743,8 @@ $template->param(borrowernumber => $borrowernumber_hold); > > $template->param( failed_holds => \@failed_holds ); > >+$template->param( closed_stack_request => $closed_stack_request ); >+ > # printout the page > output_html_with_http_headers $input, $cookie, $template->output; > >diff --git a/svc/holds b/svc/holds >index eb40d2f2c7..6cab0113aa 100755 >--- a/svc/holds >+++ b/svc/holds >@@ -55,6 +55,8 @@ my $sorting_direction = $input->param('sSortDir_0') || 'desc'; > my $iSortCol = $input->param('iSortCol_0') // 0; > my $sorting_column = $sort_columns[$iSortCol] // 'reservedate'; > >+my $closed_stack_request = $input->param('closed_stack_request'); >+ > binmode STDOUT, ":encoding(UTF-8)"; > print $input->header( -type => 'application/json', -charset => 'UTF-8' ); > >@@ -65,6 +67,14 @@ my $holds_rs = Koha::Holds->search( > } > ); > >+if ( defined $closed_stack_request ) { >+ if ($closed_stack_request) { >+ $holds_rs = $holds_rs->filter_by_closed_stack_requests(); >+ } else { >+ $holds_rs = $holds_rs->filter_out_closed_stack_requests(); >+ } >+} >+ > my @holds; > while ( my $h = $holds_rs->next() ) { > my $item = $h->item(); >-- >2.39.2
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 38666
:
175354
|
175371
|
175372
|
175373
|
178977
|
178978
|
178979
|
179466
|
179467
|
179468
|
180309
|
180310
|
180311
|
180321
|
180322
|
180323
|
180822
|
180823
|
180824