Bugzilla – Attachment 183306 Details for
Bug 36135
Add tool to batch modify holds
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 36135: Add ability to batch modify holds
Bug-36135-Add-ability-to-batch-modify-holds.patch (text/plain), 56.22 KB, created by
Lucas Gass (lukeg)
on 2025-06-17 14:38:43 UTC
(
hide
)
Description:
Bug 36135: Add ability to batch modify holds
Filename:
MIME Type:
Creator:
Lucas Gass (lukeg)
Created:
2025-06-17 14:38:43 UTC
Size:
56.22 KB
patch
obsolete
>From 6da87e4b1d03b8195f8483bfa04925354707b243 Mon Sep 17 00:00:00 2001 >From: Lucas Gass <lucas@bywatersolutions.com> >Date: Tue, 17 Jun 2025 13:57:49 +0000 >Subject: [PATCH] Bug 36135: Add ability to batch modify holds > >TO TEST: > >0. APPLY PATCHES, updatedatabase, restart_all >1. Create a user with limited permissions, making sure you give them the "batch_modify_holds" permission >2. Login as chosen patron. >3. Navigate to Tools page. >4. Note there's a link "Batch modify holds". >5. Remove permission from user. >6. Link to new tool is not displayed. > >7. Navigate to new tool Tools->Batch modify holds. >8. Fill in some criteria to search holds with. >9. Confirm that holds found match your search criteria. >10. Set e.g. new expiration date for all or just selected holds. >11. Confirm that holds were modified as they should have. >12. Confirm this also from database. > >13. Find holds with a found/waiting status. >14. Attempt to modify their suspend status. >15. Alert box with text "One or more holds have found status and can't be suspended." should be displayed. >16. Play around, test different modifications. > >17. Create SQL report which contains column for reserve_id. >18. e.g "SELECT reserve_id FROM reserves limit 10". >19.. Run report. >20. Over report select "Batch operations" should be displayed and under that there should be option "Batch modify holds". >21. Click "Batch modify holds" option. >22. Confirm that holds displayed in "Batch mofidy holds" tool are same as found with your report. > >23. Do a holds search that will bring up multiple pages of results >24. Try selecting some holds from different pages to modify >25. Ensure all selected holds are being modified >26. Play around, test different modifications. > >Sponsored-by: Koha-Suomi Oy >--- > admin/columns_settings.yml | 47 +- > api/v1/swagger/definitions/hold.yaml | 5 + > api/v1/swagger/paths/holds.yaml | 2 + > .../modals/batch_modify_hold_errors.inc | 17 + > .../prog/en/includes/tools-menu.inc | 3 + > .../modules/reports/guided_reports_start.tt | 32 +- > .../en/modules/tools/batch_modify_holds.tt | 716 ++++++++++++++++++ > .../prog/en/modules/tools/tools-home.tt | 7 +- > tools/batch_modify_holds.pl | 107 +++ > 9 files changed, 930 insertions(+), 6 deletions(-) > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/modals/batch_modify_hold_errors.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_modify_holds.tt > create mode 100755 tools/batch_modify_holds.pl > >diff --git a/admin/columns_settings.yml b/admin/columns_settings.yml >index f4820008150..54f269f3b9c 100644 >--- a/admin/columns_settings.yml >+++ b/admin/columns_settings.yml >@@ -1987,7 +1987,52 @@ modules: > columnname: suspend > - > columnname: print_slip >- >+ holds_to_modify: >+ default_display_length: 20 >+ columns: >+ - >+ columnname: checkbox >+ cannot_be_toggled: 1 >+ cannot_be_modified: 1 >+ - >+ columnname: expiration_date >+ - >+ columnname: title >+ - >+ columnname: barcode >+ - >+ columnname: patron >+ - >+ columnname: status >+ - >+ columnname: pickup_library >+ - >+ columnname: suspended >+ - >+ columnname: suspended_until >+ - >+ columnname: notes >+ modified_holds: >+ default_display_length: 20 >+ columns: >+ - >+ columnname: expiration_date >+ - >+ columnname: title >+ - >+ columnname: barcode >+ - >+ columnname: patron >+ - >+ columnname: status >+ - >+ columnname: pickup_library >+ - >+ columnname: suspended >+ - >+ columnname: suspended_until >+ - >+ columnname: notes > holdsratios: > holds-ratios: > default_display_length: 20 >diff --git a/api/v1/swagger/definitions/hold.yaml b/api/v1/swagger/definitions/hold.yaml >index 810f2647db0..8e6b6e219e4 100644 >--- a/api/v1/swagger/definitions/hold.yaml >+++ b/api/v1/swagger/definitions/hold.yaml >@@ -135,5 +135,10 @@ properties: > - object > - "null" > description: Pickup library >+ patron: >+ type: >+ - object >+ - "null" >+ description: Patron for hold > > additionalProperties: false >diff --git a/api/v1/swagger/paths/holds.yaml b/api/v1/swagger/paths/holds.yaml >index cac66270182..310af48bf03 100644 >--- a/api/v1/swagger/paths/holds.yaml >+++ b/api/v1/swagger/paths/holds.yaml >@@ -105,6 +105,8 @@ > - biblio > - deleted_biblio > - pickup_library >+ - item >+ - patron > collectionFormat: csv > produces: > - application/json >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/batch_modify_hold_errors.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/batch_modify_hold_errors.inc >new file mode 100644 >index 00000000000..ed9d1526228 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/batch_modify_hold_errors.inc >@@ -0,0 +1,17 @@ >+<!-- Batch modify hold error modal --> >+<div class="modal fade" id="errorModal" tabindex="-1" role="dialog" aria-labelledby="errorModalLabel"> >+ <div class="modal-dialog"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <h1 class="modal-title" id="errorModalLabel">Errors</h1> >+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> >+ </div> >+ <div class="modal-body"> >+ <ul id="errorList"></ul> >+ </div> >+ <div class="modal-footer"> >+ <button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button> >+ </div> >+ </div> >+ </div> >+</div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >index 28cd71f6b7c..15bdd2fc970 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc >@@ -39,6 +39,9 @@ > [% IF ( CAN_user_tools_batch_extend_due_dates ) %] > <li><a href="/cgi-bin/koha/tools/batch_extend_due_dates.pl">Batch extend due dates</a></li> > [% END %] >+ [% IF ( CAN_user_tools_batch_modify_holds ) %] >+ <li><a href="/cgi-bin/koha/tools/batch_modify_holds.pl">Batch modify holds</a></li> >+ [% END %] > [% IF ( CAN_user_tools_moderate_tags ) %] > <li><a href="/cgi-bin/koha/tags/review.pl">Tags</a></li> > [% END %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt >index c61553b88b3..76221287328 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reports/guided_reports_start.tt >@@ -1071,6 +1071,16 @@ > > > </form> > >+ <form method="post" action="/cgi-bin/koha/tools/batch_modify_holds.pl" id="batch_modify_holds" target="_blank"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <input type="hidden" name="op" value="cud-form" /> >+ <textarea style="display:none" name="reserve_ids_list" id="reserve_ids_list"> >+ [%# Preserve the whitespace of the following textarea in order to format the values correctly %] >+ [%- reserve_ids = PROCESS batch_list results=results batch_type='reserve_id' | trim | html %][% IF reserve_ids %][% SET batch_reserve_ids = 1 %][% reserve_ids | html %][% END -%] >+ </textarea >+ > >+ </form> >+ > [% BLOCK batch_list %] > [%- FOREACH result IN results %] > [%- FOREACH cells IN result.cells %] >@@ -1095,9 +1105,9 @@ > </form> > <!-- /#limitselect --> > >- [% IF ( batch_biblionumbers || batch_itemnumbers || batch_cardnumbers || batch_borrowernumbers ) || ( unlimited_total > 10 && limit <= 1000 ) %] >+ [% IF ( batch_biblionumbers || batch_itemnumbers || batch_cardnumbers || batch_borrowernumbers || batch_reserve_ids ) || ( unlimited_total > 10 && limit <= 1000 ) %] > <div id="toolbar" class="btn-toolbar"> >- [% IF ( batch_biblionumbers || batch_itemnumbers || batch_cardnumbers || batch_borrowernumbers ) %] >+ [% IF ( batch_biblionumbers || batch_itemnumbers || batch_cardnumbers || batch_borrowernumbers || batch_reserve_ids ) %] > <div class="btn-group"> > <button class="btn btn-default btn-sm dropdown-toggle" type="button" id="batch_mod_menu" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> > Batch operations with [% IF unlimited_total >= limit %][% results.size | html %][% ELSE %][% unlimited_total | html %][% END %] visible records >@@ -1205,12 +1215,26 @@ > > > </li> > [% END %] >+ [% IF header_ro.cell == 'reserve_id' || header_types.item( header_ro.cell ) == 'reserve_id' %] >+ <li class="dropdown-header">Hold records</li> >+ <li> >+ <a >+ href="#" >+ data-submit="batch_modify_holds" >+ data-toggle="tooltip" >+ data-placement="right" >+ title="Send visible results to batch hold modification" >+ class="batch_op send_to_hold_mod dropdown-item" >+ >Batch hold modification</a >+ > >+ </li> >+ [% END %] > [% END # /FOREACH header_ro %] > </ul> > <!-- /.dropdown-menu --> > </div> > <!-- /.dropdown --> >- [% END # /IF ( batch_biblionumbers || batch_itemnumbers || batch_cardnumbers ) %] >+ [% END # /IF ( batch_biblionumbers || batch_itemnumbers || batch_cardnumbers || batch_reserve_ids ) %] > > [% IF ( unlimited_total > 10 && limit <= 1000 ) %] > <div class="btn-group"> >@@ -1238,7 +1262,7 @@ > </div> > [% END # /IF ( unlimited_total > 10 && limit <= 1000 ) %] > >- [% IF ( batch_biblionumbers || batch_itemnumbers || batch_cardnumbers ) %] >+ [% IF ( batch_biblionumbers || batch_itemnumbers || batch_cardnumbers || batch_reserve_ids ) %] > <a href="#" class="btn btn-link" id="toggle_auto_links"> > <i class="fa-solid fa-eye autolink" aria-hidden="true" style="display:none"></i> > <i class="fa-solid fa-eye-slash autolink" aria-hidden="true"></i> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_modify_holds.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_modify_holds.tt >new file mode 100644 >index 00000000000..b0fe375af1b >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_modify_holds.tt >@@ -0,0 +1,716 @@ >+[% USE raw %] >+[% USE Asset %] >+[% SET footerjs = 1 %] >+[% USE Branches %] >+[% USE Categories %] >+[% USE KohaDates %] >+[% USE ItemTypes %] >+[% USE TablesSettings %] >+[% USE To %] >+[% PROCESS 'html_helpers.inc' %] >+ >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Batch modify holds › Tools › Koha</title> >+[% INCLUDE 'doc-head-close.inc' %] >+[% Asset.css("css/humanmsg.css") | $raw %] >+</head> >+ >+<body id="tools_batch_extend_due_dates" class="tools"> >+[% WRAPPER 'header.inc' %] >+ [% INCLUDE 'cat-search.inc' %] >+ [% INCLUDE modals/batch_modify_hold_errors.inc %] >+[% END %] >+[% WRAPPER 'sub-header.inc' %] >+ [% WRAPPER breadcrumbs %] >+ [% WRAPPER breadcrumb_item %] >+ <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> >+ [% END %] >+ [% IF ( view == 'form' || view == 'report' ) %] >+ [% WRAPPER breadcrumb_item %] >+ <a href="/cgi-bin/koha/tools/batch_modify_holds.pl">Batch modify holds</a> >+ [% END %] >+ [% ELSE %] >+ [% WRAPPER breadcrumb_item bc_active= 1 %] >+ <span>Batch modify holds</span> >+ [% END %] >+ [% END %] >+ [% END #/ WRAPPER breadcrumbs %] >+[% END #/ WRAPPER sub-header.inc %] >+ >+<div class="main container-fluid"> >+ <div class="row"> >+ <div class="col-sm-10 col-sm-push-2 order-sm-1"> >+ <main> >+ <h1>Batch modify holds</h1> >+ [% IF view == "cud-form" %] >+ <div id="modify_holds_search"> >+ <form method="get" action="/cgi-bin/koha/tools/batch_modify_holds.pl" id="modify_holds_form"> >+ <fieldset class="rows"> >+ <legend>Hold search criteria:</legend> >+ <ol> >+ <li> >+ <label for="expirationdate_from">Expiration date from:</label> >+ <input type="text" size="10" id="expirationdate_from" name="expirationdate_from" class="flatpickr" /> >+ </li> >+ <li> >+ <label for="expirationdate_to">Expiration date to:</label> >+ <input type="text" size="10" id="expirationdate_to" name="expirationdate_to" class="flatpickr" /> >+ </li> >+ <li> >+ <label for="branchcodes">Libraries:</label> >+ <select name="branchcodes" id="branchcodes" multiple="multiple"> >+ [% PROCESS options_for_libraries libraries => Branches.all(selected => "") %] >+ </select> >+ </li> >+ <li> >+ <label for="found_status">Found status:</label> >+ <select name="found_status" id="found_status" multiple="multiple"> >+ <option value="NULL">No status</option> >+ <option value="T">In transit</option> >+ <option value="P">In processing</option> >+ <option value="W">Waiting</option> >+ </select> >+ </li> >+ <li> >+ <label for="suspend_status">Suspended:</label> >+ <select name="suspend_status" id="suspend_status"> >+ <option value="none" selected="selected"></option> >+ <option value="0">Not suspended</option> >+ <option value="1">Suspended</option> >+ </select> >+ </li> >+ <li> >+ <label for="suspend_until_from">Suspended until from:</label> >+ <input type="text" size="10" id="suspend_until_from" name="suspend_until_from" class="flatpickr" data-date_to="suspend_until_to" /> >+ </li> >+ <li> >+ <label for="suspend_until_to">Suspended until to:</label> >+ <input type="text" size="10" id="suspend_until_to" name="suspend_until_to" class="flatpickr" /> >+ </li> >+ <li> >+ <label for="holdnotes">Hold note:</label> >+ <input type="text" id="holdnotes" name="holdnotes" /> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="submit" class="btn btn-primary" value="Search" /> >+ </fieldset> >+ </form> >+ <!-- /#modify_holds_form --> >+ </div> >+ <!-- /#modify_holds_search --> >+ [% ELSIF view == "report" %] >+ <div id="modified_holds_results-wrapper"> >+ <div id="modified_holds_results" class="page-section"> >+ <table id="modified_holds"></table> >+ </div> >+ <!-- /#modified_holds_results --> >+ <fieldset class="action"> >+ <a href="/cgi-bin/koha/tools/batch_modify_holds.pl">Return to batch hold modification</a> >+ </fieldset> >+ </div> >+ <!-- /#modified_holds_results-wrapper --> >+ [% END %] >+ <div id="modify_holds_results-wrapper" style="display:none;"> >+ <div id="toolbar" class="btn-toolbar"> >+ <a href="#" id="edit_search" class="btn btn-default"><i class="fa-solid fa-pencil" aria-hidden="true"></i> Edit search</a> >+ </div> >+ <!-- /#toolbar --> >+ <form action="/cgi-bin/koha/tools/batch_modify_holds.pl" method="post" id="process_mods"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <div id="modify_holds_results" class="page-section"> >+ <div id="searchheader" class="searchheader"> >+ <a id="select_all" href="#" class="btn btn-link"><i class="fa fa-check"></i> Select all visible rows</a> >+ | <a id="clear_all" href="#" class="btn btn-link"><i class="fa fa-times"></i> Clear selections</a> >+ <div id="table_search_selections" class="btn-group" style="display:none;"> >+ <span></span> >+ <a href="#" id="clear-row-selection"><i class="fa fa-times"></i> Clear</a> >+ </div> >+ </div> >+ <!-- /#searchheader --> >+ <h3>Holds found for: <span class="searchpattern"></span></h3> >+ <table id="holds_to_modify"></table> >+ </div> >+ <!-- /#modify_holds_results --> >+ <div id="modify_holds_form_options" class="page-section"> >+ <h2>Modify holds</h2> >+ <table id="modify_holds_options"> >+ <thead> >+ <tr> >+ <th>New expiration date</th> >+ <th>New pickup library</th> >+ <th>Suspend holds</th> >+ <th>Suspend until</th> >+ <th>New hold note</th> >+ <th>Clear hold notes</th> >+ </tr> >+ </thead> >+ <tbody> >+ <tr> >+ <td> >+ <input type="text" id="new_expiration_date" name="new_expiration_date" class="flatpickr" data-flatpickr-futuredate="true" /> >+ </td> >+ <td> >+ <select name="new_pickup_loc" id="new_pickup_loc"> >+ <option value="" selected="selected"></option> >+ [% PROCESS options_for_libraries libraries => Branches.pickup_locations({ selected => "" }) %] >+ </select> >+ </td> >+ <td> >+ <select id="new_suspend_status" name="new_suspend_status"> >+ <option value="" selected="selected"></option> >+ <option value="0">Not suspended</option> >+ <option value="1">Suspend</option> >+ </select> >+ </td> >+ <td> >+ <input type="text" id="new_suspend_date" name="new_suspend_date" class="flatpickr" data-flatpickr-futuredate="true" /> >+ </td> >+ <td> >+ <input type="text" id="new_hold_note" name="new_hold_note" /> >+ </td> >+ <td> >+ <input class="selection" type="checkbox" id="clear_hold_notes" name="clear_hold_notes" value="1" /> >+ </td> >+ </tr> >+ </tbody> >+ </table> >+ <!-- /#modify_holds_options --> >+ </div> >+ <!-- /#modify_holds_options --> >+ <fieldset class="action"> >+ <input type="hidden" name="op" value="cud-modify" /> >+ <input type="submit" class="btn btn-primary" value="Modify holds" /> >+ <a class="cancel" href="/cgi-bin/koha/tools/batch_modify_holds.pl">Cancel</a> >+ </fieldset> >+ </form> >+ <!-- /#process_mods --> >+ </div> >+ <!-- /#modify_holds_results-wrapper --> >+ </main> >+ </div> >+ <!-- /.col-sm-10 col-sm-push-2 --> >+ <div class="col-sm-2 col-sm-pull-10"> >+ <aside> [% INCLUDE 'tools-menu.inc' %] </aside> >+ </div> >+ <!-- /.col-sm-2 col-sm-pull-10 --> >+ </div> >+ [% MACRO jsinclude BLOCK %] >+ [% Asset.js("js/tools-menu.js") | $raw %] >+ [% INCLUDE 'calendar.inc' %] >+ [% INCLUDE 'datatables.inc' %] >+ [% INCLUDE 'select2.inc' %] >+ [% INCLUDE 'js-patron-format.inc' %] >+ [% Asset.js("lib/jquery/plugins/humanmsg.js") | $raw %] >+ <script> >+ let hide_patron_name = [% IF Koha.Preference('HidePatronName') %]true[% ELSE %]false[% END %]; >+ let hold_ids = [% IF updated_holds %] [% updated_holds | $raw %] [% ELSE %] [] [% END %]; >+ let hold_ids_from_sql = [% IF reserve_ids_list %] [% reserve_ids_list | $raw %] [% ELSE %] [] [% END %]; >+ >+ $(document).ready(function() { >+ >+ // Apply select2 to all select fields having a "multiple" attribute >+ let selectFields = document.querySelectorAll('select[multiple]'); >+ selectFields.forEach((selectField) => { >+ selectField.style.minWidth = '200px'; >+ $(selectField).select2(); >+ }); >+ >+ //Empty local storage from previous selections >+ localStorage.removeItem("holds_modify_selections"); >+ >+ $("#holds_to_modify").on("change", "input[type='checkbox']", function(){ >+ var holds_modify_selections = JSON.parse(localStorage.getItem("holds_modify_selections")) || []; >+ var hold_id = $(this).val(); >+ if ($(this).prop("checked")) { >+ >+ hold_data = { >+ hold_id: hold_id, >+ found_status: $(this).closest('tr').find('.found_status').children('span').data('found-status'), >+ suspended: $(this).closest('tr').find('.suspended').children('span').data('suspended'), >+ } >+ >+ holds_modify_selections.push(hold_data); >+ localStorage.setItem("holds_modify_selections", JSON.stringify(holds_modify_selections)); >+ showHoldSelections(holds_modify_selections.length); >+ } else { >+ var filtered = holds_modify_selections.filter(function( hold_obj ){ >+ return hold_obj.hold_id !== hold_id; >+ }); >+ if( filtered.length > 0 ){ >+ localStorage.setItem("holds_modify_selections", JSON.stringify( filtered )); >+ holds_modify_selections = filtered; >+ showHoldSelections( filtered.length ); >+ } else { >+ holds_modify_selections = []; >+ localStorage.removeItem("holds_modify_selections"); >+ showHoldSelections( 0 ); >+ } >+ } >+ }); >+ >+ $("#select_all").on("click", function(e){ >+ e.preventDefault(); >+ $("#holds_to_modify input[type='checkbox']").each(function(){ >+ $(this).prop("checked", true).change(); >+ }); >+ }); >+ >+ $("#clear_all").on("click", function(e){ >+ e.preventDefault(); >+ $("#holds_to_modify input[type='checkbox']").each(function(){ >+ $(this).prop("checked", false).change(); >+ }); >+ }); >+ >+ $("#clear-row-selection").on("click", function (e) { >+ e.preventDefault(); >+ $("input[type='checkbox']").prop("checked", false).change(); >+ localStorage.removeItem("holds_modify_selections"); >+ $("#table_search_selections").hide(); >+ }); >+ >+ $('#modify_holds_form').submit(function() { >+ var modify_holds_form = $(this); >+ search_holds(modify_holds_form); >+ $("#modify_holds_search").hide(); >+ $('#edit_search').show(); >+ return false; >+ }); >+ >+ if(hold_ids_from_sql.length > 0){ >+ $('#modify_holds_form').trigger('submit'); >+ } >+ >+ $("#edit_search").on("click", function(e){ >+ e.preventDefault(); >+ $('#modify_holds_search').show(); >+ $('#edit_search').hide(); >+ }); >+ >+ function showHoldSelections( number ){ >+ if (number === 0) { >+ $("#table_search_selections").hide(); >+ } else { >+ $("#table_search_selections") >+ .show() >+ .find("span") >+ .text(__("Holds selected: %s").format(number)); >+ } >+ } >+ >+ function search_holds(modify_holds_form){ >+ var table_settings = [% TablesSettings.GetTableSettings( 'circ', 'holds', 'holds_to_modify', 'json' ) | $raw %]; >+ var searchpattern = ""; >+ var filters = { >+ "me.expirationdate": function(){ >+ var expirationdate_from = modify_holds_form.find("#expirationdate_from").val(); >+ var expirationdate_to = modify_holds_form.find("#expirationdate_to").val(); >+ >+ if (expirationdate_from && expirationdate_to){ >+ searchpattern += _("expiration date between ") + expirationdate_from + " and " + expirationdate_to + " "; >+ return {"-between": [expirationdate_from, expirationdate_to]}; >+ } >+ else if (expirationdate_from && !expirationdate_to){ >+ searchpattern += _("expiration date from ") + expirationdate_from + " "; >+ return {">=": expirationdate_from }; >+ } else if (!expirationdate_from && expirationdate_to){ >+ searchpattern += _("expiration date to ") + expirationdate_to + " "; >+ return {"<=": expirationdate_to }; >+ } >+ }, >+ "me.branchcode": function(){ >+ var branchcodes = modify_holds_form.find("#branchcodes").val(); >+ if( branchcodes.length > 0 ){ >+ searchpattern += _("from libraries "); >+ var selected_libraries = modify_holds_form.find("#branchcodes option:selected"); >+ $.each(selected_libraries, function(key, value) { >+ searchpattern += value.text + " "; >+ }); >+ return {"-in": branchcodes }; >+ } >+ }, >+ "me.status": function(){ >+ var found_status = modify_holds_form.find("#found_status").val(); >+ if( found_status.length > 0 ){ >+ var filters = [{"-in": found_status}]; >+ if(jQuery.inArray("NULL", found_status) !== -1){ >+ filters.push({"=": null}); >+ } >+ searchpattern += _("found status in "); >+ $.each(found_status, function(key, value) { >+ let status = _("No status "); >+ if ( value == "T" ) { >+ status = _("In transit "); >+ } else if( value == "P" ) { >+ status = _("In processing "); >+ } else if( value == "W" ) { >+ status = _("Waiting "); >+ } >+ searchpattern += status; >+ }); >+ return filters; >+ } >+ }, >+ "me.suspended": function(){ >+ var suspend_status = modify_holds_form.find("#suspend_status").val(); >+ >+ if(suspend_status != "none"){ >+ var suspended_text = suspend_status == 0 ? _("Not suspended") : _("Suspended"); >+ searchpattern += _("with suspend status ") + suspended_text; >+ >+ return {"=": suspend_status}; >+ } >+ }, >+ "me.suspend_until": function(){ >+ var suspend_until_from = modify_holds_form.find("#suspend_until_from").val(); >+ var suspend_until_to = modify_holds_form.find("#suspend_until_to").val(); >+ >+ searchpattern += suspend_until_from && suspend_until_to ? _("suspend date between ")+ suspend_until_from +" and "+suspend_until_to : ""; >+ >+ return suspend_until_from && suspend_until_to ? {"-between": [suspend_until_from, suspend_until_to]} : ""; >+ }, >+ "me.notes": function(){ >+ var holdnotes = modify_holds_form.find("#holdnotes").val(); >+ >+ searchpattern += holdnotes ? _("hold notes like ") + holdnotes : ""; >+ >+ return holdnotes ? {"-like": "%"+holdnotes+"%"} : ""; >+ }, >+ "me.hold_id": function(){ >+ return hold_ids_from_sql.length > 0 ? { "-in": hold_ids_from_sql } : ""; >+ } >+ } >+ >+ var holds_modify_table = $("#holds_to_modify").kohaTable({ >+ "ajax": { >+ "url": "/api/v1/holds" >+ }, >+ "embed": [ >+ "biblio", >+ "item", >+ "pickup_library", >+ "patron" >+ ], >+ "destroy": true, >+ "autoWidth": false, >+ "processing": true, >+ "columns": [ >+ { >+ "data": "hold_id", >+ "name": "checkbox", >+ "orderable": false, >+ "render": function(data, type, row, meta) { >+ return '<input class="selection" type="checkbox" value="'+ encodeURIComponent(data) +'"/>' >+ } >+ }, >+ { >+ "data": "expiration_date", >+ "name": "expiration_date", >+ "type": "date", >+ "title": _("Expiration date"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return $date(data); >+ } >+ }, >+ { >+ "data": "biblio.title", >+ "name": "title", >+ "title": _("Title"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return '<a href="/cgi-bin/koha/catalogue/detail.pl?' + >+ 'biblionumber=' + encodeURIComponent( row.biblio_id ) + '">' + escape_str( data ) + '</a>'; >+ } >+ }, >+ { >+ "data": "item.external_id", >+ "name": "barcode", >+ "title": _("Barcode"), >+ "defaultContent": _(""), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ if( row.item ){ >+ let item_id = encodeURIComponent( row.item_id ); >+ let biblio_id = encodeURIComponent( row.biblio_id ); >+ return '<a href="/cgi-bin/koha/catalogue/moredetail.pl?' + >+ 'itemnumber='+ item_id +'&biblionumber='+ biblio_id +'&' + >+ 'bi='+ biblio_id +'#item'+ item_id +'">' + escape_str( data ) + '</a>' >+ } >+ } >+ }, >+ { >+ "data": "patron_id", >+ "name": "patron", >+ "title": _("Patron"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ let patron_to_html = $patron_to_html(row.patron, { url: true, display_cardnumber: true, hide_patron_name }); >+ return patron_to_html; >+ } >+ }, >+ { >+ "data": "status", >+ "name": "status", >+ "title": _("Status"), >+ "className": "found_status", >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ let status = _("No status"); >+ if ( data == "T" ) { >+ status = _("In transit"); >+ } else if( data == "P" ) { >+ status = _("In processing"); >+ } else if( data == "W" ) { >+ status = _("Waiting"); >+ } >+ return "<span data-found-status='" + escape_str( data ) + "'>" + status + "</span>"; >+ } >+ }, >+ { >+ "data": "pickup_library_id", >+ "name": "pickup_library", >+ "title": _("Hold pickup library"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return escape_str( row.pickup_library.name ); >+ } >+ }, >+ { >+ "data": "suspended", >+ "name": "suspended", >+ "className": "suspended", >+ "title": _("Suspended"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ var suspended_string = data == 0 ? _("No") : _("Yes"); >+ return "<span data-suspended='" + data + "'>" + suspended_string + "</span>"; >+ } >+ }, >+ { >+ "data": "suspended_until", >+ "name": "suspended_until", >+ "title": _("Suspended until"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return $date( data ); >+ } >+ }, >+ { >+ "data": "notes", >+ "name": "notes", >+ "title": _("Notes"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return escape_str( data ); >+ } >+ } >+ ], >+ }, table_settings, 0, filters); >+ >+ $(".searchpattern").text(searchpattern); >+ >+ $("#modify_holds_results-wrapper").show(); >+ } >+ >+ $("#process_mods").on('submit', function(e) { >+ >+ var hold_ids = JSON.parse( localStorage.getItem("holds_modify_selections") ) || []; >+ >+ $("#process_mods input[name='hold_id']").remove(); >+ >+ hold_ids.forEach(function(hold) { >+ $('<input>').attr({ >+ type: 'hidden', >+ name: 'hold_id', >+ value: hold.hold_id >+ }).appendTo('#process_mods'); >+ }); >+ >+ var errors = []; >+ >+ var new_expiration_date = $("#new_expiration_date").val(); >+ var new_pickup_loc = $("#new_pickup_loc").val(); >+ var new_suspend_status = $("#new_suspend_status").val(); >+ var new_suspend_date = $("#new_suspend_date").val(); >+ var new_hold_note = $("#new_hold_note").val(); >+ var clear_hold_notes = $("#clear_hold_notes").val(); >+ >+ if ( hold_ids.length == 0 ) { >+ e.preventDefault(); >+ errors.push(_("Please select at least one hold to process.")); >+ } >+ >+ if( new_pickup_loc || new_suspend_status || new_suspend_date ){ >+ var found = false; >+ hold_ids.forEach(function(hold){ >+ if( hold.found_status ){ >+ e.preventDefault(); >+ found = true; >+ } >+ }); >+ if( ( new_suspend_status || new_suspend_date ) && found ){ >+ errors.push(_("One or more holds have found status and can't be suspended.")); >+ } >+ if( new_pickup_loc && found ){ >+ errors.push(_("One or more holds have found status and their pick up location can't be changed.")); >+ } >+ } >+ >+ var suspended = false; >+ >+ hold_ids.forEach(function(hold){ >+ if( hold.suspened ){ >+ suspended = true; >+ } >+ }); >+ >+ if( ( !new_suspend_status || new_suspend_status == "0" || !suspended ) && new_suspend_date ){ >+ new_suspend_status = 1; >+ } >+ >+ if (errors.length > 0) { >+ e.preventDefault(); >+ $("#errorList").html(errors.map(error => `<li>${error}</li>`).join("")); >+ $("#errorModal").modal('show'); >+ return false; >+ } >+ return true; >+ }); >+ >+ //Modified holds table >+ var table_settings = [% TablesSettings.GetTableSettings( 'circ', 'holds', 'modified_holds', 'json' ) | $raw %]; >+ var filters = { >+ "me.hold_id": function(){ >+ return {"-in": hold_ids}; >+ } >+ } >+ >+ var holds_modified_table = $("#modified_holds").kohaTable({ >+ "ajax": { >+ "url": "/api/v1/holds" >+ }, >+ "embed": [ >+ "biblio", >+ "item", >+ "pickup_library", >+ "patron" >+ ], >+ "destroy": true, >+ "autoWidth": false, >+ "processing": true, >+ "columns": [ >+ { >+ "data": "expiration_date", >+ "name": "expiration_date", >+ "type": "date", >+ "title": _("Expiration date"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return $date(data); >+ } >+ }, >+ { >+ "data": "biblio.title", >+ "name": "title", >+ "title": _("Title"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return '<a href="/cgi-bin/koha/catalogue/detail.pl?' + >+ 'biblionumber=' + encodeURIComponent( row.biblio_id ) + '">' + escape_str( data ) + '</a>'; >+ } >+ }, >+ { >+ "data": "item.external_id", >+ "name": "barcode", >+ "title": _("Barcode"), >+ "defaultContent": _(""), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ if( row.item ){ >+ let item_id = encodeURIComponent( row.item_id ); >+ let biblio_id = encodeURIComponent( row.biblio_id ); >+ return '<a href="/cgi-bin/koha/catalogue/moredetail.pl?' + >+ 'itemnumber='+ item_id +'&biblionumber='+ biblio_id +'&' + >+ 'bi='+ biblio_id +'#item'+ item_id +'">' + escape_str( data ) + '</a>' >+ } >+ } >+ }, >+ { >+ "data": "patron_id", >+ "name": "patron", >+ "title": _("Patron"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ let patron_to_html = $patron_to_html(row.patron, { url: true, display_cardnumber: true, hide_patron_name }); >+ return patron_to_html; >+ } >+ }, >+ { >+ "data": "status", >+ "name": "status", >+ "className": "found_status", >+ "title": _("Status"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ let status = _("No status"); >+ if ( data == "T" ) { >+ status = _("In transit"); >+ } else if( data == "P" ) { >+ status = _("In processing"); >+ } else if( data == "W" ) { >+ status = _("Waiting"); >+ } >+ return status; >+ } >+ }, >+ { >+ "data": "pickup_library_id", >+ "name": "pickup_library", >+ "title": _("Hold pickup library"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return escape_str( row.pickup_library.name ); >+ } >+ }, >+ { >+ "data": "suspended", >+ "name": "suspended", >+ "title": _("Suspended"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return data == 0 ? _("No") : _("Yes"); >+ } >+ }, >+ { >+ "data": "suspended_until", >+ "name": "suspended_until", >+ "title": _("Suspended until"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return $date( data ); >+ } >+ }, >+ { >+ "data": "notes", >+ "name": "notes", >+ "title": _("Notes"), >+ "orderable": true, >+ "render": function( data, type, row, meta ) { >+ return escape_str( data ); >+ } >+ } >+ ] >+ }, table_settings, 0, filters); >+ >+ var modified_message = '<div class="alert alert-info">'+hold_ids.length+' hold(s) have been modified!</div>'; >+ $("#modified_holds_results-wrapper").prepend(modified_message); >+ >+ }); >+ </script> >+ [% END %] >+ >+ [% INCLUDE 'intranet-bottom.inc' %] >+</div> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >index 3aa18b39636..f7839ed8938 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >@@ -29,7 +29,7 @@ > <h1>Tools</h1> > <div class="row"> > <div class="col-sm-6"> >- [% IF ( CAN_user_tools_manage_patron_lists || CAN_user_clubs || CAN_user_tools_moderate_comments || CAN_user_tools_import_patrons || CAN_user_tools_edit_notices || CAN_user_tools_edit_notice_status_triggers || CAN_user_tools_label_creator || CAN_user_tools_delete_anonymize_patrons || CAN_user_tools_edit_patrons || CAN_user_tools_batch_extend_due_dates || CAN_user_tools_moderate_tags || CAN_user_tools_rotating_collections || ( CAN_user_tools_batch_upload_patron_images && Koha.Preference('patronimages') ) ) %] >+ [% IF ( CAN_user_tools_manage_patron_lists || CAN_user_clubs || CAN_user_tools_moderate_comments || CAN_user_tools_import_patrons || CAN_user_tools_edit_notices || CAN_user_tools_edit_notice_status_triggers || CAN_user_tools_label_creator || CAN_user_tools_delete_anonymize_patrons || CAN_user_tools_edit_patrons || CAN_user_tools_batch_extend_due_dates || CAN_user_tools_batch_modify_holds || CAN_user_tools_moderate_tags || CAN_user_tools_rotating_collections || ( CAN_user_tools_batch_upload_patron_images && Koha.Preference('patronimages') ) ) %] > <h3>Patrons and circulation</h3> > [% END %] > <dl> >@@ -88,6 +88,11 @@ > <dd>Modify the due date checkouts in batch</dd> > [% END %] > >+ [% IF ( CAN_user_tools_batch_modify_holds ) %] >+ <dt><a href="/cgi-bin/koha/tools/batch_modify_holds.pl">Batch modify holds</a></dt> >+ <dd>Modify holds in batch</dd> >+ [% END %] >+ > [% IF ( CAN_user_tools_moderate_tags ) %] > <dt> > <a href="/cgi-bin/koha/tags/review.pl">Tags</a> >diff --git a/tools/batch_modify_holds.pl b/tools/batch_modify_holds.pl >new file mode 100755 >index 00000000000..c73b1f332e8 >--- /dev/null >+++ b/tools/batch_modify_holds.pl >@@ -0,0 +1,107 @@ >+#!/usr/bin/perl >+ >+# This file is part of Koha. >+# >+# Copyright 2024 Koha Development Team >+# >+# 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; >+ >+use JSON qw( to_json ); >+ >+use C4::Auth qw( get_template_and_user ); >+use C4::Output qw( output_html_with_http_headers ); >+ >+use Koha::DateUtils qw( dt_from_string ); >+use Koha::Holds; >+ >+my $input = CGI->new; >+my $op = $input->param('op') // q|cud-form|; >+ >+my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => 'tools/batch_modify_holds.tt', >+ query => $input, >+ type => "intranet", >+ flagsrequired => { tools => 'batch_modify_holds' }, >+ } >+); >+ >+my @hold_ids; >+ >+if ( $op eq 'cud-form' ) { >+ my $reserve_ids_list = $input->param('reserve_ids_list') || undef; >+ if ($reserve_ids_list) { >+ my @reserve_ids = split /\n/, $reserve_ids_list; >+ $template->param( reserve_ids_list => to_json( \@reserve_ids ), ); >+ } >+ $template->param( view => 'cud-form', ); >+} elsif ( $op eq 'cud-modify' ) { >+ my $new_expiration_date = $input->param('new_expiration_date'); >+ my $new_pickup_loc = $input->param('new_pickup_loc'); >+ my $new_suspend_status = $input->param('new_suspend_status'); >+ my $new_suspend_date = $input->param('new_suspend_date'); >+ my $new_hold_note = $input->param('new_hold_note'); >+ my $clear_hold_notes = $input->param('clear_hold_notes'); >+ >+ @hold_ids = $input->multi_param('hold_id'); >+ >+ my $holds_to_update = >+ Koha::Holds->search( { reserve_id => { -in => \@hold_ids } }, { join => [ "item", "biblio" ] } ); >+ >+ while ( my $hold = $holds_to_update->next ) { >+ >+ if ($new_expiration_date) { >+ $hold->expirationdate($new_expiration_date)->store; >+ } >+ >+ if ( $new_pickup_loc && ( $hold->branchcode ne $new_pickup_loc ) ) { >+ $hold->branchcode($new_pickup_loc)->store; >+ } >+ >+ if ( $new_suspend_status && $new_suspend_status ne "" && !$hold->is_found ) { >+ $hold->suspend(1)->store; >+ if ($new_suspend_date) { >+ $hold->suspend_until($new_suspend_date)->store; >+ } else { >+ $hold->suspend_until(undef)->store; >+ } >+ } elsif ( !$new_suspend_status && $new_suspend_date ) { >+ $hold->suspend(1)->store; >+ $hold->suspend_until($new_suspend_date)->store; >+ } else { >+ $hold->suspend(0)->store; >+ $hold->suspend_until(undef)->store; >+ } >+ >+ if ($new_hold_note) { >+ $hold->reservenotes($new_hold_note)->store; >+ } >+ >+ if ($clear_hold_notes) { >+ $hold->reservenotes(undef)->store; >+ } >+ } >+ >+ $template->param( >+ updated_holds => to_json( \@hold_ids ), >+ view => 'report', >+ ); >+ >+} >+ >+output_html_with_http_headers $input, $cookie, $template->output; >-- >2.39.5
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 36135
:
163021
|
163022
|
163023
|
163024
|
163529
|
163530
|
163540
|
163541
|
163542
|
163543
|
163544
|
163796
|
164037
|
164051
|
164057
|
164058
|
164059
|
164060
|
164061
|
164062
|
164063
|
177518
|
177519
|
177959
|
177960
|
177961
|
177962
|
177963
|
177964
|
177965
|
177966
|
177967
|
177968
|
177969
|
177975
|
177976
|
177981
|
177982
|
178154
|
178155
|
178156
|
178157
|
178158
|
178159
|
178160
|
178161
|
178162
|
178163
|
178164
|
178165
|
178166
|
178167
|
178954
|
179028
|
179029
|
179030
|
179031
|
179032
|
179033
|
179034
|
179035
|
179036
|
179037
|
179038
|
179039
|
179040
|
179041
|
179042
|
179081
|
179082
|
181049
|
183151
|
183294
|
183295
|
183296
|
183297
|
183298
|
183299
|
183300
|
183305
| 183306 |
183307
|
183308
|
183771