From 5adb33e19748a606570a644dfba9a02963c49ac6 Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Mon, 17 Jun 2024 15:49:14 +0200 Subject: [PATCH] Bug 38387: Automatic item modification at checkin/checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. (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 = ") and one substitution ("items.location = ") - 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 = ") 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 Sponsored-by: Médiathèques Valence Romans Agglo --- C4/Circulation.pm | 26 +- Koha/Item.pm | 133 ++++---- .../move-sysprefs-UpdateItemLocationOn.pl | 87 ++++++ .../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, 873 insertions(+), 139 deletions(-) create mode 100755 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..e9657f69e1 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 -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, location is updated following the I preference. - -=item For B, location is updated following the I preference. - -=back - -FIXME: It should return I<$self>. See bug 35270. +This method does not call C. Caller is responsible for calling C +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 100755 index 0000000000..ecd6e38fda --- /dev/null +++ b/installer/data/mysql/atomicupdate/move-sysprefs-UpdateItemLocationOn.pl @@ -0,0 +1,87 @@ +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 @@
  • Item modifications by age
  • +
  • + Item modifications by event +
  • [% END %] [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %]
  • 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 @@
  • Item modifications by age
  • +
  • + Item modifications by event +
  • [% END %] [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %]
  • 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' %] +[% FILTER collapse %] + [% IF ( op == 'edit_form' ) %] + [% t("Rules") | html %] › + [% END %] + [% t("Automatic item modifications by event") | html %] › + [% t("Cataloging") | html %] › + [% t("Koha") | html %] +[% END %] +[% INCLUDE 'doc-head-close.inc' %] + + + +[% WRAPPER 'header.inc' %] + [% INCLUDE 'cat-search.inc' %] +[% END %] + +[% WRAPPER 'sub-header.inc' %] + [% WRAPPER breadcrumbs %] + [% WRAPPER breadcrumb_item %] + Cataloging + [% END %] + + [% IF ( op == 'edit_form' ) %] + [% WRAPPER breadcrumb_item %] + Automatic item modifications by event + [% END %] + [% WRAPPER breadcrumb_item bc_active= 1 %] + Rules + [% END %] + [% ELSE %] + [% WRAPPER breadcrumb_item bc_active= 1 %] + Automatic item modifications by event + [% END %] + [% END %] + [% END #/ WRAPPER breadcrumbs %] +[% END #/ WRAPPER sub-header.inc %] + +
    +
    +
    +
    + [% INCLUDE 'messages.inc' %] + + [% IF ( op == 'edit_form' ) %] + + [% FOREACH substitution_field IN substitution_fields %] + + [% END %] + + +
    + [% INCLUDE 'csrf-token.inc' %] +

    Rules for automatic item modifications by event

    +
    +
    + +
    +
    + +
    +
    + Cancel +
    +
    + [% ELSE %] +

    Automatic item modifications by event

    + [% IF ( rules ) %] + + [% ELSE %] + + [% END %] + [% END %] + + [% FOR message IN messages %] + [% IF message.type == "error" %] +
    + [% END %] + [% IF message.code == "unable_to_load_configuration" %] + An error occurs: Unable to load the configuration. + [% END %] +
    + [% END %] + + [% IF op == 'edit_form' %] +
    +
    + [% FOR rule IN rules %] + [% SET id = loop.count %] +
    + Rule [% loop.count | html %] Remove this rule + +
    +
    Event
    + +
    + +
    +
    Conditions
    + [% FOR condition IN rule.conditions %] +
    + + = + + Add a condition + Remove condition +
    + [% END %] +
    +
    +
    Substitutions
    + [% FOR substitution IN rule.substitutions %] +
    + + = + + Add a substitution + Remove substitution + A field name is required +
    + [% END %] +
    +
    + [% END %] +
    + +
    +
    + +
    + There are no rules defined. +
    + +
    + Rule Remove this rule + +
    +
    Event
    + +
    +
    +
    Conditions
    +
    + + = + + Add a condition + Remove condition +
    +
    +
    +
    Substitutions
    +
    + + = + + Add a substitution + Remove substitution + A field name is required +
    +
    +
    + [% ELSIF rules %] +
    +

    List of rules

    + +
    + + + + + + + + + + [% FOR rule IN rules %] + + + + + + [% END %] + +
    EventConditionsSubstitutions
    [% rule.event | html %] + [% FOR condition IN rule.conditions %] + [% IF condition.field %] +
    + [% condition.field | html %] = [% condition.value | html %] +
    + [% ELSE %] + There is no condition for this rule. + [% END %] + [% END %] +
    + [% FOR substitution IN rule.substitutions %] +
    + [% substitution.field | html %] = [% substitution.value | html %] +
    + [% END %] +
    +
    +
    + [% ELSE %] +
    + There are no rules defined. +
    + [% END %] + +
    +
    + +
    + +
    +
    + +[% 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' %] + + [% 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 . + +=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