Bug 15516: Allow to reserve first available item from a group of titles

It can be useful, for instance, if a library has 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.

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. Search for a patron
3. Check the 'Treat as hold group' checkbox
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 hold group)' in Details column.
6. Click on the "hold group" link. A modal window should appear with
the content of the hold 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.
The other holds will also be removed from the holds queue.
9. Confirm tests pass
- t/db_dependent/Koha/Holds.t
- t/db_dependent/Reserves/HoldGroup.t
Note: the "hold group" link is displayed in the following pages:
- reserve/request.pl
- circ/circulation.pl
- members/moremember.pl
- circ/pendingreserves.pl
- circ/view_holdsqueue.pl
Note: A hold group is counted as only one hold

Sponsored-by: Catalyst IT

Signed-off-by: Michal Denar <black23@gmail.com>
Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>

Signed-off-by: Michal Denar <black23@gmail.com>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Aleisha Amohia 2022-08-04 15:20:59 +12:00 committed by Lucas Gass
parent 8bfe38b3ba
commit 559801d408
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
19 changed files with 183 additions and 24 deletions

View File

@ -598,7 +598,7 @@ 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(),
holds_count => $holds->count_holds,
WaitingHolds => $waiting_holds,
);

View File

@ -55,6 +55,16 @@ if ($run_report) {
locationslimit => $locationslimit
}
);
for my $item (@$items) {
my $related_hold = Koha::Holds->search(
{
borrowernumber => $item->{borrowernumber}, reservedate => $item->{reservedate},
biblionumber => $item->{biblionumber}
},
{ order_by => 'reservedate' }
)->next;
$item->{hold_group_id} = $related_hold ? $related_hold->hold_group_id : undef;
}
$template->param(
branchlimit => $branchlimit,

View File

@ -0,0 +1,14 @@
<div id="hold-group-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="hold-group-modal-label">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title" id="hold-group-modal-label">Hold group</h4>
</div>
<div class="modal-body"> </div>
<div class="modal-footer">
<button type="button" class="btn btn-default deny" data-dismiss="modal">Close</button>
</div> </div
><!-- /.modal-content --> </div
><!-- /.modal-dialog --> </div
><!-- /.modal -->

View File

@ -266,6 +266,9 @@
[%- IF hold.non_priority -%]
<br /><i>Non priority hold</i>
[%- END -%]
[%- IF hold.hold_group_id -%]
<div>(part of a <a href="/cgi-bin/koha/reserve/hold-group.pl?hold_group_id=[% hold.hold_group_id | uri %]" class="hold-group">hold group</a>)</div>
[%- END -%]
</td>
[%- IF ( CAN_user_reserveforothers_modify_holds_priority ) -%]
[%- UNLESS hold.found -%]

View File

@ -1078,6 +1078,8 @@
[% END %]
[% INCLUDE modals/cancel_booking.inc %]
[% INCLUDE 'hold-group-modal.inc' %]
[% MACRO jsinclude BLOCK %]
[% INCLUDE 'datatables.inc' %]
[% INCLUDE 'select2.inc' %]
@ -1117,6 +1119,7 @@
[% Asset.js("js/cancel_booking_modal.js") | $raw %]
[% Asset.js("js/combobox.js") | $raw %]
[% INCLUDE 'js-biblio-format.inc' %]
[% Asset.js("js/hold-group.js") | $raw %]
<script>
table_settings_issues_table = [% TablesSettings.GetTableSettings( 'circ', 'circulation', 'issues-table', 'json' ) | $raw %]
table_settings_relatives_issues_table = [% TablesSettings.GetTableSettings( 'circ', 'circulation', 'relatives-issues-table', 'json' ) | $raw %]

View File

@ -109,6 +109,9 @@
[% ELSE %]
[% IF ( biblio.biblioitem.publicationyear ) %]<p>[% biblio.biblioitem.publicationyear | html %]</p>[% END %]
[% END %]
[% IF ( hold.hold_group_id ) %]
<p>(part of a <a class="hold-group" href="/cgi-bin/koha/reserve/hold-group.pl?hold_group_id=[% hold.hold_group_id | html %]">hold group</a>)</p>
[% END %]
</td>
[% ELSE %]
<td>"</td>
@ -289,9 +292,12 @@
</div>
<!-- /.main.container-fluid -->
[% INCLUDE 'hold-group-modal.inc' %]
[% MACRO jsinclude BLOCK %]
[% INCLUDE 'calendar.inc' %]
[% INCLUDE 'datatables.inc' %]
[% Asset.js("js/hold-group.js") | $raw %]
<script>
function get_options(column){
let regex = /(<([^>]+)>)/ig; // Remove html tags

View File

@ -243,6 +243,9 @@
>
<em>or any available</em>
[% END %]
[% IF itemsloo.hold_group_id %]
<div>(part of a <a href="/cgi-bin/koha/reserve/hold-group.pl?hold_group_id=[% itemsloo.hold_group_id | uri %]" class="hold-group">hold group</a>)</div>
[% END %]
</td>
<td class="hq-patron">
<p> [% INCLUDE 'patron-title.inc' invert_name=1 patron=itemsloo.patron hide_patron_infos_if_needed=1 link_to="circulation_reserves" %] </p>
@ -345,8 +348,11 @@
</form>
[% END %]
[% INCLUDE 'hold-group-modal.inc' %]
[% MACRO jsinclude BLOCK %]
[% INCLUDE 'datatables.inc' %]
[% Asset.js("js/hold-group.js") | $raw %]
<script>
$(document).ready(function() {
var holdst;

View File

@ -757,6 +757,7 @@
[% END %]
[% INCLUDE modals/cancel_booking.inc %]
[% INCLUDE 'hold-group-modal.inc' %]
[% MACRO jsinclude BLOCK %]
[% INCLUDE 'datatables.inc' %]
@ -796,6 +797,7 @@
[% INCLUDE 'str/members-menu.inc' %]
[% Asset.js("js/members-menu.js") | $raw %]
[% Asset.js("js/recalls.js") | $raw %]
[% Asset.js("js/hold-group.js") | $raw %]
<script>
const LoadCheckoutsTableDelay = 0;
const AlwaysLoadCheckoutsTable = [% Koha.Preference('AlwaysLoadCheckoutsTable') | html %];

View File

@ -0,0 +1,31 @@
[% INCLUDE 'doc-head-open.inc' %]
<title>Holds group ([% hold_group.id | html %]) &rsaquo; Holds &rsaquo; Circulation &rsaquo; Koha</title>
[% INCLUDE 'doc-head-close.inc' %]
</head>
<body id="hold-group">
<h1>Holds group ([% hold_group.id | html %])</h1>
<div id="main">
[% holds = hold_group.holds %]
[% IF holds.count %]
<table>
<thead>
<tr>
<th>Title</th>
<th>Priority</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
[% FOREACH hold IN holds %]
[% biblio = hold.biblio %]
<tr>
<td><a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% biblio.biblionumber | uri %]">[% biblio.title | html %]</a></td>
<td>[% hold.priority | html %]</td>
<td>[% hold.reservenotes | html %]</td>
</tr>
[% END %]
</tbody>
</table>
[% END %]
[% INCLUDE 'intranet-bottom.inc' %]
</div>

View File

@ -619,6 +619,14 @@
<input name="non_priority" id="non_priority" type="checkbox" />
<span class="hint">A non priority hold doesn't prevent a current checkout from renewing</span>
</li>
[% IF ( Koha.Preference('DisplayAddHoldGroups') && ( multi_hold ) ) %]
<li id="hold_group_list_item">
<label for="hold_group">Treat as hold group: </label>
<input name="hold_group" id="hold_group" type="checkbox" />
<span class="hint">Place a hold on the next available title from this group</span>
</li>
[% END %]
</ol>
</fieldset>
<fieldset class="rows any_specific">
@ -632,7 +640,7 @@
[% ELSE %]
<input type="radio" id="requestany" name="request" checked="checked" value="Any" />
[% END %]
<label for="requestany" class="inline"> Hold next available item </label>
<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">
@ -668,7 +676,7 @@
[% END %]
[% UNLESS remaining_holds_for_record == 1 %]
<li>
<label for="holds_to_place_count">Holds to place (count)</label>
<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 %]
@ -1391,11 +1399,14 @@
<form id="hold-actions-form"> [% INCLUDE 'csrf-token.inc' %] </form>
</div>
[% INCLUDE 'hold-group-modal.inc' %]
[% MACRO jsinclude BLOCK %]
[% INCLUDE 'datatables.inc' %]
[% INCLUDE 'calendar.inc' %]
[% INCLUDE 'select2.inc' %]
[% Asset.js("js/holds.js") | $raw %]
[% Asset.js("js/hold-group.js") | $raw %]
[% SET url_biblio_params = "biblionumber=" _ biblionumbers.join("&amp;biblionumber=") %]
[% IF multi_hold %]

View File

@ -0,0 +1,8 @@
$(document).ready(function () {
$("body").on("click", "a.hold-group", function () {
var href = $(this).attr("href");
$("#hold-group-modal .modal-body").load(href + " #main");
$("#hold-group-modal").modal("show");
return false;
});
});

View File

@ -232,6 +232,17 @@ $(document).ready(function () {
"</span>";
}
if (oObj.hold_group_id) {
title += "<br>";
var link =
'<a class="hold-group" href="/cgi-bin/koha/reserve/hold-group.pl?hold_group_id=' +
oObj.hold_group_id +
'">' +
__("part of a hold group") +
"</a>";
title += "<span>(" + link + ")</span>";
}
return title;
},
},

View File

@ -210,7 +210,7 @@ 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(),
holds_count => $holds->count_holds,
WaitingHolds => $waiting_holds,
);
@ -294,7 +294,7 @@ my $patron_lists_count = $patron->get_lists_with_patron->count();
$template->param(
patron => $patron,
issuecount => $patron->checkouts->count,
holds_count => $patron->holds->count,
holds_count => $patron->holds->count_holds,
fines => $patron->account->balance,
translated_language => $translated_language,
detailview => 1,

View File

@ -162,7 +162,7 @@ $template->param( branch => $branch );
if ( $op eq 'cud-place_reserve' ) {
my $reserve_cnt = 0;
if ($maxreserves) {
$reserve_cnt = $patron->holds->count;
$reserve_cnt = $patron->holds->count_holds;
}
# List is composed of alternating biblio/item/branch

42
reserve/hold-group.pl Executable file
View File

@ -0,0 +1,42 @@
#!/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 qw( -utf8 );
use C4::Auth qw( get_template_and_user );
use C4::Output qw( output_html_with_http_headers );
use Koha::HoldGroups;
my $cgi = CGI->new;
my ( $template, $loggedinuser, $cookie, $flags ) = get_template_and_user(
{
template_name => "reserve/hold-group.tt",
query => $cgi,
type => "intranet",
flagsrequired => { circulate => 'circulate_remaining_permissions' },
}
);
my $reserve_group_id = $cgi->param('hold_group_id');
my $hold_group = Koha::HoldGroups->find($reserve_group_id);
$template->param(
hold_group => $hold_group,
);
output_html_with_http_headers $cgi, $cookie, $template->output;

View File

@ -29,27 +29,30 @@ use C4::Auth qw( checkauth );
use Koha::Items;
use Koha::Patrons;
use Koha::HoldGroup;
my $input = CGI->new();
checkauth( $input, 0, { reserveforothers => 'place_holds' }, 'intranet' );
my @reqbib = $input->multi_param('reqbib');
my @biblionumbers = $input->multi_param('biblionumber');
my @holdable_bibs = $input->multi_param('holdable_bibs');
my $borrowernumber = $input->param('borrowernumber');
my $notes = $input->param('notes');
my $branch = $input->param('pickup');
my $startdate = $input->param('reserve_date') || '';
my @rank = $input->multi_param('rank-request');
my $title = $input->param('title');
my @checkitems = $input->multi_param('checkitem');
my $item_group_id = $input->param('item_group_id');
my $expirationdate = $input->param('expiration_date');
my $itemtype = $input->param('itemtype') || undef;
my $non_priority = $input->param('non_priority');
my $op = $input->param('op') || q{};
my $multi_holds = $input->param('multi_holds');
my @reqbib = $input->multi_param('reqbib');
my @biblionumbers = $input->multi_param('biblionumber');
my @holdable_bibs = $input->multi_param('holdable_bibs');
my $borrowernumber = $input->param('borrowernumber');
my $notes = $input->param('notes');
my $branch = $input->param('pickup');
my $startdate = $input->param('reserve_date') || '';
my @rank = $input->multi_param('rank-request');
my $title = $input->param('title');
my @checkitems = $input->multi_param('checkitem');
my $item_group_id = $input->param('item_group_id');
my $expirationdate = $input->param('expiration_date');
my $itemtype = $input->param('itemtype') || undef;
my $non_priority = $input->param('non_priority');
my $op = $input->param('op') || q{};
my $multi_holds = $input->param('multi_holds');
my $hold_group_param = $input->param('hold_group') || undef;
my $hold_group;
my $patron = Koha::Patrons->find($borrowernumber);
@ -66,6 +69,10 @@ foreach my $bibnum (@holdable_bibs) {
if ( $op eq 'cud-placerequest' && $patron ) {
my %failed_holds;
if ($hold_group_param) {
$hold_group = Koha::HoldGroup->new->store;
}
foreach my $biblionumber ( keys %bibinfos ) {
my $can_override = C4::Context->preference('AllowHoldPolicyOverride');
@ -102,6 +109,7 @@ if ( $op eq 'cud-placerequest' && $patron ) {
found => undef,
itemtype => $itemtype,
non_priority => $non_priority,
hold_group_id => $hold_group ? $hold_group->id : undef,
}
);
@ -130,6 +138,7 @@ if ( $op eq 'cud-placerequest' && $patron ) {
found => undef,
itemtype => $itemtype,
non_priority => $non_priority,
hold_group_id => $hold_group ? $hold_group->id : undef,
}
);
}
@ -153,6 +162,7 @@ if ( $op eq 'cud-placerequest' && $patron ) {
itemtype => $itemtype,
non_priority => $non_priority,
item_group_id => $item_group_id,
hold_group_id => $hold_group ? $hold_group->id : undef,
}
);
}

View File

@ -189,7 +189,7 @@ if ( $borrowernumber_hold && !$op ) {
# we check the reserves of the user, and if they can reserve a document
# FIXME At this time we have a simple count of reservs, but, later, we could improve the infos "title" ...
my $reserves_count = $patron->holds->count;
my $reserves_count = $patron->holds->count_holds;
my $new_reserves_count = scalar(@biblionumbers);
@ -677,6 +677,7 @@ if ( ( $findborrower && $borrowernumber_hold || $findclub && $club_hold )
$reserve{branchcode} = $res->branchcode();
$reserve{non_priority} = $res->non_priority();
$reserve{object} = $res;
$reserve{hold_group_id} = $res->hold_group_id;
if ( $holds_count_per_patron->{ $reserve{'borrowernumber'} } == 1 ) {
$reserve{'change_hold_type_allowed'} = 1;

View File

@ -113,6 +113,7 @@ while ( my $h = $holds_rs->next() ) {
priority => $h->priority(),
reservenotes => $h->reservenotes(),
itemtype_limit => $itemtype_limit,
hold_group_id => $h->hold_group_id,
reservedate_formatted => $h->reservedate()
? output_pref( { dt => dt_from_string( $h->reservedate() ), dateonly => 1 } )
: q{},

View File

@ -20,7 +20,7 @@
use Modern::Perl;
use Test::NoWarnings;
use Test::More tests => 13;
use Test::More tests => 14;
use Test::Warn;
use C4::Circulation qw( AddIssue );