Bug 12403: Add a batch record deletion
authorJonathan Druart <jonathan.druart@biblibre.com>
Wed, 4 Sep 2013 10:44:05 +0000 (12:44 +0200)
committerTomas Cohen Arazi <tomascohen@gmail.com>
Fri, 7 Nov 2014 18:25:49 +0000 (15:25 -0300)
This patch offers a new tool for deleting records.
Biblios and authorities will can to be deleted with a simple list of
biblionumber or authid.

This feature adds:
- a new pl/tt files tools/batch_delete_records
- a new permission: tools > records_batchdel

Test plan for biblios:
1/ There are two ways to generate a list of biblionumbers:
- using the basket: do a search, add some biblio to your basket, open
  the basket and click on the "Action" button > "Delete"
- generating a list from a report
2/ On the "Batch record deletion" tool verify:
- biblios with issues cannot be deleted (checkbox disabled and line in
  red).
- information is correct.
- sort functions work on each columns.
- the items, reserves and issues values are correct.
3/ After clicking on the "Delete selected recors" button, verify:
- reserves, items and biblio have successful been deleted.
- if an error occurs, the tool display an error message.

Test plan for authority:
1/ Generate a list of authid using a report:
2/ On the "Batch record deletion" tool verify:
- authorities are display with the summary.
- the count usage (used in X biblios) is correct.
3/ After clicking on the "Delete selected recors" button, verify:
- The authorities have successful been deleted.
- if an error occurs, the tool display an error message.

Signed-off-by: Brendan Gallagher <brendan@bywatersolutions.com>
Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Tomas Cohen Arazi <tomascohen@gmail.com>
koha-tmpl/intranet-tmpl/prog/en/includes/tools-menu.inc
koha-tmpl/intranet-tmpl/prog/en/modules/basket/basket.tt
koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_delete_records.tt [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt
tools/batch_delete_records.pl [new file with mode: 0755]

index 963195d..ea3cf82 100644 (file)
@@ -56,6 +56,9 @@
     [% IF ( CAN_user_tools_items_batchmod ) %]
        <li><a href="/cgi-bin/koha/tools/batchMod.pl">Batch item modification</a></li>
     [% END %]
+    [% IF CAN_user_tools_records_batchdel %]
+      <li><a href="/cgi-bin/koha/tools/batch_delete_records.pl">Batch record deletion</a></li>
+    [% END %]
     [% IF ( CAN_user_tools_export_catalog ) %]
     <li><a href="/cgi-bin/koha/tools/export.pl">Export data</a></li>
     [% END %]
index 5a3d22e..60fd2e4 100644 (file)
@@ -101,6 +101,12 @@ function placeHold () {
         [% END %]
         </ul>
     </div>
+    <div class="btn-group">
+        <a class="btn btn-small dropdown-toggle" data-toggle="dropdown" href="#" id="actioncart"><i class="icon-play"></i> Actions <span class="caret"></span> </a>
+        <ul class="dropdown-menu">
+            <li><a href="/cgi-bin/koha/tools/batch_delete_records.pl?op=list&amp;bib_list=[% bib_list %]&amp;type=biblio">Delete</a></li>
+        </ul>
+    </div>
     <a class="btn btn-small" href="basket.pl" onclick="printBasket(); return false;"><i class="icon-print"></i> Print</a>
     <a class="btn btn-small" href="basket.pl" onclick="delBasket('popup'); return false;"><i class="icon-trash"></i> Empty and close</a>
     <a class="btn btn-small close" href="basket.pl"><i class="icon-remove-sign"></i> Hide window</a>
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_delete_records.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/batch_delete_records.tt
new file mode 100644 (file)
index 0000000..f7fa79e
--- /dev/null
@@ -0,0 +1,236 @@
+[% PROCESS 'authorities-search-results.inc' %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha &rsaquo; Tools &rsaquo; Batch record deletion</title>
+[% INCLUDE 'doc-head-close.inc' %]
+<link rel="stylesheet" type="text/css" href="[% themelang %]/css/datatables.css" />
+[% INCLUDE 'datatables.inc' %]
+<script type="text/javascript" src="[% interface %]/lib/jquery/plugins/jquery.checkboxes.min.js"></script>
+<script type="text/javascript">
+//<![CDATA[
+var MSG_CANNOT_BE_DELETED = _("This record cannot be deleted, at least one item is currently issued");
+$(document).ready(function() {
+  $("#selectall").click(function(e){
+    e.preventDefault();
+    $(".records").checkCheckboxes();
+  });
+  $("#clearall").click(function(e){
+    e.preventDefault();
+    $(".records").unCheckCheckboxes();
+  });
+  $("#selectwithoutitems").click(function(e){
+    e.preventDefault();
+    $("#biblios").checkCheckboxes(":input[data-items='0']:not(:disabled)");
+  });
+  $("#selectnotreserved").click(function(e){
+    e.preventDefault();
+    $("#biblios").checkCheckboxes(":input[data-reserves='0']:not(:disabled)");
+
+  });
+  $("#clearlinkedtobiblio").click(function(e){
+    e.preventDefault();
+    $("#authorities").unCheckCheckboxes(":not(input[data-usage='0'])");
+  });
+  $("#selectall").click();
+
+  [% IF recordtype == 'biblio' %]
+    $(".records input:checkbox[data-issues!='0']").each(function(){
+      $(this).attr('title', MSG_CANNOT_BE_DELETED)
+      $(this).attr('disabled', true);
+      $(this).attr('checked', false);
+      $(this).parents('tr').find('td').css('background-color', 'red');
+    });
+  [% END %]
+
+  $("table#biblios").dataTable($.extend(true, {}, dataTablesDefaults, {
+    "aoColumnDefs": [
+      { "aTargets": [ 0 ], "bSortable": false, "bSearchable": false },
+      { "aTargets": [ 3, 4 ], "sType": "num-html" }
+    ],
+    "sDom": 't',
+    "aaSorting": [],
+    "bPaginate": false
+  }));
+
+  $("table#authorities").dataTable($.extend(true, {}, dataTablesDefaults, {
+    "aoColumnDefs": [
+      { "aTargets": [ 0 ], "bSortable": false, "bSearchable": false },
+      { "aTargets": [ 3 ], "sType": "num-html" }
+    ],
+    "sDom": 't',
+    "aaSorting": [],
+    "bPaginate": false
+  }));
+});
+//]]>
+</script>
+</head>
+<body id="tools_batch_delete_records" class="tools">
+[% INCLUDE 'header.inc' %]
+[% INCLUDE 'cat-search.inc' %]
+
+<div id="breadcrumbs">
+    <a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo;
+    <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> &rsaquo;
+    <a href="/cgi-bin/koha/tools/batch_delete_records.pl">Batch record deletion</a>
+</div>
+
+<div id="doc3" class="yui-t2">
+<div id="bd">
+<div id="yui-main">
+<div class="yui-b">
+  <h1>Batch record deletion</h1>
+  [% FOREACH message IN messages %]
+    [% IF message.type == 'success' %]
+      <div class="dialog message">
+    [% ELSIF message.type == 'warning' %]
+      <div class="dialog alert">
+    [% ELSIF message.type == 'error' %]
+      <div class="dialog error" style="margin:auto;">
+    [% END %]
+    [% IF message.code == 'biblio_not_exists' %]
+      The biblionumber [% message.biblionumber %] does not exist in the database.
+    [% ELSIF message.code == 'authority_not_exists' %]
+      The authority id [% message.authid %] does not exist in the database.
+    [% ELSIF message.code == 'item_issued' %]
+      At least one item issued for the biblio [% message.biblionumber %].
+    [% ELSIF message.code == 'reserve_not_cancelled' %]
+      The biblio [% message.biblionumber %] has not been deleted. A reserve (reserve_id [% message.reserve_id %]) caused an error on cancel.
+    [% ELSIF message.code == 'item_not_deleted' %]
+      The biblio [% message.biblionumber %] has not been deleted. An item (itemnumber [% message.itemnumber %]) caused an error on delete.
+    [% ELSIF message.code == 'biblio_not_deleted' %]
+      The biblio [% message.biblionumber %] has not been deleted. An error occurred on deleting it.
+    [% ELSIF message.code == 'authority_not_deleted' %]
+      The authority [% message.authid %] has not been deleted. An error occurred on deleting it.
+    [% ELSIF message.code == 'biblio_deleted' %]
+      The biblio [% message.biblionumber %] has successfully been deleted.
+    [% ELSIF message.code == 'authority_deleted' %]
+      The authority [% message.authid %] has successfully been deleted.
+    [% END %]
+    [% IF message.error %]
+      (The error was: [% message.error%], see the Koha logfile for more information).
+    [% END %]
+    </div>
+  [% END %]
+  [% IF op == 'form' %]
+    <form method="post" enctype="multipart/form-data" action="/cgi-bin/koha/tools/batch_delete_records.pl">
+      <fieldset class="rows">
+        <legend>Record type</legend>
+        <ol>
+          <li><label for="biblio_type">Biblios: </label><input type="radio" name="recordtype" value="biblio" id="biblio_type" checked="checked" /></li>
+          <li><label for="authority_type">Authorities: </label><input type="radio" name="recordtype" value="authority" id="authority_type" /></li>
+        </ol>
+      </fieldset>
+      <fieldset class="rows">
+        <legend>Use a file</legend>
+        <ol>
+          <li><label for="uploadfile">File: </label> <input type="file" id="uploadfile" name="uploadfile" /></li>
+        </ol>
+      </fieldset>
+      <fieldset class="rows">
+        <legend>Or enter a list of record numbers</legend>
+        <ol>
+          <li>
+            <label for="recordnumber_list">Record number list (one per line): </label>
+            <textarea rows="10" cols="30" id="recordnumber_list" name="recordnumber_list"></textarea>
+          </li>
+        </ol>
+      </fieldset>
+      <fieldset class="action">
+        <input type="hidden" name="op" value="list" />
+        <input type="submit" value="Continue" class="button" />
+        <a class="cancel" href="/cgi-bin/koha/tools/tools-home.pl">Cancel</a>
+      </fieldset>
+    </form>
+  [% ELSIF op == 'list' %]
+    [% IF records %]
+      [% IF recordtype == 'biblio' %]
+        <div id="toolbar">
+          <a id="selectall" href="#">Select All</a>
+          | <a id="clearall" href="#">Clear All</a>
+          | <a id="selectwithoutitems" href="#">Select without items</a>
+          | <a id="selectnotreserved" href="#">Select not reserved</a>
+        </div>
+        <form action="/cgi-bin/koha/tools/batch_delete_records.pl" method="post">
+          <table id="biblios" class="records">
+            <thead>
+              <tr>
+                <th></th>
+                <th>Biblionumber</th>
+                <th>Title</th>
+                <th>Items</th>
+                <th>Reserves</th>
+                <th>Issues</th>
+              </tr>
+            </thead>
+            <tbody>
+              [% FOR biblio IN records %]
+                <tr>
+                  <td><input type="checkbox" name="record_id" value="[% biblio.biblionumber %]" data-items="[% biblio.itemnumbers.size %]" data-issues="[% biblio.issues_count %]" data-reserves="[% biblio.reserves.size %]" /></td>
+                  <td>[% biblio.biblionumber %]</td>
+                  <td><a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% biblio.biblionumber %]">[% biblio.title %]</a></td>
+                  <td><a href="/cgi-bin/koha/catalogue/moredetail.pl?biblionumber=[% biblio.biblionumber %]">[% biblio.itemnumbers.size %]</a></td>
+                  <td><a href="/cgi-bin/koha/reserve/request.pl?biblionumber=[% biblio.biblionumber %]">[% biblio.reserves.size %]</a></td>
+                  <td><a href="/cgi-bin/koha/catalogue/issuehistory.pl?biblionumber=[% biblio.biblionumber %]">[% biblio.issues_count %]</a></td>
+                </tr>
+              [% END %]
+            </tbody>
+          </table>
+          <div class="note">Reminder: this action will delete all selected biblios, attached subscriptions, existing holds and items!</div>
+      [% ELSE %]
+        <div id="toolbar">
+          <a id="selectall" href="#">Select All</a>
+          | <a id="clearall" href="#">Clear All</a>
+          | <a id="clearlinkedtobiblio" href="#">Clear linked to biblio </a>
+        </div>
+        <form action="/cgi-bin/koha/tools/batch_delete_records.pl" method="post">
+          <table id="authorities" class="records">
+            <thead>
+              <tr>
+                <th></th>
+                <th>Authid</th>
+                <th>Summary</th>
+                <th>Used in</th>
+              </tr>
+            </thead>
+            <tbody>
+              [% FOR authority IN records %]
+                <tr>
+                  <td><input type="checkbox" name="record_id" value="[% authority.authid %]" data-usage="[% authority.count_usage %]" /></td>
+                  <td><a href="/cgi-bin/koha/authorities/detail.pl?authid=[% authority.authid %]">[% authority.authid %]</a></td>
+                  <td>[% PROCESS authresult summary=authority.summary %]</td>
+                  <td><a href="/cgi-bin/koha/catalogue/search.pl?type=intranet&op=do_search&idx=an,phr&q=[% authority.authid %]">[% authority.count_usage %] biblio(s)</a></td>
+                </tr>
+              [% END %]
+            </tbody>
+          </table>
+          <div class="note">Reminder: this action will delete all selected authorities!</div>
+      [% END %]
+        <fieldset class="action">
+          <input type="hidden" name="op" value="delete" />
+          <input type="hidden" name="recordtype" value="[% recordtype %]" />
+          <input type="submit" value="Delete selected records" class="button" />
+          <a class="cancel" href="/cgi-bin/koha/tools/batch_delete_records.pl">Cancel</a>
+        </fieldset>
+      </form>
+    [% ELSE %]
+      There is no record ids defined.
+    [% END %]
+  [% ELSIF op == 'report' %]
+    [% IF report.total_records == report.total_success %]
+      All records have successfully been deleted!
+    [% ELSIF report.total_success == 0 %]
+      No record has been deleted, some errors occurred.
+    [% ELSE %]
+      [% report.total_success %] / [% report.total_records %] records have successfully been deleted but some errors occurred.
+    [% END %]
+    <p><a href="/cgi-bin/koha/tools/batch_delete_records.pl" title="New batch record deletion">New batch record deletion</a></p>
+  [% ELSE %]
+    No action defined for the template.
+  [% END %]
+</div>
+</div>
+<div class="yui-b">
+  [% INCLUDE 'tools-menu.inc' %]
+</div>
+</div>
+[% INCLUDE 'intranet-bottom.inc' %]
index 723dcba..82dffca 100644 (file)
     <dt><a href="/cgi-bin/koha/tools/batchMod.pl">Batch item modification</a></dt>
     <dd>Modify items in a batch</dd>
     [% END %]
-    
+
+    [% IF CAN_user_tools_records_batchdel %]
+      <dt><a href="/cgi-bin/koha/tools/batch_delete_records.pl">Batch record deletion</a></dt>
+      <dd>Delete a batch of records (biblios or authorities)</dd>
+    [% END %]
+
     [% IF ( CAN_user_tools_export_catalog ) %]
     <dt><a href="/cgi-bin/koha/tools/export.pl">Export data</a></dt>
     <dd>Export bibliographic, holdings, and authority records</dd>
diff --git a/tools/batch_delete_records.pl b/tools/batch_delete_records.pl
new file mode 100755 (executable)
index 0000000..7bb8489
--- /dev/null
@@ -0,0 +1,231 @@
+#!/usr/bin/perl
+
+# This file is part of Koha.
+#
+# Copyright 2013 BibLibre
+#
+# 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 List::MoreUtils qw( uniq );
+
+use C4::Auth;
+use C4::Output;
+use C4::AuthoritiesMarc;
+use C4::Biblio;
+
+my $input = new CGI;
+my $dbh = C4::Context->dbh;
+my $op = $input->param('op') // q|form|;
+my $recordtype = $input->param('recordtype') // 'biblio';
+
+my ($template, $loggedinuser, $cookie) = get_template_and_user({
+        template_name => 'tools/batch_delete_records.tt',
+        query => $input,
+        type => "intranet",
+        authnotrequired => 0,
+        flagsrequired => { tools => 'biblio_batchdel' },
+});
+
+my @records;
+my @messages;
+if ( $op eq 'form' ) {
+    # Display the form
+    $template->param( op => 'form' );
+} elsif ( $op eq 'list' ) {
+    # List all records to process
+    my @record_ids;
+    if ( my $bib_list = $input->param('bib_list') ) {
+        # Come from the basket
+        @record_ids = split /\//, $bib_list;
+        $recordtype = 'biblio';
+    } elsif ( my $uploadfile = $input->param('uploadfile') ) {
+        # A file of id is given
+        while ( my $content = <$uploadfile> ) {
+            next unless $content;
+            $content =~ s/[\r\n]*$//;
+            push @record_ids, $content if $content;
+        }
+    } else {
+        # The user enters manually the list of id
+        push @record_ids, split( /\s\n/, $input->param('recordnumber_list') );
+    }
+
+    for my $record_id ( uniq @record_ids ) {
+        if ( $recordtype eq 'biblio' ) {
+            # Retrieve biblio information
+            my $biblio = C4::Biblio::GetBiblio( $record_id );
+            unless ( $biblio ) {
+                push @messages, {
+                    type => 'warning',
+                    code => 'biblio_not_exists',
+                    biblionumber => $record_id,
+                };
+                next;
+            }
+            $biblio->{itemnumbers} = C4::Items::GetItemnumbersForBiblio( $record_id );
+            $biblio->{reserves} = C4::Reserves::GetReservesFromBiblionumber({ biblionumber => $record_id });
+            $biblio->{issues_count} = C4::Biblio::CountItemsIssued( $record_id );
+            push @records, $biblio;
+        } else {
+            # Retrieve authority information
+            my $authority = C4::AuthoritiesMarc::GetAuthority( $record_id );
+            unless ( $authority ) {
+                push @messages, {
+                    type => 'warning',
+                    code => 'authority_not_exists',
+                    authid => $record_id,
+                };
+                next;
+            }
+
+            $authority = {
+                authid => $record_id,
+                summary => C4::AuthoritiesMarc::BuildSummary( $authority, $record_id ),
+                count_usage => C4::AuthoritiesMarc::CountUsage( $record_id ),
+            };
+            push @records, $authority;
+        }
+    }
+    $template->param(
+        records => \@records,
+        op => 'list',
+    );
+} elsif ( $op eq 'delete' ) {
+    # We want to delete selected records!
+    my @record_ids = $input->param('record_id');
+    my $dbh = C4::Context->dbh;
+    $dbh->{AutoCommit} = 0;
+    $dbh->{RaiseError} = 1;
+
+    my $error;
+    my $report = {
+        total_records => 0,
+        total_success => 0,
+    };
+    RECORD_IDS: for my $record_id ( sort { $a <=> $b } @record_ids ) {
+        $report->{total_records}++;
+        next unless $record_id;
+        if ( $recordtype eq 'biblio' ) {
+            # Biblios
+            my $biblionumber = $record_id;
+            # First, checking if issues exist.
+            # If yes, nothing to do
+            if ( C4::Biblio::CountItemsIssued( $biblionumber ) ) {
+                push @messages, {
+                    type => 'warning',
+                    code => 'item_issued',
+                    biblionumber => $biblionumber,
+                };
+                $dbh->rollback;
+                next;
+            }
+
+            # Cancel reserves
+            my $reserves = C4::Reserves::GetReservesFromBiblionumber({ biblionumber => $biblionumber });
+            for my $reserve ( @$reserves ) {
+                eval{
+                    C4::Reserves::CancelReserve( { reserve_id => $reserve->{reserve_id} } );
+                };
+                if ( $@ ) {
+                    push @messages, {
+                        type => 'error',
+                        code => 'reserve_not_cancelled',
+                        biblionumber => $biblionumber,
+                        reserve_id => $reserve->{reserve_id},
+                        error => $@,
+                    };
+                    $dbh->rollback;
+                    next RECORD_IDS;
+                }
+            }
+
+            # Delete items
+            my @itemnumbers = @{ C4::Items::GetItemnumbersForBiblio( $biblionumber ) };
+            ITEMNUMBER: for my $itemnumber ( @itemnumbers ) {
+                my $error = eval { C4::Items::DelItemCheck( $dbh, $biblionumber, $itemnumber ) };
+                if ( $error != 1 or $@ ) {
+                    push @messages, {
+                        type => 'error',
+                        code => 'item_not_deleted',
+                        biblionumber => $biblionumber,
+                        itemnumber => $itemnumber,
+                        error => ($@ ? $@ : $error),
+                    };
+                    $dbh->rollback;
+                    next BIBLIONUMBER;
+                }
+            }
+
+            # Finally, delete the biblio
+            my $error = eval {
+                C4::Biblio::DelBiblio( $biblionumber );
+            };
+            if ( $error or $@ ) {
+                push @messages, {
+                    type => 'error',
+                    code => 'biblio_not_deleted',
+                    biblionumber => $biblionumber,
+                    error => ($@ ? $@ : $error),
+                };
+                $dbh->rollback;
+                next;
+            }
+
+            push @messages, {
+                type => 'success',
+                code => 'biblio_deleted',
+                biblionumber => $biblionumber,
+            };
+            $report->{total_success}++;
+            $dbh->commit;
+        } else {
+            # Authorities
+            my $authid = $record_id;
+            my $r = eval { C4::AuthoritiesMarc::DelAuthority( $authid ) };
+            if ( $r eq '0E0' or $@ ) {
+                push @messages, {
+                    type => 'error',
+                    code => 'authority_not_deleted',
+                    authid => $authid,
+                    error => ($@ ? $@ : 0),
+                };
+                $dbh->rollback;
+                next;
+            } else {
+                push @messages, {
+                    type => 'success',
+                    code => 'authority_deleted',
+                    authid => $authid,
+                };
+                $report->{total_success}++;
+                $dbh->commit;
+            }
+        }
+    }
+    $template->param(
+        op => 'report',
+        report => $report,
+    );
+}
+
+$template->param(
+    messages => \@messages,
+    recordtype => $recordtype,
+);
+
+output_html_with_http_headers $input, $cookie, $template->output;