@@ -, +, @@ of titles --- a/C4/Reserves.pm +++ a/C4/Reserves.pm @@ -137,6 +137,10 @@ BEGIN { IsItemOnHoldAndFound GetMaxPatronHoldsForRecord + + &AddReserveGroup + &DeleteReserveGroup + &GetReserveGroupReserves ); @EXPORT_OK = qw( MergeHolds ); } @@ -162,7 +166,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' }) @@ -211,6 +216,7 @@ sub AddReserve { expirationdate => $expdate, itemtype => $itemtype, item_level_hold => $checkitem ? 1 : 0, + reserve_group_id => $reserve_group_id, } )->store(); $hold->set_waiting() if $found eq 'W'; @@ -1075,6 +1081,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 ); @@ -2174,6 +2190,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.subtitle, biblio.medium, @@ -268,6 +269,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 @@ -1817,6 +1817,7 @@ CREATE TABLE `reserves` ( -- information related to holds/reserves in Koha `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 `item_level_hold` BOOLEAN NOT NULL DEFAULT 0, -- Is the hpld placed at item level + `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`), @@ -1828,7 +1829,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 @@ -164,6 +164,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/modules/circ/circulation.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt @@ -1106,4 +1106,5 @@ [% 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-title.inc' biblio=reserveloo %]

[% IF ( reserveloo.author ) %]

by [% reserveloo.author | html %]

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

(part of a reserve group)

+ [% END %] [% ELSE %] " @@ -304,4 +307,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 @@ -957,4 +957,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 @@ -259,21 +259,25 @@ 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 %]
  • @@ -971,4 +975,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 @@ -44,6 +44,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 @@ -52,6 +52,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; @@ -74,6 +75,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; @@ -109,7 +115,8 @@ if ( $type eq 'str8' && $borrower ) { my $bibinfo = $bibinfos{$biblionumber}; if ( $can_override || CanBookBeReserved($borrower->{'borrowernumber'}, $biblionumber)->{status} eq 'OK' ) { AddReserve($branch,$borrower->{'borrowernumber'},$biblionumber,[$biblionumber], - $bibinfo->{rank},$startdate,$expirationdate,$notes,$bibinfo->{title},$checkitem,$found); + $bibinfo->{rank},$startdate,$expirationdate,$notes,$bibinfo->{title},$checkitem,$found, + $itemtype, $reserve_group_id); } } else { # place a request on 1st available --- a/reserve/request.pl +++ a/reserve/request.pl @@ -609,6 +609,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 @@ -108,6 +108,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, reservedate_formatted => $h->reservedate() ? output_pref( { dt => dt_from_string( $h->reservedate() ), dateonly => 1 } ) --