@@ -, +, @@ of titles --- a/C4/Reserves.pm +++ a/C4/Reserves.pm @@ -139,6 +139,10 @@ BEGIN { IsItemOnHoldAndFound GetMaxPatronHoldsForRecord + + &AddReserveGroup + &DeleteReserveGroup + &GetReserveGroupReserves ); @EXPORT_OK = qw( MergeHolds ); } @@ -164,7 +168,8 @@ sub AddReserve { my ( $branch, $borrowernumber, $biblionumber, $bibitems, $priority, $resdate, $expdate, $notes, - $title, $checkitem, $found, $itemtype + $title, $checkitem, $found, $itemtype, + $reserve_group_id ) = @_; $resdate = output_pref( { str => dt_from_string( $resdate ), dateonly => 1, dateformat => 'iso' }) @@ -212,6 +217,7 @@ sub AddReserve { waitingdate => $waitingdate, expirationdate => $expdate, itemtype => $itemtype, + reserve_group_id => $reserve_group_id, } )->store(); $hold->set_waiting() if $found eq 'W'; @@ -1066,6 +1072,16 @@ sub ModReserveAffect { $hold->itemnumber($itemnumber); $hold->set_waiting($transferToDo); + if ($hold->reserve_group_id) { + # Delete other reserves from same group + $dbh->do(q{ + DELETE FROM reserves + WHERE reserve_group_id = ? + AND reserve_id != ? + }, undef, $hold->reserve_group_id, $hold->reserve_id); + DeleteReserveGroup($hold->reserve_group_id); + } + _koha_notify_reserve( $hold->reserve_id ) if ( !$transferToDo && !$already_on_shelf ); @@ -2152,6 +2168,60 @@ sub GetHoldRule { return $sth->fetchrow_hashref(); } +=head2 AddReserveGroup + + my $reserve_group_id = AddReserveGroup(); + +Creates a new reserve group and returns its ID + +=cut + +sub AddReserveGroup { + my $dbh = C4::Context->dbh; + + $dbh->do('INSERT INTO reserve_group VALUES ()'); + return $dbh->last_insert_id(undef, undef, 'reserve_group', undef); +} + +=head2 DeleteReserveGroup + + DeleteReserveGroup($reserve_group_id); + +Delete a reserve group. Reserves that belong to this group are not removed. + +=cut + +sub DeleteReserveGroup { + my ($reserve_group_id) = @_; + + my $dbh = C4::Context->dbh; + my $query = 'DELETE FROM reserve_group WHERE reserve_group_id = ?'; + $dbh->do($query, undef, $reserve_group_id); +} + +=head2 GetReserveGroupReserves + + my @reserves = GetReserveGroupReserves($reserve_group_id); + +Fetch all reserve from a reserve group. + +=cut + +sub GetReserveGroupReserves { + my ($reserve_group_id) = @_; + + my $dbh = C4::Context->dbh; + + my $reserves = $dbh->selectall_arrayref(q{ + SELECT * FROM reserves + WHERE reserve_group_id = ? + }, { Slice => {} }, $reserve_group_id); + + return () unless defined $reserves; + + return @$reserves; +} + =head1 AUTHOR Koha Development Team --- a/Koha/Template/Plugin/Biblio.pm +++ a/Koha/Template/Plugin/Biblio.pm @@ -63,4 +63,12 @@ sub CanArticleRequest { return $biblio ? $biblio->can_article_request( $borrower ) : 0; } +sub Get { + my ($self, $biblionumber) = @_; + + my $biblio = Koha::Biblios->find($biblionumber); + + return $biblio; +} + 1; --- a/circ/pendingreserves.pl +++ a/circ/pendingreserves.pl @@ -197,6 +197,7 @@ my $strsth = ORDER BY items.itemnumber SEPARATOR '
') l_enumchron, GROUP_CONCAT(DISTINCT items.copynumber ORDER BY items.itemnumber SEPARATOR '
') l_copynumber, + reserves.reserve_group_id, biblio.title, biblio.author, count(DISTINCT items.itemnumber) as icount, @@ -263,6 +264,7 @@ while ( my $data = $sth->fetchrow_hashref ) { holdingbranch => $data->{holdingbranch}, homebranch => $data->{homebranch}, itemnumber => $data->{itemnumber}, + reserve_group_id => $data->{reserve_group_id}, } ); } --- a/installer/data/mysql/atomicupdate/bug_15516.sql +++ a/installer/data/mysql/atomicupdate/bug_15516.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS reserve_group; +CREATE TABLE reserve_group ( + reserve_group_id int unsigned NOT NULL AUTO_INCREMENT, + PRIMARY KEY (reserve_group_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +ALTER TABLE reserves ADD COLUMN reserve_group_id int unsigned NULL DEFAULT NULL; +ALTER TABLE reserves ADD CONSTRAINT reserves_ibfk_6 FOREIGN KEY (reserve_group_id) REFERENCES reserve_group (reserve_group_id) ON DELETE SET NULL ON UPDATE CASCADE; --- a/installer/data/mysql/kohastructure.sql +++ a/installer/data/mysql/kohastructure.sql @@ -1894,6 +1894,7 @@ CREATE TABLE `reserves` ( -- information related to holds/reserves in Koha `suspend` BOOLEAN NOT NULL DEFAULT 0, `suspend_until` DATETIME NULL DEFAULT NULL, `itemtype` VARCHAR(10) NULL DEFAULT NULL, -- If record level hold, the optional itemtype of the item the patron is requesting + `reserve_group_id` int unsigned NULL DEFAULT NULL, -- Id used on reservations spanning multiple titles PRIMARY KEY (`reserve_id`), KEY priorityfoundidx (priority,found), KEY `borrowernumber` (`borrowernumber`), @@ -1905,7 +1906,18 @@ CREATE TABLE `reserves` ( -- information related to holds/reserves in Koha CONSTRAINT `reserves_ibfk_2` FOREIGN KEY (`biblionumber`) REFERENCES `biblio` (`biblionumber`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `reserves_ibfk_3` FOREIGN KEY (`itemnumber`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT `reserves_ibfk_4` FOREIGN KEY (`branchcode`) REFERENCES `branches` (`branchcode`) ON DELETE CASCADE ON UPDATE CASCADE, - CONSTRAINT `reserves_ibfk_5` FOREIGN KEY (`itemtype`) REFERENCES `itemtypes` (`itemtype`) ON DELETE CASCADE ON UPDATE CASCADE + CONSTRAINT `reserves_ibfk_5` FOREIGN KEY (`itemtype`) REFERENCES `itemtypes` (`itemtype`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `reserves_ibfk_6` FOREIGN KEY (`reserve_group_id`) REFERENCES `reserve_group` (`reserve_group_id`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table reserve_group +-- + +DROP TABLE IF EXISTS `reserve_group`; +CREATE TABLE `reserve_group` ( + `reserve_group_id` int unsigned NOT NULL AUTO_INCREMENT, + PRIMARY KEY(`reserve_group_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- --- a/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc @@ -157,6 +157,9 @@ [% END %] [% END %] + [% IF hold.reserve_group_id %] +
(part of a reserve group)
+ [% END %] [% IF ( CAN_user_reserveforothers_modify_holds_priority ) %] --- a/koha-tmpl/intranet-tmpl/prog/en/includes/reserve-group-modal.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/reserve-group-modal.inc @@ -0,0 +1,27 @@ + + + --- a/koha-tmpl/intranet-tmpl/prog/en/includes/strings.inc +++ a/koha-tmpl/intranet-tmpl/prog/en/includes/strings.inc @@ -46,5 +46,7 @@ var CURRENT = _(" (current) "); var MSG_NO_ITEMTYPE = _("No itemtype"); var MSG_CHECKOUTS_BY_ITEMTYPE = _("Number of checkouts by item type"); + var PART_OF_A = _("(part of a %s)"); + var RESERVE_GROUP = _("reserve group"); //]]> --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt @@ -1101,4 +1101,5 @@ No patron matched [% message | html %] [% Asset.js("js/members-menu.js") | $raw %] [% END %] +[% INCLUDE 'reserve-group-modal.inc' %] [% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt @@ -80,6 +80,9 @@ [% INCLUDE 'biblio-default-view.inc' biblionumber = reserveloo.biblionumber %] [% reserveloo.title | html %] [% FOREACH s IN reserveloo.subtitle %] [% s | html %][% END %]

[% IF ( reserveloo.author ) %]

by [% reserveloo.author | html %]

[% END %] + [% IF reserveloo.reserve_group_id %] +

(part of a reserve group)

+ [% END %] [% ELSE %] " @@ -262,4 +265,6 @@ [% END %] +[%# TODO: this module needs to be separated to html and js part to be included correctly %] +[% INCLUDE 'reserve-group-modal.inc' %] [% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt @@ -929,4 +929,5 @@ [% END %] +[% INCLUDE 'reserve-group-modal.inc' %] [% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt @@ -231,20 +231,24 @@ Clear date - [% UNLESS ( multi_hold ) %]
  • - [% IF force_hold_level == 'item' %] + [% IF multi_hold %] + + [% ELSE %] + [% IF force_hold_level == 'item' %] - [% ELSIF force_hold_level == 'record' %] + [% ELSIF force_hold_level == 'record' %] - [% ELSE %] + [% ELSE %] [% END %] + [% END %]
  • + [% UNLESS ( multi_hold ) %] [% IF remaining_holds_for_record > 1 %]
  • @@ -872,4 +876,6 @@ [% END %] +[% INCLUDE 'reserve-group-modal.inc' %] + [% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/reserve-group.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/reserve-group.tt @@ -0,0 +1,30 @@ +[% USE Biblio %] +[% INCLUDE 'doc-head-open.inc' %] + Koha › Circulation › Holds › Holds group ([% reserve_group_id %]) + [% INCLUDE 'doc-head-close.inc' %] + + +

    Holds group ([% reserve_group_id %])

    +
    + [% IF reserves.size %] + + + + + + + + + + [% FOREACH reserve IN reserves %] + [% biblio = Biblio.Get(reserve.biblionumber) %] + + + + + + [% END %] + +
    TitlePriorityNotes
    [% biblio.title %][% reserve.priority %][% reserve.reservenotes %]
    + [% END %] + [% INCLUDE 'intranet-bottom.inc' %] --- a/koha-tmpl/intranet-tmpl/prog/js/holds.js +++ a/koha-tmpl/intranet-tmpl/prog/js/holds.js @@ -42,6 +42,12 @@ $(document).ready(function() { title += " - " + oObj.itemnotes.escapeHtml() + "" } + if (oObj.reserve_group_id) { + title += '
    '; + var link = '' + RESERVE_GROUP +'' + title += '' + PART_OF_A.format(link) + ''; + } + return title; } }, --- a/reserve/placerequest.pl +++ a/reserve/placerequest.pl @@ -50,6 +50,7 @@ my $title = $input->param('title'); my $checkitem = $input->param('checkitem'); my $expirationdate = $input->param('expiration_date'); my $itemtype = $input->param('itemtype') || undef; +my $requestany = $input->param('requestany'); my $borrower = Koha::Patrons->find( $borrowernumber ); $borrower = $borrower->unblessed if $borrower; @@ -72,6 +73,11 @@ my $found; if ( $type eq 'str8' && $borrower ) { + my $reserve_group_id; + if ($multi_hold && $requestany) { + $reserve_group_id = AddReserveGroup(); + } + foreach my $biblionumber ( keys %bibinfos ) { my $count = @bibitems; @bibitems = sort @bibitems; @@ -95,8 +101,10 @@ if ( $type eq 'str8' && $borrower ) { if ($multi_hold) { my $bibinfo = $bibinfos{$biblionumber}; - AddReserve($branch,$borrower->{'borrowernumber'},$biblionumber,[$biblionumber], - $bibinfo->{rank},$startdate,$expirationdate,$notes,$bibinfo->{title},$checkitem,$found); + AddReserve($branch, $borrower->{'borrowernumber'}, $biblionumber, + [$biblionumber], $bibinfo->{rank}, $startdate, $expirationdate, + $notes, $bibinfo->{title}, $checkitem, $found, $itemtype, + $reserve_group_id); } else { # place a request on 1st available for ( my $i = 0 ; $i < $holds_to_place_count ; $i++ ) { --- a/reserve/request.pl +++ a/reserve/request.pl @@ -597,6 +597,7 @@ foreach my $biblionumber (@biblionumbers) { $reserve{itemtype} = $res->itemtype(); $reserve{branchcode} = $res->branchcode(); $reserve{object} = $res; + $reserve{'reserve_group_id'} = $res->reserve_group_id; push( @reserveloop, \%reserve ); } --- a/reserve/reserve-group.pl +++ a/reserve/reserve-group.pl @@ -0,0 +1,42 @@ +#!/usr/bin/env 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 . + +use Modern::Perl; +use CGI qw( -utf8 ); + +use C4::Auth; +use C4::Output; +use C4::Reserves; + +my $cgi = new CGI; +my ($template, $loggedinuser, $cookie) = get_template_and_user({ + template_name => "reserve/reserve-group.tt", + query => $cgi, + type => "intranet", + authnotrequired => 0, + flagsrequired => { circulate => 'circulate_remaining_permissions' }, +}); + +my $reserve_group_id = $cgi->param('reserve_group_id'); +my @reserves = GetReserveGroupReserves($reserve_group_id); + +$template->param( + reserve_group_id => $reserve_group_id, + reserves => \@reserves, +); + +output_html_with_http_headers $cgi, $cookie, $template->output; --- a/svc/holds +++ a/svc/holds @@ -102,6 +102,7 @@ while ( my $h = $holds_rs->next() ) { waiting_here => $h->branch()->branchcode() eq $branch, priority => $h->priority(), itemtype_limit => $itemtype_limit, + reserve_group_id => $h->reserve_group_id, subtitle => GetRecordValue( 'subtitle', GetMarcBiblio({ biblionumber => $biblionumber }), --