Bugzilla – Attachment 174116 Details for
Bug 38387
Automatic item modification at checkin/checkout
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 38387: Automatic item modification at checkin/checkout
Bug-38387-Automatic-item-modification-at-checkinch.patch (text/plain), 46.09 KB, created by
Julian Maurice
on 2024-11-07 11:26:50 UTC
(
hide
)
Description:
Bug 38387: Automatic item modification at checkin/checkout
Filename:
MIME Type:
Creator:
Julian Maurice
Created:
2024-11-07 11:26:50 UTC
Size:
46.09 KB
patch
obsolete
>From f2ca800a2a67bf8fd20f46b6cd0f350ea284b204 Mon Sep 17 00:00:00 2001 >From: Julian Maurice <julian.maurice@biblibre.com> >Date: Mon, 17 Jun 2024 15:49:14 +0200 >Subject: [PATCH] Bug 38387: Automatic item modification at checkin/checkout > >This replaces system preferences UpdateItemLocationOnCheckin and >UpdateItemLocationOnCheckout by a new cataloguing tool > >The new tool allows to modify any `items` column, based on "rules". >Each rule can have multiple conditions (or zero) and multiple >substitutions. >Conditions can be made on any `biblio`, `biblioitems` or `items` column. >The new value can be a fixed string (as with UpdateItemLocationOn* >sysprefs) or the value of another `items` column, using the syntax: >$items.<column> (for instance: $items.permanent_location) > >The tool's UI is based on "automatic item modifications by age" so it >should feel very similar. > >Test plan. >1. Set a value for UpdateItemLocationOnCheckin and > UpdateItemLocationOnCheckout. They will be transferred to the new > tool >2. Apply patch, run updatedatabase, update DBIC schema, restart >3. Go to "Cataloging > Item modifications by event" >4. Verify that UpdateItemLocationOn* sysprefs content has been > transferred correctly: > > - If syspref content had a key '_ALL_', you should have only one rule > with no conditions. Other keys are ignored. Otherwise, each > key-value pair is transformed to a new rule with one condition > ("items.location = <YAML key>") and one substitution > ("items.location = <YAML value>") > - The string '_BLANK_' is transformed to an empty string (''), > whether it's a key or a value > - The string '_PERM_' is transformed to '$items.permanent_location' > when it's a value > - For each created rule, another substitution is added > ("items.permanent_location = <YAML value>") unless the value is > 'PROC' or 'CART' > > This should match the current behavior. >5. Add other rules if needed, check out/check in some items and see your > modifications applied. >6. prove t/db_dependent/Koha/Item/trigger_automatic_modifications_by_event.t >--- > C4/Circulation.pm | 26 +- > Koha/Item.pm | 133 ++++---- > .../move-sysprefs-UpdateItemLocationOn.pl | 79 +++++ > .../prog/en/includes/cat-menu.inc | 3 + > .../en/modules/cataloguing/cataloging-home.tt | 3 + > .../automatic_item_modification_by_event.tt | 295 ++++++++++++++++++ > .../automatic_item_modification_by_event.js | 120 +++++++ > t/db_dependent/Koha/Item.t | 63 +--- > ...trigger_automatic_modifications_by_event.t | 168 ++++++++++ > tools/automatic_item_modification_by_event.pl | 114 +++++++ > 10 files changed, 865 insertions(+), 139 deletions(-) > create mode 100644 installer/data/mysql/atomicupdate/move-sysprefs-UpdateItemLocationOn.pl > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/automatic_item_modification_by_event.tt > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/automatic_item_modification_by_event.js > create mode 100755 t/db_dependent/Koha/Item/trigger_automatic_modifications_by_event.t > create mode 100755 tools/automatic_item_modification_by_event.pl > >diff --git a/C4/Circulation.pm b/C4/Circulation.pm >index 58946ae12a..ee678b8824 100644 >--- a/C4/Circulation.pm >+++ b/C4/Circulation.pm >@@ -1742,8 +1742,14 @@ sub AddIssue { > CartToShelf( $item_object->itemnumber ); > } > >- # Update item location >- $item_object->location_update_trigger( 'checkout' ); >+ $item_object->trigger_automatic_modifications_by_event('checkout'); >+ $item_object->store( >+ { >+ log_action => 0, >+ skip_record_index => 1, >+ skip_holds_queue => 1 >+ } >+ ); > > if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) { > UpdateTotalIssues( $item_object->biblionumber, 1, undef, { skip_holds_queue => 1 } ); >@@ -2209,10 +2215,18 @@ sub AddReturn { > my $borrowernumber = $patron ? $patron->borrowernumber : undef; # we don't know if we had a borrower or not > my $patron_unblessed = $patron ? $patron->unblessed : {}; > >- # Update item location >- my $loc_messages = $item->location_update_trigger( 'checkin' ); >- foreach my $loc_msg_key ( keys %$loc_messages ) { >- $messages->{ $loc_msg_key } = $loc_messages->{ $loc_msg_key }; >+ my $item_location = $item->location; >+ $item->trigger_automatic_modifications_by_event('checkin'); >+ $item->store( >+ { >+ log_action => 0, >+ skip_record_index => 1, >+ skip_holds_queue => 1 >+ } >+ ); >+ >+ if ( $item_location ne $item->location ) { >+ $messages->{ItemLocationUpdated} = { from => $item_location, to => $item->location }; > } > > my $rules = C4::Context->yaml_preference('UpdateNotForLoanStatusOnCheckin'); >diff --git a/Koha/Item.pm b/Koha/Item.pm >index 572b5f1ff6..e4ce9ddf4e 100644 >--- a/Koha/Item.pm >+++ b/Koha/Item.pm >@@ -2582,93 +2582,84 @@ sub strings_map { > } > > >-=head3 location_update_trigger >+=head3 trigger_automatic_modifications_by_event > >- $item->location_update_trigger( $action ); >+Update item properties according to the configuration stored in system >+preference C<automatic_item_modification_by_event_configuration> > >-Updates the item location based on I<$action>. It is done like this: >+ $item->trigger_automatic_modifications_by_event($event) > >-=over 4 >+C<$event> can be either C<'checkin'> or C<'checkout'> > >-=item For B<checkin>, location is updated following the I<UpdateItemLocationOnCheckin> preference. >- >-=item For B<checkout>, location is updated following the I<UpdateItemLocationOnCheckout> preference. >- >-=back >- >-FIXME: It should return I<$self>. See bug 35270. >+This method does not call C<store>. Caller is responsible for calling C<store> >+if modifications need to be saved. > > =cut > >-sub location_update_trigger { >- my ( $self, $action ) = @_; >+sub trigger_automatic_modifications_by_event { >+ my ($self, $event) = @_; > >- my ( $update_loc_rules, $messages ); >- if ( $action eq 'checkin' ) { >- $update_loc_rules = C4::Context->yaml_preference('UpdateItemLocationOnCheckin'); >- } else { >- $update_loc_rules = C4::Context->yaml_preference('UpdateItemLocationOnCheckout'); >+ my @rules; >+ my $rules_json = C4::Context->preference('automatic_item_modification_by_event_configuration'); >+ if ($rules_json) { >+ require JSON; >+ @rules = @{ JSON::decode_json($rules_json) }; > } > >- if ($update_loc_rules) { >- if ( defined $update_loc_rules->{_ALL_} ) { >- if ( $update_loc_rules->{_ALL_} eq '_PERM_' ) { >- $update_loc_rules->{_ALL_} = $self->permanent_location; >+ @rules = grep { $_->{event} eq $event } @rules; >+ >+ RULE: foreach my $rule (@rules) { >+ my @conditions = @{ $rule->{conditions} // [] }; >+ foreach my $condition (@conditions) { >+ my $field = $condition->{field}; >+ next unless $field; >+ >+ my $item_value; >+ if ($field =~ /^biblio\.(\w+)$/) { >+ my $item_field = $1; >+ $item_value = $self->biblio->get_column($item_field); >+ } elsif ($field =~ /^biblioitems\.(\w+)$/) { >+ my $item_field = $1; >+ $item_value = $self->biblioitem->get_column($item_field); >+ } elsif ($field =~ /^items\.(\w+)$/) { >+ my $item_field = $1; >+ $item_value = $self->get_column($item_field); >+ } else { >+ warn "Unknown condition field: $field"; >+ next; > } >- if ( $update_loc_rules->{_ALL_} eq '_BLANK_' ) { >- $update_loc_rules->{_ALL_} = ''; >+ >+ # Consider that NULL and empty strings are the same >+ $item_value //= ''; >+ my $condition_value = $condition->{value} // ''; >+ >+ if ( $item_value ne $condition_value ) { >+ next RULE; > } >- if ( >- ( defined $self->location && $self->location ne $update_loc_rules->{_ALL_} ) >- || ( !defined $self->location >- && $update_loc_rules->{_ALL_} ne "" ) >- ) >- { >- $messages->{'ItemLocationUpdated'} = >- { from => $self->location, to => $update_loc_rules->{_ALL_} }; >- $self->location( $update_loc_rules->{_ALL_} )->store( >- { >- log_action => 0, >- skip_record_index => 1, >- skip_holds_queue => 1 >- } >- ); >+ } >+ >+ my @substitutions = @{ $rule->{substitutions} // [] }; >+ foreach my $substitution (@substitutions) { >+ my $field = $substitution->{field}; >+ next unless $field; >+ >+ my $item_field; >+ if ($field =~ /^items\.(\w+)$/) { >+ $item_field = $1; >+ } else { >+ warn "Unknown substitution field: $field"; >+ next; > } >- } else { >- foreach my $key ( keys %$update_loc_rules ) { >- if ( $update_loc_rules->{$key} eq '_PERM_' ) { >- $update_loc_rules->{$key} = $self->permanent_location; >- } elsif ( $update_loc_rules->{$key} eq '_BLANK_' ) { >- $update_loc_rules->{$key} = ''; >- } >- if ( >- ( >- defined $self->location >- && $self->location eq $key >- && $self->location ne $update_loc_rules->{$key} >- ) >- || ( $key eq '_BLANK_' >- && ( !defined $self->location || $self->location eq '' ) >- && $update_loc_rules->{$key} ne '' ) >- ) >- { >- $messages->{'ItemLocationUpdated'} = { >- from => $self->location, >- to => $update_loc_rules->{$key} >- }; >- $self->location( $update_loc_rules->{$key} )->store( >- { >- log_action => 0, >- skip_record_index => 1, >- skip_holds_queue => 1 >- } >- ); >- last; >- } >+ >+ my $value = $substitution->{value} // ''; >+ if ($value =~ /^\$items\.(\w+)$/) { >+ my $value_field = $1; >+ $value = $self->get_column($value_field); > } >+ >+ $self->set({ $item_field => $value }); > } > } >- return $messages; > } > > >diff --git a/installer/data/mysql/atomicupdate/move-sysprefs-UpdateItemLocationOn.pl b/installer/data/mysql/atomicupdate/move-sysprefs-UpdateItemLocationOn.pl >new file mode 100644 >index 0000000000..7991314523 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/move-sysprefs-UpdateItemLocationOn.pl >@@ -0,0 +1,79 @@ >+use Modern::Perl; >+ >+use YAML::XS; >+use JSON; >+ >+return { >+ bug_number => '38387', >+ description => 'Move sysprefs UpdateItemLocationOnCheckin and UpdateItemLocationOnCheckout', >+ up => sub { >+ my ($args) = @_; >+ my ( $dbh, $out ) = @$args{qw(dbh out)}; >+ >+ my @sysprefs = ( >+ { name => 'UpdateItemLocationOnCheckin', event => 'checkin' }, >+ { name => 'UpdateItemLocationOnCheckout', event => 'checkout' }, >+ ); >+ >+ my @new_rules; >+ foreach my $syspref (@sysprefs) { >+ my $yaml = $dbh->selectrow_array('SELECT value FROM systempreferences WHERE variable = ?', undef, $syspref->{name}); >+ next unless $yaml; >+ >+ my $rules = eval { YAML::XS::Load( Encode::encode_utf8( $yaml ) ); }; >+ if ($@) { >+ die sprintf('Unable to parse %s system preference : %s', $syspref->{name}, $@); >+ } >+ >+ unless (ref $rules && ref $rules eq 'HASH') { >+ die sprintf('System preference %s is not a YAML mapping', $syspref->{name}); >+ } >+ >+ if (defined $rules->{_ALL_}) { >+ my $value = $rules->{_ALL_}; >+ if ($value eq '_PERM_') { >+ $value = '$items.permanent_location'; >+ } elsif ($value eq '_BLANK_') { >+ $value = ''; >+ } >+ >+ my $new_rule = { >+ event => $syspref->{event}, >+ conditions => [], >+ substitutions => [ { field => 'items.location', value => $value } ], >+ }; >+ if ($value ne 'PROC' && $value ne 'CART') { >+ push @{ $new_rule->{substitutions} }, { field => 'items.permanent_location', value => $value }; >+ } >+ push @new_rules, $new_rule; >+ } else { >+ while (my ($key, $value) = each %$rules) { >+ if ($key eq '_BLANK_') { >+ $key = ''; >+ } >+ if ($value eq '_PERM_') { >+ $value = '$items.permanent_location'; >+ } elsif ($value eq '_BLANK_') { >+ $value = ''; >+ } >+ >+ my $new_rule = { >+ event => $syspref->{event}, >+ conditions => [ { field => 'items.location', value => $key } ], >+ substitutions => [ { field => 'items.location', value => $value } ], >+ }; >+ if ($value ne 'PROC' && $value ne 'CART') { >+ push @{ $new_rule->{substitutions} }, { field => 'items.permanent_location', value => $value }; >+ } >+ push @new_rules, $new_rule; >+ } >+ } >+ } >+ >+ $dbh->do('INSERT IGNORE INTO systempreferences (variable, value) VALUES (?, ?)', undef, 'automatic_item_modification_by_event_configuration', encode_json(\@new_rules)); >+ say $out sprintf('Added %d rules for automatic item modifications by event', scalar @new_rules); >+ >+ $dbh->do("DELETE FROM systempreferences WHERE variable IN ('UpdateItemLocationOnCheckin', 'UpdateItemLocationOnCheckout')"); >+ say $out sprintf('Removed system preferences UpdateItemLocationOnCheckin and UpdateItemLocationOnCheckout') >+ }, >+}; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc >index 90cbd56bf2..d87af2c3e6 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc >@@ -83,6 +83,9 @@ > <li> > <a href="/cgi-bin/koha/tools/automatic_item_modification_by_age.pl">Item modifications by age</a> > </li> >+ <li> >+ <a href="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl">Item modifications by event</a> >+ </li> > [% END %] > [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %] > <li> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt >index 0bfd275638..216154debd 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt >@@ -163,6 +163,9 @@ > <li> > <a class="circ-button" href="/cgi-bin/koha/tools/automatic_item_modification_by_age.pl"><i class="fa-solid fa-calendar-days"></i> Item modifications by age</a> > </li> >+ <li> >+ <a class="circ-button" href="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl"><i class="fa-solid fa-right-left fa-rotate-90"></i> Item modifications by event</a> >+ </li> > [% END %] > [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %] > <li> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/automatic_item_modification_by_event.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/automatic_item_modification_by_event.tt >new file mode 100644 >index 0000000000..5dcfb264ea >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/automatic_item_modification_by_event.tt >@@ -0,0 +1,295 @@ >+[% USE raw %] >+[% USE Asset %] >+[% PROCESS 'i18n.inc' %] >+[% SET footerjs = 1 %] >+[% INCLUDE 'doc-head-open.inc' %] >+<title>[% FILTER collapse %] >+ [% IF ( op == 'edit_form' ) %] >+ [% t("Rules") | html %] › >+ [% END %] >+ [% t("Automatic item modifications by event") | html %] › >+ [% t("Cataloging") | html %] › >+ [% t("Koha") | html %] >+[% END %]</title> >+[% INCLUDE 'doc-head-close.inc' %] >+</head> >+ >+<body id="tools_automatic_item_modification_by_event" class="tools"> >+[% WRAPPER 'header.inc' %] >+ [% INCLUDE 'cat-search.inc' %] >+[% END %] >+ >+[% WRAPPER 'sub-header.inc' %] >+ [% WRAPPER breadcrumbs %] >+ [% WRAPPER breadcrumb_item %] >+ <a href="/cgi-bin/koha/cataloguing/cataloging-home.pl">Cataloging</a> >+ [% END %] >+ >+ [% IF ( op == 'edit_form' ) %] >+ [% WRAPPER breadcrumb_item %] >+ <a href="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl">Automatic item modifications by event</a> >+ [% END %] >+ [% WRAPPER breadcrumb_item bc_active= 1 %] >+ <span>Rules</span> >+ [% END %] >+ [% ELSE %] >+ [% WRAPPER breadcrumb_item bc_active= 1 %] >+ <span>Automatic item modifications by event</span> >+ [% END %] >+ [% END %] >+ [% END #/ WRAPPER breadcrumbs %] >+[% END #/ WRAPPER sub-header.inc %] >+ >+<div class="main container-fluid"> >+ <div class="row"> >+ <div class="col-md-10 order-md-2 order-sm-1"> >+ <main> >+ [% INCLUDE 'messages.inc' %] >+ >+ [% IF ( op == 'edit_form' ) %] >+ <datalist id="substitution-fields"> >+ [% FOREACH substitution_field IN substitution_fields %] >+ <option value="$[% substitution_field | html %]">$[% substitution_field | html %]</option> >+ [% END %] >+ </datalist> >+ >+ <form method="post" id="rules_form" action="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <h1>Rules for automatic item modifications by event</h1> >+ <div id="toolbar" class="btn-toolbar"> >+ <div class="btn-group"> >+ <button class="btn btn-default add_rule"><i class="fa fa-plus"></i> Add rule</button> >+ </div> >+ <div class="btn-group"> >+ <button type="submit" id="save_rules" class="btn btn-default"><i class="fa fa-save"></i> Save</button> >+ </div> >+ <div class="btn-group"> >+ <a class="btn btn-default" href="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl"><i class="fa fa-times"></i> Cancel</a> >+ </div> >+ </div> >+ [% ELSE %] >+ <h1>Automatic item modifications by event</h1> >+ [% IF ( rules ) %] >+ <div id="toolbar" class="btn-toolbar"> >+ <a class="btn btn-default" id="newentry" href="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl?op=edit_form"><i class="fa-solid fa-pencil" aria-hidden="true"></i> Edit rules</a> >+ </div> >+ [% ELSE %] >+ <div id="toolbar" class="btn-toolbar"> >+ <a class="btn btn-default" id="newentry" href="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl?op=edit_form"><i class="fa fa-plus"></i> Add rules</a> >+ </div> >+ [% END %] >+ [% END %] >+ >+ [% FOR message IN messages %] >+ [% IF message.type == "error" %] >+ <div class="dialog alert"> >+ [% END %] >+ [% IF message.code == "unable_to_load_configuration" %] >+ An error occurs: Unable to load the configuration. >+ [% END %] >+ </div> >+ [% END %] >+ >+ [% IF op == 'edit_form' %] >+ <div id="edit_rules"> >+ <div id="rules"> >+ [% FOR rule IN rules %] >+ [% SET id = loop.count %] >+ <fieldset class="rule"> >+ <legend>Rule <span class="rulecount">[% loop.count | html %]</span> <a href="#" class="remove_rule"><i class="fa fa-trash-can"></i> Remove this rule</a></legend> >+ <input type="hidden" name="unique_id" value="[% loop.count | html %]" /> <!-- FIXME on update, the unique_id should be filled --> >+ <div class="event"> >+ <h5>Event</h5> >+ <select class="event" type="text" name="event_[% id | html %]"> >+ [% IF rule.event == "checkin" %] >+ <option value="checkin" selected>Checkin</option> >+ [% ELSE %] >+ <option value="checkin">Checkin</option> >+ [% END %] >+ >+ [% IF rule.event == "checkout" %] >+ <option value="checkout" selected>Checkout</option> >+ [% ELSE %] >+ <option value="checkout">Checkout</option> >+ [% END %] >+ </select> >+ </div> >+ >+ <div class="blocks"> >+ <h5>Conditions</h5> >+ [% FOR condition IN rule.conditions %] >+ <div class="block"> >+ <select name="condition_field_[% id | html %]"> >+ <option value="">Choose a field name</option> >+ [% FOR field IN condition_fields.sort %] >+ [% IF condition.field == field %] >+ <option value="[% field | html %]" selected="selected">[% field | html %]</option> >+ [% ELSE %] >+ <option value="[% field | html %]">[% field | html %]</option> >+ [% END %] >+ [% END %] >+ </select> >+ = >+ <input type="text" value="[% condition.value | html %]" name="condition_value_[% id | html %]" /> >+ <a class="add_block" href="#"><i class="fa fa-plus"></i> Add a condition</a> >+ <a class="remove_block" href="#"><i class="fa fa-trash-can"></i> Remove condition</a> >+ </div> >+ [% END %] >+ </div> >+ <div class="blocks"> >+ <h5>Substitutions</h5> >+ [% FOR substitution IN rule.substitutions %] >+ <div class="block"> >+ <select class="required" required="required" name="substitution_field_[% id | html %]"> >+ <option value="">Choose a field name</option> >+ [% FOR field IN substitution_fields.sort %] >+ [% IF substitution.field == field %] >+ <option value="[% field | html %]" selected="selected">[% field | html %]</option> >+ [% ELSE %] >+ <option value="[% field | html %]">[% field | html %]</option> >+ [% END %] >+ [% END %] >+ </select> >+ = >+ <input type="text" value="[% substitution.value | html %]" name="substitution_value_[% id | html %]" list="substitution-fields" autocomplete="off" /> >+ <a class="add_block" href="#"><i class="fa fa-plus"></i> Add a substitution</a> >+ <a class="remove_block" href="#"><i class="fa fa-trash-can"></i> Remove substitution</a> >+ <span class="required">A field name is required</span> >+ </div> >+ [% END %] >+ </div> >+ </fieldset> >+ [% END %] >+ </div> >+ <input type="hidden" name="op" value="cud-update" /> >+ </div> >+ </form> >+ >+ <div id="norules" class="dialog message"> >+ There are no rules defined. >+ </div> >+ >+ <fieldset id="new_rule"> >+ <legend>Rule <span class="rulecount"></span> <a href="#" class="remove_rule"><i class="fa fa-trash-can"></i> Remove this rule</a></legend> >+ <input type="hidden" name="unique_id" /> >+ <div class="event"> >+ <h5>Event</h5> >+ <select class="event" type="text" name="event"> >+ <option value="checkin">Checkin</option> >+ <option value="checkout">Checkout</option> >+ </select> >+ </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.sort %] >+ <option value="[% field | html %]">[% field | html %]</option> >+ [% END %] >+ </select> >+ = >+ <input type="text" value="" name="condition_value" /> >+ <a class="add_block" href="#"><i class="fa fa-plus"></i> Add a condition</a> >+ <a class="remove_block" href="#"><i class="fa fa-trash-can"></i> Remove condition</a> >+ </div> >+ </div> >+ <div class="blocks"> >+ <h5>Substitutions</h5> >+ <div class="block"> >+ <select required="required" class="required" name="substitution_field"> >+ <option value="">Choose a field name</option> >+ [% FOR field IN substitution_fields.sort %] >+ <option value="[% field | html %]">[% field | html %]</option> >+ [% END %] >+ </select> >+ = >+ <input type="text" value="" name="substitution_value" list="substitution-fields" autocomplete="off"/> >+ <a class="add_block" href="#"><i class="fa fa-plus"></i> Add a substitution</a> >+ <a class="remove_block" href="#"><i class="fa fa-trash-can"></i> Remove substitution</a> >+ <span class="required">A field name is required</span> >+ </div> >+ </div> >+ </fieldset> >+ [% ELSIF rules %] >+ <div> >+ <h4>List of rules</h4> >+ >+ <div class="page-section"> >+ <table id="rulest"> >+ <thead> >+ <tr> >+ <th>Event</th> >+ <th>Conditions</th> >+ <th>Substitutions</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOR rule IN rules %] >+ <tr> >+ <td>[% rule.event | html %]</td> >+ <td> >+ [% FOR condition IN rule.conditions %] >+ [% IF condition.field %] >+ <div class="block"> >+ [% condition.field | html %] = [% condition.value | html %] >+ </div> >+ [% ELSE %] >+ There is no condition for this rule. >+ [% END %] >+ [% END %] >+ </td> >+ <td> >+ [% FOR substitution IN rule.substitutions %] >+ <div class="block"> >+ [% substitution.field | html %] = [% substitution.value | html %] >+ </div> >+ [% END %] >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ </div> <!-- /.page-section --> >+ </div> >+ [% ELSE %] >+ <div class="dialog message"> >+ There are no rules defined. >+ </div> >+ [% END %] >+ >+ </main> >+ </div> >+ >+ <div class="col-md-2 order-md-1 order-sm-2"> >+ <aside> >+ [% INCLUDE 'cat-menu.inc' %] >+ </aside> >+ </div> >+ </div> <!-- /.row --> >+ >+[% MACRO jsinclude BLOCK %] >+ [% Asset.js("lib/hc-sticky/hc-sticky.js") | $raw %] >+ [% Asset.js("js/automatic_item_modification_by_event.js") | $raw %] >+ [% IF op == 'edit_form' %] >+ <script> >+ var Sticky; >+ $(document).ready(function() { >+ [% IF ( op == 'edit_form' ) %] >+ Sticky = $("#toolbar"); >+ Sticky.hcSticky({ >+ stickTo: "main", >+ stickyClass: "floating" >+ }); >+ [% END %] >+ [% IF rules.size > 0 %] >+ $("#norules").hide(); >+ [% ELSE %] >+ $("#rules").show(); >+ [% END %] >+ }); >+ </script> >+ [% END %] >+[% END %] >+ >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/js/automatic_item_modification_by_event.js b/koha-tmpl/intranet-tmpl/prog/js/automatic_item_modification_by_event.js >new file mode 100644 >index 0000000000..27771ca3d4 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/automatic_item_modification_by_event.js >@@ -0,0 +1,120 @@ >+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(".rule").length < 2 ) { >+ $("#rules").hide(); >+ $("#norules").show(); >+ } >+ $(link).parent().parent().remove(); >+ update_rule_count(); >+} >+ >+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(e){ >+ e.preventDefault(); >+ remove_block_action($(this)); >+ }).show(); >+} >+ >+function update_rule_count(){ >+ rules = $(".rulecount"); >+ rules.each( function( i ){ >+ $(this).text( i + 1 ); >+ }); >+} >+ >+$(document).ready(function() { >+ $("#new_rule .remove_rule").hide(); >+ $("#new_rule a.remove_block").hide(); >+ $("#rules a.remove_block").click(function(e){ >+ e.preventDefault(); >+ remove_block_action($(this)); >+ }); >+ $("#rules .remove_rule").click(function(e){ >+ e.preventDefault(); >+ remove_rule_action($(this)); >+ }); >+ >+ var unique_id = $(".rule").length + 1; >+ $(".add_rule").click(function(e){ >+ e.preventDefault(); >+ var rule = $("#new_rule"); >+ var rules = $("#rules"); >+ 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("select[name='event']").attr('name', 'event_' + unique_id); >+ new_rule.find("input[name='unique_id']").val(unique_id); >+ >+ $("#rules").append(new_rule); >+ update_rule_count(); >+ var scrollToPoint = new_rule.position(); >+ window.scroll(0, scrollToPoint.top - $("#toolbar").height() ); >+ >+ if( $("#rules").find(".rule").length > 0 ) { >+ $("#rules").show(); >+ $("#norules").hide(); >+ } >+ if( $("#rules").find(".conditions > .condition").length > 1 ) { >+ >+ } >+ if( $("#rules").find(".conditions > .condition").length > 1 ) { >+ >+ } >+ new_rule.find('.remove_rule').click(function(e){ >+ e.preventDefault(); >+ remove_rule_action( $(this) ); >+ }).show(); >+ new_rule.find('.add_rule').remove(); >+ unique_id++; >+ }); >+ >+ $("a.add_block").click(function(e){ >+ e.preventDefault(); >+ clone_block( $(this).parent() ); >+ }); >+ >+ if( $("#rules").find(".rule").length < 1 ) { >+ $("#rules").hide(); >+ $("#norules").show(); >+ } >+ >+ $("#rules .rule .blocks").each(function(){ >+ if ( $(this).find(".block").length == 1 ) { >+ $(this).find("a.remove_block").hide(); >+ } >+ }); >+ >+ $("#rules_form").validate(); >+}); >diff --git a/t/db_dependent/Koha/Item.t b/t/db_dependent/Koha/Item.t >index 0822ccf4f0..e90d965901 100755 >--- a/t/db_dependent/Koha/Item.t >+++ b/t/db_dependent/Koha/Item.t >@@ -20,7 +20,7 @@ > use Modern::Perl; > use utf8; > >-use Test::More tests => 38; >+use Test::More tests => 37; > use Test::Exception; > use Test::MockModule; > use Test::Warn; >@@ -2795,67 +2795,6 @@ subtest 'current_branchtransfers relationship' => sub { > $schema->storage->txn_rollback; > }; > >-subtest 'location_update_trigger() tests' => sub { >- >- plan tests => 10; >- >- $schema->storage->txn_begin; >- >- my $location = 'YA'; >- my $permanent_location = 'HEY'; >- >- foreach my $action (qw{ checkin checkout }) { >- >- my $pref = $action eq 'checkin' ? 'UpdateItemLocationOnCheckin' : 'UpdateItemLocationOnCheckout'; >- >- t::lib::Mocks::mock_preference( 'UpdateItemLocationOnCheckin', q{} ); >- t::lib::Mocks::mock_preference( 'UpdateItemLocationOnCheckout', q{} ); >- >- my $item = $builder->build_sample_item( { location => $location, permanent_location => $permanent_location } ); >- >- $item->location_update_trigger($action); >- >- is( $item->location, $location, "$pref does not modify value when not enabled" ); >- >- t::lib::Mocks::mock_preference( $pref, qq{$location: GEN} ); >- >- $item->location_update_trigger($action); >- >- is( $item->location, 'GEN', qq{'location' value set from '$location' to 'GEN' with setting `$location: GEN`} ); >- >- t::lib::Mocks::mock_preference( $pref, q{_ALL_: BOO} ); >- >- $item->location_update_trigger($action); >- >- is( $item->location, 'BOO', q{`_ALL_` does the job} ); >- >- t::lib::Mocks::mock_preference( $pref, qq{$location: _BLANK_} ); >- $item->location($location)->store(); >- >- $item->location_update_trigger($action); >- is( $item->location, q{}, q{`_BLANK_` does the job} ); >- >- t::lib::Mocks::mock_preference( $pref, qq{GEN: _BLANK_\n_BLANK_: PROC\n$location: _PERM_} ); >- >- $item->set( >- { >- location => $location, >- permanent_location => >- $permanent_location, # setting `permanent_location` explicitly because ->store messes with it. >- } >- )->store; >- >- $item->location_update_trigger($action); >- >- is( >- $item->location, $permanent_location, >- q{_PERM_ does the job"} >- ); >- } >- >- $schema->storage->txn_rollback; >-}; >- > subtest 'bookings' => sub { > plan tests => 4; > >diff --git a/t/db_dependent/Koha/Item/trigger_automatic_modifications_by_event.t b/t/db_dependent/Koha/Item/trigger_automatic_modifications_by_event.t >new file mode 100755 >index 0000000000..7c488fb7ed >--- /dev/null >+++ b/t/db_dependent/Koha/Item/trigger_automatic_modifications_by_event.t >@@ -0,0 +1,168 @@ >+#!/usr/bin/perl >+ >+use Modern::Perl; >+ >+use JSON qw(encode_json); >+use Test::More; >+ >+use t::lib::Mocks; >+use t::lib::TestBuilder; >+ >+my $builder = t::lib::TestBuilder->new; >+ >+subtest 'simple substitution' => sub { >+ plan tests => 1; >+ >+ my $rules = [ >+ { >+ event => 'checkin', >+ conditions => [], >+ substitutions => [ >+ { field => 'items.damaged', value => '1' }, >+ ], >+ }, >+ ]; >+ t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) ); >+ >+ my $item = Koha::Item->new; >+ $item->damaged('0'); >+ $item->trigger_automatic_modifications_by_event('checkin'); >+ is( $item->damaged, '1' ); >+}; >+ >+subtest 'field substitution' => sub { >+ plan tests => 1; >+ >+ my $rules = [ >+ { >+ event => 'checkin', >+ conditions => [], >+ substitutions => [ >+ { field => 'items.location', value => '$items.permanent_location' }, >+ ], >+ }, >+ ]; >+ t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) ); >+ >+ my $item = Koha::Item->new; >+ $item->location('CART'); >+ $item->permanent_location('CHILD'); >+ $item->trigger_automatic_modifications_by_event('checkin'); >+ is( $item->location, $item->permanent_location ); >+}; >+ >+subtest 'blank substitution' => sub { >+ plan tests => 2; >+ >+ my $rules = [ >+ { >+ event => 'checkin', >+ conditions => [], >+ substitutions => [ >+ { field => 'items.location', value => '' }, >+ { field => 'items.damaged' }, # no value is the same as an empty string >+ ], >+ }, >+ ]; >+ t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) ); >+ >+ my $item = Koha::Item->new; >+ $item->location('CART'); >+ $item->damaged('1'); >+ $item->trigger_automatic_modifications_by_event('checkin'); >+ is( $item->location, '' ); >+ is( $item->damaged, '' ); >+}; >+ >+subtest 'conditions' => sub { >+ plan tests => 2; >+ >+ my $rules = [ >+ { >+ event => 'checkin', >+ conditions => [ >+ { field => 'items.location', value => 'NEW' }, >+ { field => 'items.permanent_location', value => 'CHILD' }, >+ ], >+ substitutions => [ >+ { field => 'items.location', value => 'CART' }, >+ ], >+ }, >+ ]; >+ t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) ); >+ >+ my $item = Koha::Item->new; >+ >+ # Conditions are not met, item is not modified >+ $item->location('PROC'); >+ $item->permanent_location('CHILD'); >+ $item->trigger_automatic_modifications_by_event('checkin'); >+ is( $item->location, 'PROC' ); >+ >+ # Conditions are met, item is modified >+ $item->location('NEW'); >+ $item->permanent_location('CHILD'); >+ $item->trigger_automatic_modifications_by_event('checkin'); >+ is( $item->location, 'CART' ); >+}; >+ >+subtest 'blank condition' => sub { >+ plan tests => 2; >+ >+ my $rules = [ >+ { >+ event => 'checkin', >+ conditions => [ >+ { field => 'items.location', value => '' }, >+ ], >+ substitutions => [ >+ { field => 'items.location', value => 'CART' }, >+ ], >+ }, >+ ]; >+ t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) ); >+ >+ my $item = Koha::Item->new; >+ >+ # Conditions are not met, item is not modified >+ $item->location('PROC'); >+ $item->trigger_automatic_modifications_by_event('checkin'); >+ is( $item->location, 'PROC' ); >+ >+ # Conditions are met, item is modified >+ $item->location(''); >+ $item->trigger_automatic_modifications_by_event('checkin'); >+ is( $item->location, 'CART' ); >+}; >+ >+subtest 'condition on biblio and biblioitems fields' => sub { >+ plan tests => 2; >+ >+ my $rules = [ >+ { >+ event => 'checkin', >+ conditions => [ >+ { field => 'biblio.title', value => 'No title' }, >+ { field => 'biblioitems.volume', value => '1' }, >+ ], >+ substitutions => [ >+ { field => 'items.location', value => 'CART' }, >+ ], >+ }, >+ ]; >+ t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) ); >+ >+ my $item = $builder->build_object( { class => 'Koha::Items' } ); >+ >+ $item->location('PROC'); >+ $item->biblio->title('A title'); >+ $item->biblioitem->volume('1'); >+ $item->trigger_automatic_modifications_by_event('checkin'); >+ is( $item->location, 'PROC' ); >+ >+ $item->biblio->title('No title'); >+ $item->trigger_automatic_modifications_by_event('checkin'); >+ is( $item->location, 'CART' ); >+}; >+ >+done_testing; >diff --git a/tools/automatic_item_modification_by_event.pl b/tools/automatic_item_modification_by_event.pl >new file mode 100755 >index 0000000000..b9435b5d21 >--- /dev/null >+++ b/tools/automatic_item_modification_by_event.pl >@@ -0,0 +1,114 @@ >+#!/usr/bin/perl >+ >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+=head1 NAME >+ >+automatic_item_modification_by_event.pl: Configuration for automatic update of items at checkin/checkout >+ >+=cut >+ >+use Modern::Perl; >+ >+use CGI; >+use JSON qw( encode_json decode_json ); >+ >+use C4::Auth qw( get_template_and_user ); >+use C4::Context; >+use C4::Output qw( output_html_with_http_headers ); >+ >+use Koha::Items; >+use Koha::Biblioitems; >+use Koha::Biblios; >+ >+my $cgi = CGI->new; >+ >+# open template >+my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => "tools/automatic_item_modification_by_event.tt", >+ query => $cgi, >+ type => "intranet", >+ flagsrequired => { tools => 'items_batchmod' }, >+ } >+); >+ >+my $op = $cgi->param('op') // 'cud-show'; >+ >+my $syspref_name = q|automatic_item_modification_by_event_configuration|; >+if ( $op eq 'cud-update' ) { >+ my @rules; >+ my @unique_ids = $cgi->multi_param('unique_id'); >+ for my $unique_id (@unique_ids) { >+ my @substitution_fields = $cgi->multi_param("substitution_field_$unique_id"); >+ my @substitution_values = $cgi->multi_param("substitution_value_$unique_id"); >+ my @condition_fields = $cgi->multi_param("condition_field_$unique_id"); >+ my @condition_values = $cgi->multi_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->{event} = $cgi->param("event_$unique_id"); >+ >+ push @rules, $rule; >+ } >+ my $syspref_content = encode_json( \@rules ); >+ C4::Context->set_preference( $syspref_name, $syspref_content ); >+ >+ $op = 'cud-show'; >+} >+ >+my @messages; >+my $syspref_content = C4::Context->preference($syspref_name); >+my $rules; >+$rules = eval { decode_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.$_" } Koha::Items->columns; >+my @biblioitem_fields = map { "biblioitems.$_" } Koha::Biblioitems->columns; >+my @biblio_fields = map { "biblio.$_" } Koha::Biblios->columns; >+$template->param( >+ op => $op, >+ messages => \@messages, >+ condition_fields => [ @item_fields, @biblioitem_fields, @biblio_fields ], >+ substitution_fields => \@item_fields, >+ rules => $rules, >+); >+ >+output_html_with_http_headers $cgi, $cookie, $template->output; >-- >2.39.2
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 38387
:
174116
|
174117
|
174184