From ec7dfc6f5d639b9629aec36ff8cc3f923232e7c2 Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Thu, 7 Jan 2016 14:14:18 +0100 Subject: [PATCH] Bug 15516: Allow to reserve first available item from a group of titles It can be useful, for instance, if a library have the same title from different publishers (so 1 title but several biblio records) but the user only wants a copy of the book, regardless of the publisher. This patch is only for staff interface. Test plan: 0. Run updatedatabase.pl and misc/devel/update_dbix_class_files.pl 1. Go to intranet search, display some results, click on some checkboxes and click on 'Place hold' button at the top of the results. 2. Enter a patron cardnumber and click Search 3. Check the box for 'Place a hold on the next available item' (it's usually hidden when placing a hold on multiple biblios) 4. Click on 'Place hold' 5. In the next screen you should see all the holds you placed with the additional text '(part of a reserve group)' in Details column. 6. Click on the "reserve group" link. A modal window should appear with the content of the reserve group (a list of holds) 7. Check in an item of one of the reserved biblios and confirm the hold 8. The hold status is changed to Waiting, and all other holds are deleted. Note: the "reserve group" link is displayed in the following pages: - reserve/request.pl - circ/circulation.pl - members/moremember.pl - circ/pendingreserves.pl Signed-off-by: Josef Moravec Signed-off-by: Benjamin Rokseth --- C4/Reserves.pm | 73 +++++++++++++++++++++- Koha/Template/Plugin/Biblio.pm | 8 +++ circ/pendingreserves.pl | 4 +- installer/data/mysql/atomicupdate/bug_15516.sql | 8 +++ installer/data/mysql/kohastructure.sql | 14 ++++- .../prog/en/includes/reserve-group-modal.inc | 27 ++++++++ .../intranet-tmpl/prog/en/includes/strings.inc | 2 + .../prog/en/modules/circ/circulation.tt | 1 + .../prog/en/modules/circ/pendingreserves.tt | 15 +++-- .../prog/en/modules/members/moremember.tt | 1 + .../prog/en/modules/reserve/request.tt | 18 ++++-- .../prog/en/modules/reserve/reserve-group.tt | 30 +++++++++ koha-tmpl/intranet-tmpl/prog/js/holds.js | 6 ++ reserve/placerequest.pl | 12 +++- reserve/request.pl | 1 + reserve/reserve-group.pl | 42 +++++++++++++ svc/holds | 1 + 17 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/bug_15516.sql create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/reserve-group-modal.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/reserve/reserve-group.tt create mode 100644 reserve/reserve-group.pl diff --git a/C4/Reserves.pm b/C4/Reserves.pm index 3f124a630d..a1d654383b 100644 --- a/C4/Reserves.pm +++ b/C4/Reserves.pm @@ -148,6 +148,10 @@ BEGIN { IsItemOnHoldAndFound GetMaxPatronHoldsForRecord + + &AddReserveGroup + &DeleteReserveGroup + &GetReserveGroupReserves ); @EXPORT_OK = qw( MergeHolds ); } @@ -172,7 +176,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' }) @@ -210,6 +215,7 @@ sub AddReserve { waitingdate => $waitingdate, expirationdate => $expdate, itemtype => $itemtype, + reserve_group_id => $reserve_group_id, } )->store(); @@ -1346,6 +1352,16 @@ sub ModReserveAffect { } $hold->store(); + 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 ); @@ -1428,6 +1444,7 @@ sub GetReserveInfo { reminderdate, priority, found, + reserve_group_id, firstname, surname, phone, @@ -2532,6 +2549,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 diff --git a/Koha/Template/Plugin/Biblio.pm b/Koha/Template/Plugin/Biblio.pm index e9d7a73009..ab6a716a0e 100644 --- a/Koha/Template/Plugin/Biblio.pm +++ b/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; diff --git a/circ/pendingreserves.pl b/circ/pendingreserves.pl index e9cee0c229..80def7e1d6 100755 --- a/circ/pendingreserves.pl +++ b/circ/pendingreserves.pl @@ -119,6 +119,7 @@ if ( $run_report ) { reminderdate, max(priority) as priority, reserves.found, + reserves.reserve_group_id, biblio.title, biblio.author, count(DISTINCT items.itemnumber) as icount, @@ -173,7 +174,8 @@ if ( $run_report ) { biblionumber => $data->{biblionumber}, statusw => ( $data->{found} eq "W" ), statusf => ( $data->{found} eq "F" ), - holdingbranches => [split('\|', $data->{l_holdingbranch})],, + reserve_group_id => $data->{reserve_group_id}, + holdingbranches => [split('\|', $data->{l_holdingbranch})], branch => $data->{l_branch}, itemcallnumber => $data->{l_itemcallnumber}, enumchron => $data->{l_enumchron}, diff --git a/installer/data/mysql/atomicupdate/bug_15516.sql b/installer/data/mysql/atomicupdate/bug_15516.sql new file mode 100644 index 0000000000..60158bfbb5 --- /dev/null +++ b/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; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 2533e9d66e..f893ac8268 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -1883,6 +1883,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`), @@ -1894,7 +1895,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=utf8 COLLATE=utf8_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=utf8 COLLATE=utf8_unicode_ci; -- diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/reserve-group-modal.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/reserve-group-modal.inc new file mode 100644 index 0000000000..2a3e68b157 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/reserve-group-modal.inc @@ -0,0 +1,27 @@ + + + diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/strings.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/strings.inc index 7483ccf161..caa48fbef0 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/strings.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/strings.inc @@ -41,5 +41,7 @@ var SUSPEND_HOLD_ERROR_NOT_FOUND = _("Unable to suspend hold, hold not found"); var RESUME_HOLD_ERROR_NOT_FOUND = _("Unable to resume, hold not found"); var CURRENT = _(" (current) "); + var PART_OF_A = _("(part of a %s)"); + var RESERVE_GROUP = _("reserve group"); //]]> diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt index af88d33e4f..37740b558f 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation.tt @@ -1022,4 +1022,5 @@ No patron matched [% message | html %] +[% INCLUDE 'reserve-group-modal.inc' %] [% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt index 11c7a0fbf4..8d1e61bc13 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/pendingreserves.tt @@ -112,10 +112,16 @@ $(document).ready(function() { [% reserveloo.count %] [% reserveloo.rcount %] -

- [% INCLUDE 'biblio-default-view.inc' biblionumber = reserveloo.biblionumber %] - [% reserveloo.title |html %] [% FOREACH s IN reserveloo.subtitle %] [% s %][% END %]

- [% IF ( reserveloo.author ) %]

by [% reserveloo.author %]

[% END %] +

+ [% INCLUDE 'biblio-default-view.inc' biblionumber = reserveloo.biblionumber %] + [% reserveloo.title |html %] [% FOREACH s IN reserveloo.subtitle %] [% s %][% END %] +

+ [% IF ( reserveloo.author ) %] +

by [% reserveloo.author %]

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

(part of a reserve group)

+ [% END %] [% ELSE %] " @@ -199,4 +205,5 @@ $(document).ready(function() { +[% INCLUDE 'reserve-group-modal.inc' %] [% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt index 271b30bc03..13f18c4c34 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt @@ -624,4 +624,5 @@ function validate1(date) { [% INCLUDE 'circ-menu.inc' %] +[% INCLUDE 'reserve-group-modal.inc' %] [% INCLUDE 'intranet-bottom.inc' %] 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 12b4e1b790..ef6d10d06d 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt @@ -406,20 +406,24 @@ function checkMultiHold() { 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 %] [% SET count = 1 %]
  • @@ -840,6 +844,9 @@ function checkMultiHold() { [% END %] [% END %] + [% IF reserveloo.reserve_group_id %] +
    (part of a reserve group)
    + [% END %] [% IF ( CAN_user_reserveforothers_modify_holds_priority ) %] @@ -902,4 +909,7 @@ function checkMultiHold() { + +[% INCLUDE 'reserve-group-modal.inc' %] + [% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/reserve-group.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/reserve-group.tt new file mode 100644 index 0000000000..8926c0a4ca --- /dev/null +++ b/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' %] diff --git a/koha-tmpl/intranet-tmpl/prog/js/holds.js b/koha-tmpl/intranet-tmpl/prog/js/holds.js index 46c826cecc..8ce905eea4 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/holds.js +++ b/koha-tmpl/intranet-tmpl/prog/js/holds.js @@ -42,6 +42,12 @@ $(document).ready(function() { title += " - " + oObj.itemnotes + "" } + if (oObj.reserve_group_id) { + title += '
    '; + var link = '' + RESERVE_GROUP +'' + title += '' + PART_OF_A.format(link) + ''; + } + return title; } }, diff --git a/reserve/placerequest.pl b/reserve/placerequest.pl index 92f3c48b01..c25301a25e 100755 --- a/reserve/placerequest.pl +++ b/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 = GetMember( 'borrowernumber' => $borrowernumber ); @@ -83,6 +84,11 @@ if (defined $checkitem && $checkitem ne ''){ 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; @@ -106,8 +112,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++ ) { diff --git a/reserve/request.pl b/reserve/request.pl index 3f4dba3c12..3889261458 100755 --- a/reserve/request.pl +++ b/reserve/request.pl @@ -581,6 +581,7 @@ foreach my $biblionumber (@biblionumbers) { $reserve{'reserve_id'} = $res->reserve_id(); $reserve{itemtype} = $res->itemtype(); $reserve{branchcode} = $res->branchcode(); + $reserve{'reserve_group_id'} = $res->reserve_group_id; push( @reserveloop, \%reserve ); } diff --git a/reserve/reserve-group.pl b/reserve/reserve-group.pl new file mode 100644 index 0000000000..91ed958538 --- /dev/null +++ b/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; diff --git a/svc/holds b/svc/holds index 3c3a2b0f69..7700a53109 100755 --- a/svc/holds +++ b/svc/holds @@ -101,6 +101,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), GetFrameworkCode($biblionumber) -- 2.11.0