Bug 11023: Automatic item modification by age (Was Toggle "new" status")
authorJonathan Druart <jonathan.druart@biblibre.com>
Wed, 25 Sep 2013 14:45:14 +0000 (16:45 +0200)
committerBrendan A Gallagher <brendan@bywatersolutions.com>
Wed, 2 Mar 2016 22:56:31 +0000 (22:56 +0000)
This patch adds:
- a new DB field items.new.
- a new page to configure this new status
  (tools/toggle_new_status.pl).
- a new cronjob script (misc/cronjobs/automatic_item_modification_by_age.pl
was misc/cronjob/toggle_new_status.pl)

Why this status is useful for some libraries ?
The use cases are:
- to know easily what are the new items (with a simple sql query).
- to display an icon in the search results.
- issuing rules can be adapt for new items. Automatically (using the
  cronjob script), the status change (depending the configuration) and
  the item can be issued, for example.
- a RSS/Atom feeds can be created on these new items.

Test plan:
- log in with a librarian having the tools > items_batchmod permission.
- navigate to Home > Tools > Automatic item modifications by age (was: Toggle new status)
- click on the edit button
- there are 3 "blocks":
  * duration: the duration during an item is considered as new.
  * conditions: the status will change only if the conditions are meet.
  * substitutions: if there is no substitution, no action will be done.
    You can add some change to apply to the matching items.
    E.g. ccode=3
         new=''
    If the value is an empty string (in other words, the input does not
    contain anything), the field will be deleted.
  You can create as many rules as you want.
- test the interface : add/remove rule, conditions, substitutions,
  submit the form, edit, etc.
  (There is a looot of JS everywhere, so certainly a looot of bugs...).
- when you have your rules defined, you can now launch the cronjob
  script without any parameter.
  A report will be displayed with the matching itemnumber and the
  substitutions to apply. Verify results are consistent.
- launch the script with the -c argument and verify values have been
  modified depending the substitution rules.

Signed-off-by: juliette et remy <juliette.levast@iepg.fr>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Bug 11023: Add the ability to specify fields from biblioitems table.

Test plan:
Same as before but try with fields from the biblioitems table.

Signed-off-by: juliette et remy <juliette.levast@iepg.fr>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Bug 11023: Add UT for C4::Items::ToggleNewStatus

Test plan:
prove t/db_dependent/Items/ToggleNewStatus.t

Signed-off-by: juliette et remy <juliette.levast@iepg.fr>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Bug 11023: FIX - condition on biblioitems table does not work

If a rule contains a condition on the biblioitems table, the match won't
work. This patch fixes this issue.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Bug 11023: Use DBIx-Class to retrieve column names

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Bug 11023: Don't use the biblioitems fields for the subtitution

It's dangerous to allow a change on the biblioitems fields with this
feature.

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Bug 11023: Rename the duration parameter with 'age'

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Bug 11023: The age parameter should be a number

The template should check if the age parameter is correctly filled
(should be a number).

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Bug 11023: Change the name of the feature

Originaly this feature only permits to update the "new" field.
Now all item fields can be updated.
The name of the feature is now "Automatic item modifications by age".

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Bug 11023 [QA Followup]

* Update DB version
* Fix capitalization error
* Rename misc/cronjobs/toggle_new_status.pl to misc/cronjobs/automatic_item_modification_by_age.pl

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Bug 11023 [QA Followup] - Complete the renaming of "toggle new status" to "automatic item modification by age"

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Brendan A Gallagher <brendan@bywatersolutions.com>
C4/Items.pm
koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css
koha-tmpl/intranet-tmpl/prog/en/modules/help/tools/automatic_item_modification_by_age.tt [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/tools/automatic_item_modification_by_age.tt [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt
misc/cronjobs/automatic_item_modification_by_age.pl [new file with mode: 0755]
t/db_dependent/Items/AutomaticItemModificationByAge.t [new file with mode: 0644]
tools/automatic_item_modification_by_age.pl [new file with mode: 0755]

index 26e9342..51479ec 100644 (file)
@@ -35,6 +35,7 @@ use DateTime::Format::MySQL;
 use Data::Dumper; # used as part of logging item record changes, not just for
                   # debugging; so please don't remove this
 use Koha::DateUtils qw/dt_from_string/;
+use Koha::Database;
 
 use Koha::Database;
 
@@ -473,6 +474,7 @@ sub _build_default_values_for_mod_marc {
         location                 => undef,
         permanent_location       => undef,
         materials                => undef,
+        new                      => undef,
         notforloan               => 0,
         # paidfor => undef, # commented, see bug 12817
         price                    => undef,
@@ -2198,7 +2200,8 @@ sub _koha_new_item {
             enumchron           = ?,
             more_subfields_xml  = ?,
             copynumber          = ?,
-            stocknumber         = ?
+            stocknumber         = ?,
+            new                 = ?
           ";
     my $sth = $dbh->prepare($query);
     my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 });
@@ -2242,6 +2245,7 @@ sub _koha_new_item {
             $item->{'more_subfields_xml'},
             $item->{'copynumber'},
             $item->{'stocknumber'},
+            $item->{'new'},
     );
 
     my $itemnumber;
@@ -3135,4 +3139,94 @@ sub PrepareItemrecordDisplay {
     };
 }
 
+=head2 columns
+
+  my @columns = C4::Items::columns();
+
+Returns an array of items' table columns on success,
+and an empty array on failure.
+
+=cut
+
+sub columns {
+    my $rs = Koha::Database->new->schema->resultset('Item');
+    return $rs->result_source->columns;
+}
+
+=head2 biblioitems_columns
+
+  my @columns = C4::Items::biblioitems_columns();
+
+Returns an array of biblioitems' table columns on success,
+and an empty array on failure.
+
+=cut
+
+sub biblioitems_columns {
+    my $rs = Koha::Database->new->schema->resultset('Biblioitem');
+    return $rs->result_source->columns;
+}
+
+sub ToggleNewStatus {
+    my ( $params ) = @_;
+    my @rules = @{ $params->{rules} };
+    my $report_only = $params->{report_only};
+
+    my $dbh = C4::Context->dbh;
+    my @errors;
+    my @item_columns = map { "items.$_" } C4::Items::columns;
+    my @biblioitem_columns = map { "biblioitems.$_" } C4::Items::biblioitems_columns;
+    my $report;
+    for my $rule ( @rules ) {
+        my $age = $rule->{age};
+        my $conditions = $rule->{conditions};
+        my $substitutions = $rule->{substitutions};
+        my @params;
+
+        my $query = q|
+            SELECT items.biblionumber, items.itemnumber
+            FROM items
+            LEFT JOIN biblioitems ON biblioitems.biblionumber = items.biblionumber
+            WHERE 1
+        |;
+        for my $condition ( @$conditions ) {
+            if (
+                 grep {/^$condition->{field}$/} @item_columns
+              or grep {/^$condition->{field}$/} @biblioitem_columns
+            ) {
+                if ( $condition->{value} =~ /\|/ ) {
+                    my @values = split /\|/, $condition->{value};
+                    $query .= qq| AND $condition->{field} IN (|
+                        . join( ',', ('?') x scalar @values )
+                        . q|)|;
+                    push @params, @values;
+                } else {
+                    $query .= qq| AND $condition->{field} = ?|;
+                    push @params, $condition->{value};
+                }
+            }
+        }
+        if ( defined $age ) {
+            $query .= q| AND TO_DAYS(NOW()) - TO_DAYS(dateaccessioned) >= ? |;
+            push @params, $age;
+        }
+        my $sth = $dbh->prepare($query);
+        $sth->execute( @params );
+        while ( my $values = $sth->fetchrow_hashref ) {
+            my $biblionumber = $values->{biblionumber};
+            my $itemnumber = $values->{itemnumber};
+            my $item = C4::Items::GetItem( $itemnumber );
+            for my $substitution ( @$substitutions ) {
+                next unless $substitution->{field};
+                C4::Items::ModItem( {$substitution->{field} => $substitution->{value}}, $biblionumber, $itemnumber )
+                    unless $report_only;
+                push @{ $report->{$itemnumber} }, $substitution;
+            }
+        }
+    }
+
+    return $report;
+}
+
+
 1;
index b8a857d..2328304 100644 (file)
@@ -2815,6 +2815,7 @@ span.onsite_checkout {
     border-radius: 4px;
     border : 1px solid #FFF2CE;
 }
+
 div.lastchecked {
     padding : .2em 1em;
     border: 2px solid #BCDB89;
@@ -2845,3 +2846,32 @@ div#cn_browser_table_wrapper > table#cn_browser_table {
    margin: auto;
    width:90%;
 }
+
+/* Tools > automatic_item_modification_by_age */
+div.rules {
+    display: block;
+}
+div#new_rule, div.rule {
+    background-color: #F4F8F9;
+    border: 2px solid #B9D8D9;
+    border-radius: 5px;
+    margin: .3em;
+    padding: .3em;
+}
+
+div.duration, div.blocks {
+    border: 2px solid #B9D8D9;
+    border-radius: 5px 5px 5px 5px;
+    margin: .3em;
+    padding: 0 .3em .3em .3em;
+}
+
+div.duration h5, div.blocks h5 {
+    padding-bottom: 4px;
+    padding-left: 0.2em;
+    background-color: #E6F0F2;
+    border-radius: 1px;
+}
+div.duration span, div.blocks div {
+    display:block;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/help/tools/automatic_item_modification_by_age.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/help/tools/automatic_item_modification_by_age.tt
new file mode 100644 (file)
index 0000000..8fdda1b
--- /dev/null
@@ -0,0 +1,45 @@
+[% INCLUDE 'help-top.inc' %]
+<h1>Automatic item modifications by age configuration</h1>
+
+<p>This configuration page allows to configure the rules for the automatic item modifications by age cronjob script.</p>
+
+<p>Libraries can manage the 'new' status for items. With this script, it will be possible to:<p>
+<ul>
+  <li>know easily what are the new items in the catalogue.</li>
+  <li>display an icon in the search results for new items.</li>
+  <li>configure issuing rules depending the 'new' status.</li>
+  <li>get a RSS/Atom feeds on these new items.</li>
+</ul>
+
+<h3>How to work the configuration page?</h3>
+<p>There are 3 values to define:</p>
+<h4>The duration</h4>
+<p>This value corresponds to the duration an item is considered as new.</p>
+<h4>The conditions</h4>
+<p>Conditions should be defined if you want to test some values before to substitute fields in the items.</p>
+<p>They are cumulatives but you can separate with a pipe '|' for a field with several values.</p>
+<h4>The substitutions</h4>
+<p>Substitutions are changes to apply to the matching items.</p>
+<p>At least one substitution must be defined, else there is no sense to launch the script.</p>
+<p>If the value is an empty string, the field will be deleted.</p>
+<h3>Examples</h3>
+<p>You want to remove the items.new value for items created 10 days ago:</p>
+<ul>
+  <li>Duration: 10 days</li>
+  <li>No condition</li>
+  <li>Substitution: items.new = '' (no value in the input)</li>
+</ul>
+
+<p>You want to change the items.ccode=1 to items.ccode=2 for items created 7 days ago.
+<ul>
+  <li>Duration: 7 days</li>
+  <li>Condition: items.ccode = 1</li>
+  <li>Substitution: items.ccode = 2</li>
+</ul>
+
+<h3>How to execute the cronjob script?</h3>
+<p>The cronjob script is misc/cronjobs/automatic_item_modification_by_age.pl.</p>
+<p>Try the -h parameter in order to see the help.</p>
+<p>Without any parameter, the script will be launched in a dry-run mode. If the -c (or --confirm) flag is given, the script will apply the changes.</p>
+
+[% INCLUDE 'help-bottom.inc' %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/automatic_item_modification_by_age.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/automatic_item_modification_by_age.tt
new file mode 100644 (file)
index 0000000..a08e96d
--- /dev/null
@@ -0,0 +1,294 @@
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha &rsaquo; Tools &rsaquo; Automatic item modifications by age</title>
+[% INCLUDE 'doc-head-close.inc' %]
+<script type="text/javascript">//<![CDATA[
+  function clear_inputs(node, new_node) {
+    var selects = $(node).find("select");
+    $(selects).each(function(i) {
+      var select = this;
+      $(new_node).find("select").eq(i).val($(select).val());
+    });
+    var inputs = $(node).find("input");
+    $(inputs).each(function(i) {
+      var input = this;
+      $(new_node).find("input").eq(i).val($(input).val());
+    });
+  }
+
+  function remove_block_action( link ) {
+    var blocks = $(link).parent().parent();
+    if( $(blocks).find(".block").length > 2 ) {
+      $(blocks).find("a.remove_block").show();
+    } else {
+      $(blocks).find("a.remove_block").hide();
+    }
+    $(link).parent().remove();
+  }
+
+  function remove_rule_action( link ) {
+    if( $("#rules").find("div.rule").length < 2 ) {
+        $("#rules").hide();
+        $("#norules").show();
+    }
+    $(link).parent().remove();
+  }
+
+  function clone_block(block) {
+    var new_block = $(block).clone(1);
+    clear_inputs(block, new_block);
+    $(new_block).find('a.remove_block').show();
+    var blocks = $(block).parent();
+    $(blocks).append(new_block);
+    $(blocks).find('a.remove_block').click(function(){
+      remove_block_action($(this));
+    }).show();
+  }
+
+  $(document).ready(function() {
+    $("#new_rule a.remove_rule").hide();
+    $("#new_rule a.remove_block").hide();
+    $("#rules a.remove_block").click(function(){
+      remove_block_action($(this));
+    });
+    $("#rules a.remove_rule").click(function(){
+      remove_rule_action($(this));
+    });
+
+    var unique_id = $("div.rule").length + 1;
+    $("a.add_rule").click(function(){
+      var rule = $("#new_rule");
+      var new_rule = $(rule).clone(1);
+      $(new_rule).removeAttr('id');
+      $(new_rule).attr('class', 'rule');
+      clear_inputs(rule, new_rule);
+      $(new_rule).find("select[name='condition_field']").attr('name', 'condition_field_' + unique_id);
+      $(new_rule).find("select[name='substitution_field']").attr('name', 'substitution_field_' + unique_id);
+      $(new_rule).find("input[name='condition_value']").attr('name', 'condition_value_' + unique_id);
+      $(new_rule).find("input[name='substitution_value']").attr('name', 'substitution_value_' + unique_id);
+      $(new_rule).find("input[name='age']").attr('name', 'age_' + unique_id);
+      $(new_rule).find("input[name='unique_id']").val(unique_id);
+
+      $("#rules").append(new_rule);
+
+      if( $("#rules").find("div.rule").length > 0 ) {
+          $("#rules").show();
+          $("#norules").hide();
+      }
+      if( $("#rules").find(".conditions > .condition").length > 1 ) {
+
+      }
+      if( $("#rules").find(".conditions > .condition").length > 1 ) {
+
+      }
+      $(new_rule).find('a.remove_rule').click(function(){
+        remove_rule_action( $(this) );
+      }).show();
+      $(new_rule).find('a.add_rule').remove();
+      unique_id++;
+    });
+
+    $("a.add_block").click(function(){
+      clone_block( $(this).parent() );
+    });
+
+    if( $("#rules").find("div.rule").length < 1 ) {
+        $("#rules").hide();
+        $("#norules").show();
+    }
+
+    $("#rules .rule .blocks").each(function(){
+      if ( $(this).find(".block").length == 1 ) {
+        $(this).find("a.remove_block").hide();
+      }
+    });
+
+    [% IF op == 'edit_form' %]
+      [% IF rules.size > 0 %]
+        $("#norules").hide();
+      [% ELSE %]
+        $("#rules").show();
+      [% END %]
+    [% END %]
+  });
+//]]>
+</script>
+</head>
+<body id="tools_automatic_item_modification_by_age" 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/automatic_item_modification_by_age.pl">Automatic item modifications by age</a></div>
+
+<div id="doc3" class="yui-t2">
+  <div id="bd">
+    <div id="yui-main">
+      <div class="yui-b">
+        <h3>Automatic item modifications by age</h3>
+        <div id="toolbar" class="btn-toolbar">
+          <a class="btn btn-small" id="newentry" href="/cgi-bin/koha/tools/automatic_item_modification_by_age.pl?op=edit_form"><i class="icon-plus"></i> Edit</a>
+        </div>
+        [% FOR message IN messages %]
+          [% IF message.type == "error" %]
+            <div class="dialog error">
+          [% END %]
+          [% IF message.code == "unable_to_load_configuration" %]
+            An error occurs: Unable to load the configuration.
+          [% END %]
+          </div>
+        [% END %]
+
+        [% IF op == 'edit_form' %]
+          <form method="post" action="/cgi-bin/koha/tools/automatic_item_modification_by_age.pl">
+            <div id="edit_rules">
+              <h4>List of rules</h4>
+                <div id="rules">
+                [% FOR rule IN rules %]
+                  [% SET id = loop.count %]
+                  <div class="rule">
+                    <input type="hidden" name="unique_id" value="[% loop.count %]" /> <!-- FIXME on update, the unique_id should be filled -->
+                    <div class="age">
+                      <h5>Age</h5>
+                      <input type="number" value="[% rule.age %]" name="age_[% id %]" /> days
+                    </div>
+                    <div class="blocks">
+                      <h5>Conditions</h5>
+                      [% FOR condition IN rule.conditions %]
+                        <div class="block">
+                          <select name="condition_field_[% id %]">
+                            <option value="">Choose a field name</option>
+                            [% FOR field IN condition_fields %]
+                              [% IF condition.field == field %]
+                                <option value="[% field %]" selected="selected">[% field %]</option>
+                              [% ELSE %]
+                                <option value="[% field %]">[% field %]</option>
+                              [% END %]
+                            [% END %]
+                          </select>
+                          =
+                          <input type="text" value="[% condition.value %]" name="condition_value_[% id%]" />
+                          <a class="add_block" style="cursor:pointer"><i class="icon-plus"></i></a>
+                          <a class="remove_block" style="cursor:pointer"><i class="icon-remove"></i></a>
+                        </div>
+                      [% END %]
+                    </div>
+                    <div class="blocks">
+                      <h5>Substitutions</h5>
+                      [% FOR substitution IN rule.substitutions %]
+                        <div class="block">
+                          <select name="substitution_field_[% id %]">
+                            <option value="">Choose a field name</option>
+                            [% FOR field IN substitution_fields %]
+                              [% IF substitution.field == field %]
+                                <option value="[% field %]" selected="selected">[% field %]</option>
+                              [% ELSE %]
+                                <option value="[% field %]">[% field %]</option>
+                              [% END %]
+                            [% END %]
+                          </select>
+                          =
+                          <input type="text" value="[% substitution.value %]" name="substitution_value_[% id %]" />
+                          <a class="add_block" style="cursor:pointer"><i class="icon-plus"></i></a>
+                          <a class="remove_block" style="cursor:pointer"><i class="icon-remove"></i></a>
+                        </div>
+                      [% END %]
+                    </div>
+                    <a class="remove_rule" style="cursor:pointer">Remove this rule</a>
+                  </div>
+                [% END %]
+                </div>
+                <div id="norules">
+                  There is no rule defined.
+                </div>
+              <fieldset class="action">
+                <input type="hidden" name="op" value="update" />
+                <a class="cancel" href="/cgi-bin/koha/tools/automatic_item_modification_by_age.pl">Cancel</a>
+                <input type="submit" value="Submit theses rules" />
+              </fieldset>
+            </div>
+          </form>
+          <h4>Add a new rule</h4>
+          <div id="new_rule">
+            <input type="hidden" name="unique_id" />
+            <div class="age">
+              <h5>Age</h5>
+              <input type="number" value="" name="age" /> days
+            </div>
+            <div class="blocks">
+              <h5>Conditions</h5>
+              <div class="block">
+                <select name="condition_field">
+                  <option value="">Choose a field name</option>
+                  [% FOR field IN condition_fields %]
+                    <option value="[% field %]">[% field %]</option>
+                  [% END %]
+                </select>
+                =
+                <input type="text" value="" name="condition_value" />
+                <a class="add_block" style="cursor:pointer"><i class="icon-plus"></i></a>
+                <a class="remove_block" style="cursor:pointer"><i class="icon-remove"></i></a>
+              </div>
+            </div>
+            <div class="blocks">
+              <h5>Substitutions</h5>
+              <div class="block">
+                <select name="substitution_field">
+                  <option value="">Choose a field name</option>
+                  [% FOR field IN substitution_fields %]
+                    <option value="[% field %]">[% field %]</option>
+                  [% END %]
+                </select>
+                =
+                <input type="text" value="" name="substitution_value" />
+                <a class="add_block" style="cursor:pointer"><i class="icon-plus"></i></a>
+                <a class="remove_block" style="cursor:pointer"><i class="icon-remove"></i></a>
+              </div>
+            </div>
+          <a class="add_rule" style="cursor:pointer">Add this rule</a>
+          <a class="remove_rule" style="cursor:pointer">Remove this rule</a>
+          </div>
+        [% ELSIF rules and op == 'show' %]
+          <div id="rules">
+            <h4>List of rules</h4>
+            [% FOR rule IN rules %]
+              <div class="rule">
+                <div class="age">
+                  <h5>Age</h5>
+                  [% IF rule.age.defined and rule.age.length > 0 %]
+                    [% rule.age %] days
+                  [% ELSE %]
+                    There is no age for this rule.
+                  [% END %]
+                </div>
+                <div class="blocks">
+                  <h5>Conditions</h5>
+                  [% FOR condition IN rule.conditions %]
+                    [% IF condition.field %]
+                      <div class="block">
+                        [% condition.field %] = [% condition.value %]
+                      </div>
+                    [% ELSE %]
+                      There is no condition for this rule.
+                    [% END %]
+                  [% END %]
+                </div>
+                <div class="blocks">
+                  <h5>Substitutions</h5>
+                  [% FOR substitution IN rule.substitutions %]
+                    <div class="block">
+                      [% substitution.field %] = [% substitution.value %]
+                    </div>
+                  [% END %]
+                </div>
+              </div>
+            [% END %]
+          </div>
+        [% ELSE %]
+          There is no rule defined. Please click on the edit button.
+        [% END %]
+
+      </div>
+    </div>
+  <div class="yui-b noprint">
+    [% INCLUDE 'tools-menu.inc' %]
+  </div>
+</div>
+[% INCLUDE 'intranet-bottom.inc' %]
index 629ce84..bb54f54 100644 (file)
       <dd>Modify a batch of records (biblios or authorities)</dd>
     [% END %]
 
+    [% IF ( CAN_user_tools_items_batchmod ) %]
+      <dt><a href="/cgi-bin/koha/tools/automatic_item_modification_by_age.pl">Automatic item modifications by age</a></dt>
+      <dd>Define rules to modify items by age</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/misc/cronjobs/automatic_item_modification_by_age.pl b/misc/cronjobs/automatic_item_modification_by_age.pl
new file mode 100755 (executable)
index 0000000..51effd3
--- /dev/null
@@ -0,0 +1,106 @@
+#!/usr/bin/perl
+
+use Modern::Perl;
+
+use Getopt::Long;
+use Pod::Usage;
+use JSON;
+
+use C4::Context;
+use C4::Items;
+
+# Getting options
+my ( $verbose, $help, $confirm );
+my $result = GetOptions(
+    'h|help'    => \$help,
+    'v|verbose' => \$verbose,
+    'c|confirm' => \$confirm,
+);
+
+pod2usage(1) if $help;
+$verbose = 1 unless $confirm;
+
+# Load configuration from the syspref
+my $syspref_content = C4::Context->preference('automatic_item_modification_by_age_configuration');
+my $rules = eval { JSON::from_json( $syspref_content ) };
+pod2usage({ -message => "Unable to load the configuration : $@", -exitval => 1 })
+    if $@;
+
+my $report = C4::Items::ToggleNewStatus( { rules => $rules, report_only => not $confirm } );
+
+if ( $verbose ) {
+    if ( $report ) {
+        say "Item to modify:";
+        while ( my ( $itemnumber, $substitutions ) = each %$report ) {
+            for my $substitution ( @$substitutions ) {
+                if ( defined $substitution->{value} and $substitution->{value} ne q|| ) {
+                   say "\titemnumber $itemnumber: $substitution->{field}=$substitution->{value}";
+                } else {
+                   say "\titemnumber $itemnumber: field $substitution->{field} to delete";
+                }
+            }
+        }
+    } else {
+        say "There is no item to modify";
+    }
+}
+
+exit(0);
+
+__END__
+
+=head1 NAME
+
+automatic_item_modification_by_age.pl
+
+=head1 SYNOPSIS
+
+./automatic_item_modification_by_age.pl -h
+
+Toggle recent acquisitions status.
+Use this script to delete "new" status for items.
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<-h|--help>
+
+Prints this help message.
+
+=item B<-v|--verbose>
+
+Set the verbose flag.
+
+=item B<-c|--confirm>
+
+The script will modify the items.
+
+=back
+
+=head1 AUTHOR
+
+Jonathan Druart <jonathan.druart@biblibre.com>
+
+=head1 COPYRIGHT
+
+Copyright 2013 BibLibre
+
+=head1 LICENSE
+
+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>.
+
+=cut
diff --git a/t/db_dependent/Items/AutomaticItemModificationByAge.t b/t/db_dependent/Items/AutomaticItemModificationByAge.t
new file mode 100644 (file)
index 0000000..3f2943c
--- /dev/null
@@ -0,0 +1,283 @@
+#!/usr/bin/perl
+
+use Modern::Perl;
+use Test::More tests => 16;
+use MARC::Record;
+use MARC::Field;
+use DateTime;
+use DateTime::Duration;
+
+use C4::Biblio;
+use C4::Context;
+use C4::Items;
+use Koha::DateUtils;
+
+my $dbh = C4::Context->dbh;
+$dbh->{AutoCommit} = 0;
+$dbh->{RaiseError} = 1;
+
+$dbh->do(q|
+    DELETE FROM marc_subfield_structure
+    WHERE kohafield = 'items.new' OR kohafield = 'items.stocknumber'
+|);
+
+my $new_tagfield = 'i';
+$dbh->do(qq|
+    INSERT INTO marc_subfield_structure(tagfield, tagsubfield, kohafield, frameworkcode)
+    VALUES ( 952, '$new_tagfield', 'items.new', '' )
+|);
+
+my $record = MARC::Record->new();
+$record->append_fields(
+    MARC::Field->new('100', ' ', ' ', a => 'Moffat, Steven'),
+    MARC::Field->new('245', ' ', ' ', a => 'Silence in the library'),
+    MARC::Field->new('942', ' ', ' ', c => 'ITEMTYPE_T'),
+);
+my ($biblionumber, undef) = C4::Biblio::AddBiblio($record, '');
+
+my ($item_bibnum, $item_bibitemnum, $itemnumber) = C4::Items::AddItem(
+    {
+        homebranch => 'CPL',
+        holdingbranch => 'CPL',
+        new => 'new_value',
+        ccode => 'FIC',
+    },
+    $biblionumber
+);
+
+my $item = C4::Items::GetItem( $itemnumber );
+is ( $item->{new}, 'new_value', q|AddItem insert the 'new' field| );
+
+my ( $tagfield, undef ) = GetMarcFromKohaField('items.itemnumber', '');
+my $marc_item = C4::Items::GetMarcItem( $biblionumber, $itemnumber );
+is( $marc_item->subfield($tagfield, $new_tagfield), 'new_value', q|Koha mapping is correct|);
+
+# Update the items.new field if items.ccode eq 'FIC' => should be updated
+my @rules = (
+    {
+        conditions => [
+            {
+                field => 'items.ccode',
+                value => 'FIC',
+            },
+        ],
+        substitutions => [
+            {
+                field => 'items.new',
+                value => 'updated_value',
+             },
+        ],
+        age => '0',
+    },
+);
+
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+
+my $modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, 'updated_value', q|ToggleNewStatus: The new value is updated|);
+$marc_item = C4::Items::GetMarcItem( $biblionumber, $itemnumber );
+is( $marc_item->subfield($tagfield, $new_tagfield), 'updated_value', q|ToggleNewStatus: The new value is updated| );
+
+# Update the items.new field if items.ccode eq 'DONT_EXIST' => should not be updated
+@rules = (
+    {
+        conditions => [
+            {
+                field => 'items.ccode',
+                value => 'DONT_EXIST',
+            },
+        ],
+        substitutions => [
+            {
+                field => 'items.new',
+                value => 'new_updated_value',
+             },
+        ],
+        age => '0',
+    },
+);
+
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+
+$modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, 'updated_value', q|ToggleNewStatus: The new value is not updated|);
+$marc_item = C4::Items::GetMarcItem( $biblionumber, $itemnumber );
+is( $marc_item->subfield($tagfield, $new_tagfield), 'updated_value', q|ToggleNewStatus: The new value is not updated| );
+
+# Play with age
+$item = C4::Items::GetItem( $itemnumber );
+my $dt_today = dt_from_string;
+my $days5ago = $dt_today->add_duration( DateTime::Duration->new( days => -5 ) );
+
+C4::Items::ModItem( { dateaccessioned => $days5ago }, $biblionumber, $itemnumber );
+$item = C4::Items::GetItem( $itemnumber );
+
+@rules = (
+    {
+        conditions => [
+            {
+                field => 'items.ccode',
+                value => 'FIC',
+            },
+        ],
+        substitutions => [
+            {
+                field => 'items.new',
+                value => 'new_updated_value',
+             },
+        ],
+        age => '10',
+    },
+);
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+$modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, 'updated_value', q|ToggleNewStatus: Age = 10 : The new value is not updated|);
+
+$rules[0]->{age} = 5;
+$rules[0]->{substitutions}[0]{value} = 'new_updated_value5';
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+$modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, 'new_updated_value5', q|ToggleNewStatus: Age = 5 : The new value is updated|);
+
+$rules[0]->{age} = '';
+$rules[0]->{substitutions}[0]{value} = 'new_updated_value_empty_string';
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+$modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, 'new_updated_value_empty_string', q|ToggleNewStatus: Age = '' : The new value is updated|);
+
+$rules[0]->{age} = undef;
+$rules[0]->{substitutions}[0]{value} = 'new_updated_value_undef';
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+$modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, 'new_updated_value_undef', q|ToggleNewStatus: Age = undef : The new value is updated|);
+
+# Field deletion
+@rules = (
+    {
+        conditions => [
+            {
+                field => 'items.ccode',
+                value => 'FIC',
+            },
+        ],
+        substitutions => [
+            {
+                field => 'items.new',
+                value => '',
+             },
+        ],
+        age => '0',
+    },
+);
+
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+
+$modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, '', q|ToggleNewStatus: The new value is empty|);
+$marc_item = C4::Items::GetMarcItem( $biblionumber, $itemnumber );
+is( $marc_item->subfield($tagfield, $new_tagfield), undef, q|ToggleNewStatus: The new field is removed from the item marc| );
+
+# conditions multiple
+@rules = (
+    {
+        conditions => [
+            {
+                field => 'items.ccode',
+                value => 'FIC',
+            },
+            {
+                field => 'items.homebranch',
+                value => 'CPL',
+            },
+        ],
+        substitutions => [
+            {
+                field => 'items.new',
+                value => 'new_value',
+             },
+        ],
+        age => '0',
+    },
+);
+
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+
+$modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, 'new_value', q|ToggleNewStatus: conditions multiple: all match, the new value is updated|);
+
+@rules = (
+    {
+        conditions => [
+            {
+                field => 'items.ccode',
+                value => 'FIC',
+            },
+            {
+                field => 'items.homebranch',
+                value => 'DONT_EXIST',
+            },
+        ],
+        substitutions => [
+            {
+                field => 'items.new',
+                value => 'new_updated_value',
+             },
+        ],
+        age => '0',
+    },
+);
+
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+
+$modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, 'new_value', q|ToggleNewStatus: conditions multiple: at least 1 condition does not match, the new value is not updated|);
+
+@rules = (
+    {
+        conditions => [
+            {
+                field => 'items.ccode',
+                value => 'FIC|NFIC',
+            },
+            {
+                field => 'items.homebranch',
+                value => 'MPL|CPL',
+            },
+        ],
+        substitutions => [
+            {
+                field => 'items.new',
+                value => 'new_updated_value',
+             },
+        ],
+        age => '0',
+    },
+);
+
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+
+$modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, 'new_updated_value', q|ToggleNewStatus: conditions multiple: the 2 conditions match, the new value is updated|);
+
+@rules = (
+    {
+        conditions => [
+            {
+                field => 'biblioitems.itemtype',
+                value => 'ITEMTYPE_T',
+            },
+        ],
+        substitutions => [
+            {
+                field => 'items.new',
+                value => 'another_new_updated_value',
+             },
+        ],
+        age => '0',
+    },
+);
+
+C4::Items::ToggleNewStatus( { rules => \@rules } );
+
+$modified_item = C4::Items::GetItem( $itemnumber );
+is( $modified_item->{new}, 'another_new_updated_value', q|ToggleNewStatus: conditions on biblioitems|);
diff --git a/tools/automatic_item_modification_by_age.pl b/tools/automatic_item_modification_by_age.pl
new file mode 100755 (executable)
index 0000000..eca339c
--- /dev/null
@@ -0,0 +1,119 @@
+#!/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>.
+
+=head1 NAME
+
+automatic_item_modification_by_age.pl: Update new status for items.
+
+=cut
+
+=head1 DESCRIPTION
+
+This script allows a user to update the new status for items.
+
+=cut
+
+use Modern::Perl;
+
+use CGI;
+use JSON qw( to_json from_json );
+
+use C4::Auth;
+use C4::Context;
+use C4::Items;
+use C4::Output;
+use C4::Koha;
+
+my $cgi = new CGI;
+
+# open template
+my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
+    {
+        template_name   => "tools/automatic_item_modification_by_age.tt",
+        query           => $cgi,
+        type            => "intranet",
+        authnotrequired => 0,
+        flagsrequired   => { tools => 'items_batchmod' },
+    }
+);
+
+my $op = $cgi->param('op') // 'show';
+
+my $syspref_name = q|automatic_item_modification_by_age_configuration|;
+if ( $op eq 'update' ) {
+    my @rules;
+    my @unique_ids = $cgi->param('unique_id');
+    for my $unique_id ( @unique_ids ) {
+        my @substitution_fields = $cgi->param("substitution_field_$unique_id");
+        my @substitution_values = $cgi->param("substitution_value_$unique_id");
+        my @condition_fields = $cgi->param("condition_field_$unique_id");
+        my @condition_values = $cgi->param("condition_value_$unique_id");
+        my $rule = {
+            substitutions => [],
+            conditions => [],
+        };
+        for my $value ( @substitution_values ) {
+            my $field = shift @substitution_fields;
+            last unless $field;
+            push @{ $rule->{substitutions} }, { field => $field, value => $value };
+        }
+        push @{ $rule->{substitutions} }, {}
+            unless @{ $rule->{substitutions} };
+        for my $value ( @condition_values ) {
+            my $field = shift @condition_fields;
+            last unless $field;
+            push @{ $rule->{conditions} }, { field => $field, value => $value };
+        }
+        push @{ $rule->{conditions} }, {}
+            unless @{ $rule->{conditions} };
+        $rule->{age} = $cgi->param("age_$unique_id");
+        push @rules, $rule;
+    }
+    my $syspref_content = to_json( \@rules );
+    C4::Context->set_preference($syspref_name, $syspref_content);
+
+    $op = 'show';
+}
+
+my @messages;
+my $syspref_content = C4::Context->preference($syspref_name);
+my $rules;
+$rules = eval { JSON::from_json( $syspref_content ) }
+    if $syspref_content;
+if ( $@ ) {
+    push @messages, {
+        type => 'error',
+        code => 'unable_to_load_configuration'
+    };
+    $template->param( messages => \@messages );
+    output_html_with_http_headers $cgi, $cookie, $template->output;
+    exit;
+}
+
+my @item_fields = map { "items.$_" } C4::Items::columns;
+my @biblioitem_fields = map { "biblioitems.$_" } C4::Items::biblioitems_columns;
+$template->param(
+    op => $op,
+    messages => \@messages,
+    condition_fields => [ @item_fields, @biblioitem_fields ],
+    substitution_fields => \@item_fields,
+    rules => $rules,
+);
+
+output_html_with_http_headers $cgi, $cookie, $template->output;