View | Details | Raw Unified | Return to bug 38387
Collapse All | Expand All

(-)a/C4/Circulation.pm (-6 / +20 lines)
Lines 1742-1749 sub AddIssue { Link Here
1742
                CartToShelf( $item_object->itemnumber );
1742
                CartToShelf( $item_object->itemnumber );
1743
            }
1743
            }
1744
1744
1745
            # Update item location
1745
            $item_object->trigger_automatic_modifications_by_event('checkout');
1746
            $item_object->location_update_trigger( 'checkout' );
1746
            $item_object->store(
1747
                {
1748
                    log_action        => 0,
1749
                    skip_record_index => 1,
1750
                    skip_holds_queue  => 1
1751
                }
1752
            );
1747
1753
1748
            if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1754
            if ( C4::Context->preference('UpdateTotalIssuesOnCirc') ) {
1749
                UpdateTotalIssues( $item_object->biblionumber, 1, undef, { skip_holds_queue => 1 } );
1755
                UpdateTotalIssues( $item_object->biblionumber, 1, undef, { skip_holds_queue => 1 } );
Lines 2209-2218 sub AddReturn { Link Here
2209
    my $borrowernumber = $patron ? $patron->borrowernumber : undef;    # we don't know if we had a borrower or not
2215
    my $borrowernumber = $patron ? $patron->borrowernumber : undef;    # we don't know if we had a borrower or not
2210
    my $patron_unblessed = $patron ? $patron->unblessed : {};
2216
    my $patron_unblessed = $patron ? $patron->unblessed : {};
2211
2217
2212
    # Update item location
2218
    my $item_location = $item->location;
2213
    my $loc_messages = $item->location_update_trigger( 'checkin' );
2219
    $item->trigger_automatic_modifications_by_event('checkin');
2214
    foreach my $loc_msg_key ( keys %$loc_messages ) {
2220
    $item->store(
2215
        $messages->{ $loc_msg_key } = $loc_messages->{ $loc_msg_key };
2221
        {
2222
            log_action        => 0,
2223
            skip_record_index => 1,
2224
            skip_holds_queue  => 1
2225
        }
2226
    );
2227
2228
    if ( $item_location ne $item->location ) {
2229
        $messages->{ItemLocationUpdated} = { from => $item_location, to => $item->location };
2216
    }
2230
    }
2217
2231
2218
    my $rules = C4::Context->yaml_preference('UpdateNotForLoanStatusOnCheckin');
2232
    my $rules = C4::Context->yaml_preference('UpdateNotForLoanStatusOnCheckin');
(-)a/Koha/Item.pm (-71 / +62 lines)
Lines 2582-2674 sub strings_map { Link Here
2582
}
2582
}
2583
2583
2584
2584
2585
=head3 location_update_trigger
2585
=head3 trigger_automatic_modifications_by_event
2586
2586
2587
    $item->location_update_trigger( $action );
2587
Update item properties according to the configuration stored in system
2588
preference C<automatic_item_modification_by_event_configuration>
2588
2589
2589
Updates the item location based on I<$action>. It is done like this:
2590
    $item->trigger_automatic_modifications_by_event($event)
2590
2591
2591
=over 4
2592
C<$event> can be either C<'checkin'> or C<'checkout'>
2592
2593
2593
=item For B<checkin>, location is updated following the I<UpdateItemLocationOnCheckin> preference.
2594
This method does not call C<store>. Caller is responsible for calling C<store>
2594
2595
if modifications need to be saved.
2595
=item For B<checkout>, location is updated following the I<UpdateItemLocationOnCheckout> preference.
2596
2597
=back
2598
2599
FIXME: It should return I<$self>. See bug 35270.
2600
2596
2601
=cut
2597
=cut
2602
2598
2603
sub location_update_trigger {
2599
sub trigger_automatic_modifications_by_event {
2604
    my ( $self, $action ) = @_;
2600
    my ($self, $event) = @_;
2605
2601
2606
    my ( $update_loc_rules, $messages );
2602
    my @rules;
2607
    if ( $action eq 'checkin' ) {
2603
    my $rules_json = C4::Context->preference('automatic_item_modification_by_event_configuration');
2608
        $update_loc_rules = C4::Context->yaml_preference('UpdateItemLocationOnCheckin');
2604
    if ($rules_json) {
2609
    } else {
2605
        require JSON;
2610
        $update_loc_rules = C4::Context->yaml_preference('UpdateItemLocationOnCheckout');
2606
        @rules = @{ JSON::decode_json($rules_json) };
2611
    }
2607
    }
2612
2608
2613
    if ($update_loc_rules) {
2609
    @rules = grep { $_->{event} eq $event } @rules;
2614
        if ( defined $update_loc_rules->{_ALL_} ) {
2610
2615
            if ( $update_loc_rules->{_ALL_} eq '_PERM_' ) {
2611
    RULE: foreach my $rule (@rules) {
2616
                $update_loc_rules->{_ALL_} = $self->permanent_location;
2612
        my @conditions = @{ $rule->{conditions} // [] };
2613
        foreach my $condition (@conditions) {
2614
            my $field = $condition->{field};
2615
            next unless $field;
2616
2617
            my $item_value;
2618
            if ($field =~ /^biblio\.(\w+)$/) {
2619
                my $item_field = $1;
2620
                $item_value = $self->biblio->get_column($item_field);
2621
            } elsif ($field =~ /^biblioitems\.(\w+)$/) {
2622
                my $item_field = $1;
2623
                $item_value = $self->biblioitem->get_column($item_field);
2624
            } elsif ($field =~ /^items\.(\w+)$/) {
2625
                my $item_field = $1;
2626
                $item_value = $self->get_column($item_field);
2627
            } else {
2628
                warn "Unknown condition field: $field";
2629
                next;
2617
            }
2630
            }
2618
            if ( $update_loc_rules->{_ALL_} eq '_BLANK_' ) {
2631
2619
                $update_loc_rules->{_ALL_} = '';
2632
            # Consider that NULL and empty strings are the same
2633
            $item_value //= '';
2634
            my $condition_value = $condition->{value} // '';
2635
2636
            if ( $item_value ne $condition_value ) {
2637
                next RULE;
2620
            }
2638
            }
2621
            if (
2639
        }
2622
                ( defined $self->location && $self->location ne $update_loc_rules->{_ALL_} )
2640
2623
                || ( !defined $self->location
2641
        my @substitutions = @{ $rule->{substitutions} // [] };
2624
                    && $update_loc_rules->{_ALL_} ne "" )
2642
        foreach my $substitution (@substitutions) {
2625
                )
2643
            my $field = $substitution->{field};
2626
            {
2644
            next unless $field;
2627
                $messages->{'ItemLocationUpdated'} =
2645
2628
                    { from => $self->location, to => $update_loc_rules->{_ALL_} };
2646
            my $item_field;
2629
                $self->location( $update_loc_rules->{_ALL_} )->store(
2647
            if ($field =~ /^items\.(\w+)$/) {
2630
                    {
2648
                $item_field = $1;
2631
                        log_action        => 0,
2649
            } else {
2632
                        skip_record_index => 1,
2650
                warn "Unknown substitution field: $field";
2633
                        skip_holds_queue  => 1
2651
                next;
2634
                    }
2635
                );
2636
            }
2652
            }
2637
        } else {
2653
2638
            foreach my $key ( keys %$update_loc_rules ) {
2654
            my $value = $substitution->{value} // '';
2639
                if ( $update_loc_rules->{$key} eq '_PERM_' ) {
2655
            if ($value =~ /^\$items\.(\w+)$/) {
2640
                    $update_loc_rules->{$key} = $self->permanent_location;
2656
                my $value_field = $1;
2641
                } elsif ( $update_loc_rules->{$key} eq '_BLANK_' ) {
2657
                $value = $self->get_column($value_field);
2642
                    $update_loc_rules->{$key} = '';
2643
                }
2644
                if (
2645
                    (
2646
                           defined $self->location
2647
                        && $self->location eq $key
2648
                        && $self->location ne $update_loc_rules->{$key}
2649
                    )
2650
                    || (   $key eq '_BLANK_'
2651
                        && ( !defined $self->location || $self->location eq '' )
2652
                        && $update_loc_rules->{$key} ne '' )
2653
                    )
2654
                {
2655
                    $messages->{'ItemLocationUpdated'} = {
2656
                        from => $self->location,
2657
                        to   => $update_loc_rules->{$key}
2658
                    };
2659
                    $self->location( $update_loc_rules->{$key} )->store(
2660
                        {
2661
                            log_action        => 0,
2662
                            skip_record_index => 1,
2663
                            skip_holds_queue  => 1
2664
                        }
2665
                    );
2666
                    last;
2667
                }
2668
            }
2658
            }
2659
2660
            $self->set({ $item_field => $value });
2669
        }
2661
        }
2670
    }
2662
    }
2671
    return $messages;
2672
}
2663
}
2673
2664
2674
2665
(-)a/installer/data/mysql/atomicupdate/move-sysprefs-UpdateItemLocationOn.pl (+79 lines)
Line 0 Link Here
1
use Modern::Perl;
2
3
use YAML::XS;
4
use JSON;
5
6
return {
7
    bug_number  => '38387',
8
    description => 'Move sysprefs UpdateItemLocationOnCheckin and UpdateItemLocationOnCheckout',
9
    up          => sub {
10
        my ($args) = @_;
11
        my ( $dbh, $out ) = @$args{qw(dbh out)};
12
13
        my @sysprefs = (
14
            { name => 'UpdateItemLocationOnCheckin', event => 'checkin' },
15
            { name => 'UpdateItemLocationOnCheckout', event => 'checkout' },
16
        );
17
18
        my @new_rules;
19
        foreach my $syspref (@sysprefs) {
20
            my $yaml = $dbh->selectrow_array('SELECT value FROM systempreferences WHERE variable = ?', undef, $syspref->{name});
21
            next unless $yaml;
22
23
            my $rules = eval { YAML::XS::Load( Encode::encode_utf8( $yaml ) ); };
24
            if ($@) {
25
                die sprintf('Unable to parse %s system preference : %s', $syspref->{name}, $@);
26
            }
27
28
            unless (ref $rules && ref $rules eq 'HASH') {
29
                die sprintf('System preference %s is not a YAML mapping', $syspref->{name});
30
            }
31
32
            if (defined $rules->{_ALL_}) {
33
                my $value = $rules->{_ALL_};
34
                if ($value eq '_PERM_') {
35
                    $value = '$items.permanent_location';
36
                } elsif ($value eq '_BLANK_') {
37
                    $value = '';
38
                }
39
40
                my $new_rule = {
41
                    event => $syspref->{event},
42
                    conditions => [],
43
                    substitutions => [ { field => 'items.location', value => $value } ],
44
                };
45
                if ($value ne 'PROC' && $value ne 'CART') {
46
                    push @{ $new_rule->{substitutions} }, { field => 'items.permanent_location', value => $value };
47
                }
48
                push @new_rules, $new_rule;
49
            } else {
50
                while (my ($key, $value) = each %$rules) {
51
                    if ($key eq '_BLANK_') {
52
                        $key = '';
53
                    }
54
                    if ($value eq '_PERM_') {
55
                        $value = '$items.permanent_location';
56
                    } elsif ($value eq '_BLANK_') {
57
                        $value = '';
58
                    }
59
60
                    my $new_rule = {
61
                        event => $syspref->{event},
62
                        conditions => [ { field => 'items.location', value => $key } ],
63
                        substitutions => [ { field => 'items.location', value => $value } ],
64
                    };
65
                    if ($value ne 'PROC' && $value ne 'CART') {
66
                        push @{ $new_rule->{substitutions} }, { field => 'items.permanent_location', value => $value };
67
                    }
68
                    push @new_rules, $new_rule;
69
                }
70
            }
71
        }
72
73
        $dbh->do('INSERT IGNORE INTO systempreferences (variable, value) VALUES (?, ?)', undef, 'automatic_item_modification_by_event_configuration', encode_json(\@new_rules));
74
        say $out sprintf('Added %d rules for automatic item modifications by event', scalar @new_rules);
75
76
        $dbh->do("DELETE FROM systempreferences WHERE variable IN ('UpdateItemLocationOnCheckin', 'UpdateItemLocationOnCheckout')");
77
        say $out sprintf('Removed system preferences UpdateItemLocationOnCheckin and UpdateItemLocationOnCheckout')
78
    },
79
};
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc (+3 lines)
Lines 83-88 Link Here
83
            <li>
83
            <li>
84
                <a href="/cgi-bin/koha/tools/automatic_item_modification_by_age.pl">Item modifications by age</a>
84
                <a href="/cgi-bin/koha/tools/automatic_item_modification_by_age.pl">Item modifications by age</a>
85
            </li>
85
            </li>
86
            <li>
87
                <a href="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl">Item modifications by event</a>
88
            </li>
86
            [% END %]
89
            [% END %]
87
            [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %]
90
            [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %]
88
            <li>
91
            <li>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt (+3 lines)
Lines 163-168 Link Here
163
                            <li>
163
                            <li>
164
                                <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>
164
                                <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>
165
                            </li>
165
                            </li>
166
                            <li>
167
                                <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>
168
                            </li>
166
                            [% END %]
169
                            [% END %]
167
                            [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %]
170
                            [% IF ( CAN_user_stockrotation_manage_rotas && Koha.Preference('StockRotation') ) %]
168
                            <li>
171
                            <li>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/automatic_item_modification_by_event.tt (+295 lines)
Line 0 Link Here
1
[% USE raw %]
2
[% USE Asset %]
3
[% PROCESS 'i18n.inc' %]
4
[% SET footerjs = 1 %]
5
[% INCLUDE 'doc-head-open.inc' %]
6
<title>[% FILTER collapse %]
7
    [% IF ( op == 'edit_form' ) %]
8
        [% t("Rules") | html %] &rsaquo;
9
    [% END %]
10
    [% t("Automatic item modifications by event") | html %] &rsaquo;
11
    [% t("Cataloging") | html %] &rsaquo;
12
    [% t("Koha") | html %]
13
[% END %]</title>
14
[% INCLUDE 'doc-head-close.inc' %]
15
</head>
16
17
<body id="tools_automatic_item_modification_by_event" class="tools">
18
[% WRAPPER 'header.inc' %]
19
    [% INCLUDE 'cat-search.inc' %]
20
[% END %]
21
22
[% WRAPPER 'sub-header.inc' %]
23
    [% WRAPPER breadcrumbs %]
24
            [% WRAPPER breadcrumb_item %]
25
                <a href="/cgi-bin/koha/cataloguing/cataloging-home.pl">Cataloging</a>
26
            [% END %]
27
28
            [% IF ( op == 'edit_form' ) %]
29
                [% WRAPPER breadcrumb_item %]
30
                    <a href="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl">Automatic item modifications by event</a>
31
                [% END %]
32
                [% WRAPPER breadcrumb_item bc_active= 1 %]
33
                    <span>Rules</span>
34
                [% END %]
35
            [% ELSE %]
36
                [% WRAPPER breadcrumb_item bc_active= 1 %]
37
                    <span>Automatic item modifications by event</span>
38
                [% END %]
39
            [% END %]
40
    [% END #/ WRAPPER breadcrumbs %]
41
[% END #/ WRAPPER sub-header.inc %]
42
43
<div class="main container-fluid">
44
    <div class="row">
45
        <div class="col-md-10 order-md-2 order-sm-1">
46
            <main>
47
                [% INCLUDE 'messages.inc' %]
48
49
        [% IF ( op == 'edit_form' ) %]
50
          <datalist id="substitution-fields">
51
            [% FOREACH substitution_field IN substitution_fields %]
52
              <option value="$[% substitution_field | html %]">$[% substitution_field | html %]</option>
53
            [% END %]
54
          </datalist>
55
56
          <form method="post" id="rules_form" action="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl">
57
              [% INCLUDE 'csrf-token.inc' %]
58
            <h1>Rules for automatic item modifications by event</h1>
59
            <div id="toolbar" class="btn-toolbar">
60
                <div class="btn-group">
61
                    <button class="btn btn-default add_rule"><i class="fa fa-plus"></i> Add rule</button>
62
                </div>
63
                <div class="btn-group">
64
                    <button type="submit" id="save_rules" class="btn btn-default"><i class="fa fa-save"></i> Save</button>
65
                </div>
66
                <div class="btn-group">
67
                    <a class="btn btn-default" href="/cgi-bin/koha/tools/automatic_item_modification_by_event.pl"><i class="fa fa-times"></i> Cancel</a>
68
                </div>
69
            </div>
70
        [% ELSE %]
71
            <h1>Automatic item modifications by event</h1>
72
            [% IF ( rules ) %]
73
                <div id="toolbar" class="btn-toolbar">
74
                    <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>
75
                </div>
76
            [% ELSE %]
77
                <div id="toolbar" class="btn-toolbar">
78
                    <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>
79
                </div>
80
            [% END %]
81
        [% END %]
82
83
        [% FOR message IN messages %]
84
          [% IF message.type == "error" %]
85
            <div class="dialog alert">
86
          [% END %]
87
          [% IF message.code == "unable_to_load_configuration" %]
88
            An error occurs: Unable to load the configuration.
89
          [% END %]
90
          </div>
91
        [% END %]
92
93
        [% IF op == 'edit_form' %]
94
            <div id="edit_rules">
95
                <div id="rules">
96
                [% FOR rule IN rules %]
97
                  [% SET id = loop.count %]
98
                  <fieldset class="rule">
99
                    <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>
100
                    <input type="hidden" name="unique_id" value="[% loop.count | html %]" /> <!-- FIXME on update, the unique_id should be filled -->
101
                    <div class="event">
102
                      <h5>Event</h5>
103
                      <select class="event" type="text" name="event_[% id | html %]">
104
                        [% IF rule.event == "checkin" %]
105
                            <option value="checkin" selected>Checkin</option>
106
                        [% ELSE %]
107
                            <option value="checkin">Checkin</option>
108
                        [% END %]
109
110
                        [% IF rule.event == "checkout" %]
111
                            <option value="checkout" selected>Checkout</option>
112
                        [% ELSE %]
113
                            <option value="checkout">Checkout</option>
114
                        [% END %]
115
                      </select>
116
                    </div>
117
118
                    <div class="blocks">
119
                      <h5>Conditions</h5>
120
                      [% FOR condition IN rule.conditions %]
121
                        <div class="block">
122
                          <select name="condition_field_[% id | html %]">
123
                            <option value="">Choose a field name</option>
124
                            [% FOR field IN condition_fields.sort %]
125
                              [% IF condition.field == field %]
126
                                <option value="[% field | html %]" selected="selected">[% field | html %]</option>
127
                              [% ELSE %]
128
                                <option value="[% field | html %]">[% field | html %]</option>
129
                              [% END %]
130
                            [% END %]
131
                          </select>
132
                          =
133
                          <input type="text" value="[% condition.value | html %]" name="condition_value_[% id | html %]" />
134
                          <a class="add_block" href="#"><i class="fa fa-plus"></i> Add a condition</a>
135
                          <a class="remove_block" href="#"><i class="fa fa-trash-can"></i> Remove condition</a>
136
                        </div>
137
                      [% END %]
138
                    </div>
139
                    <div class="blocks">
140
                      <h5>Substitutions</h5>
141
                      [% FOR substitution IN rule.substitutions %]
142
                        <div class="block">
143
                          <select class="required" required="required" name="substitution_field_[% id | html %]">
144
                            <option value="">Choose a field name</option>
145
                            [% FOR field IN substitution_fields.sort %]
146
                              [% IF substitution.field == field %]
147
                                <option value="[% field | html %]" selected="selected">[% field | html %]</option>
148
                              [% ELSE %]
149
                                <option value="[% field | html %]">[% field | html %]</option>
150
                              [% END %]
151
                            [% END %]
152
                          </select>
153
                          =
154
                          <input type="text" value="[% substitution.value | html %]" name="substitution_value_[% id | html %]" list="substitution-fields" autocomplete="off" />
155
                          <a class="add_block" href="#"><i class="fa fa-plus"></i> Add a substitution</a>
156
                          <a class="remove_block" href="#"><i class="fa fa-trash-can"></i> Remove substitution</a>
157
                          <span class="required">A field name is required</span>
158
                        </div>
159
                      [% END %]
160
                    </div>
161
                  </fieldset>
162
                [% END %]
163
                </div>
164
                <input type="hidden" name="op" value="cud-update" />
165
            </div>
166
          </form>
167
168
        <div id="norules" class="dialog message">
169
          There are no rules defined.
170
        </div>
171
172
          <fieldset id="new_rule">
173
            <legend>Rule <span class="rulecount"></span> <a href="#" class="remove_rule"><i class="fa fa-trash-can"></i> Remove this rule</a></legend>
174
            <input type="hidden" name="unique_id" />
175
            <div class="event">
176
              <h5>Event</h5>
177
              <select class="event" type="text" name="event">
178
                <option value="checkin">Checkin</option>
179
                <option value="checkout">Checkout</option>
180
              </select>
181
            </div>
182
            <div class="blocks">
183
              <h5>Conditions</h5>
184
              <div class="block">
185
                <select name="condition_field">
186
                  <option value="">Choose a field name</option>
187
                  [% FOR field IN condition_fields.sort %]
188
                    <option value="[% field | html %]">[% field | html %]</option>
189
                  [% END %]
190
                </select>
191
                =
192
                <input type="text" value="" name="condition_value" />
193
                <a class="add_block" href="#"><i class="fa fa-plus"></i> Add a condition</a>
194
                <a class="remove_block" href="#"><i class="fa fa-trash-can"></i> Remove condition</a>
195
              </div>
196
            </div>
197
            <div class="blocks">
198
              <h5>Substitutions</h5>
199
              <div class="block">
200
                <select required="required" class="required" name="substitution_field">
201
                  <option value="">Choose a field name</option>
202
                  [% FOR field IN substitution_fields.sort %]
203
                    <option value="[% field | html %]">[% field | html %]</option>
204
                  [% END %]
205
                </select>
206
                =
207
                <input type="text" value="" name="substitution_value" list="substitution-fields" autocomplete="off"/>
208
                <a class="add_block" href="#"><i class="fa fa-plus"></i> Add a substitution</a>
209
                <a class="remove_block" href="#"><i class="fa fa-trash-can"></i> Remove substitution</a>
210
                <span class="required">A field name is required</span>
211
              </div>
212
            </div>
213
          </fieldset>
214
        [% ELSIF rules %]
215
            <div>
216
                <h4>List of rules</h4>
217
218
                <div class="page-section">
219
                    <table id="rulest">
220
                        <thead>
221
                          <tr>
222
                               <th>Event</th>
223
                                <th>Conditions</th>
224
                                <th>Substitutions</th>
225
                            </tr>
226
                        </thead>
227
                        <tbody>
228
                            [% FOR rule IN rules %]
229
                                <tr>
230
                                    <td>[% rule.event | html %]</td>
231
                                    <td>
232
                                        [% FOR condition IN rule.conditions %]
233
                                            [% IF condition.field %]
234
                                                <div class="block">
235
                                                [% condition.field | html %] = [% condition.value | html %]
236
                                                </div>
237
                                            [% ELSE %]
238
                                                There is no condition for this rule.
239
                                            [% END %]
240
                                        [% END %]
241
                                    </td>
242
                                    <td>
243
                                        [% FOR substitution IN rule.substitutions %]
244
                                            <div class="block">
245
                                                [% substitution.field | html %] = [% substitution.value | html %]
246
                                            </div>
247
                                        [% END %]
248
                                    </td>
249
                                </tr>
250
                            [% END %]
251
                        </tbody>
252
                    </table>
253
                </div> <!-- /.page-section -->
254
            </div>
255
        [% ELSE %]
256
            <div class="dialog message">
257
                There are no rules defined.
258
            </div>
259
        [% END %]
260
261
            </main>
262
        </div>
263
264
        <div class="col-md-2 order-md-1 order-sm-2">
265
            <aside>
266
                [% INCLUDE 'cat-menu.inc' %]
267
            </aside>
268
        </div>
269
     </div> <!-- /.row -->
270
271
[% MACRO jsinclude BLOCK %]
272
    [% Asset.js("lib/hc-sticky/hc-sticky.js") | $raw %]
273
    [% Asset.js("js/automatic_item_modification_by_event.js") | $raw %]
274
    [% IF op == 'edit_form' %]
275
        <script>
276
            var Sticky;
277
            $(document).ready(function() {
278
                  [% IF ( op == 'edit_form' ) %]
279
                Sticky = $("#toolbar");
280
                Sticky.hcSticky({
281
                    stickTo: "main",
282
                    stickyClass: "floating"
283
                });
284
                  [% END %]
285
                  [% IF rules.size > 0 %]
286
                      $("#norules").hide();
287
                  [% ELSE %]
288
                      $("#rules").show();
289
                  [% END %]
290
            });
291
        </script>
292
    [% END %]
293
[% END %]
294
295
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/js/automatic_item_modification_by_event.js (+120 lines)
Line 0 Link Here
1
function clear_inputs(node, new_node) {
2
    var selects = $(node).find("select");
3
    $(selects).each(function(i) {
4
        var select = this;
5
        $(new_node).find("select").eq(i).val($(select).val());
6
    });
7
    var inputs = $(node).find("input");
8
    $(inputs).each(function(i) {
9
        var input = this;
10
        $(new_node).find("input").eq(i).val($(input).val());
11
    });
12
}
13
14
function remove_block_action( link ) {
15
    var blocks = $(link).parent().parent();
16
    if( $(blocks).find(".block").length > 2 ) {
17
        $(blocks).find("a.remove_block").show();
18
    } else {
19
        $(blocks).find("a.remove_block").hide();
20
    }
21
    $(link).parent().remove();
22
}
23
24
function remove_rule_action( link ) {
25
    if( $("#rules").find(".rule").length < 2 ) {
26
            $("#rules").hide();
27
            $("#norules").show();
28
    }
29
    $(link).parent().parent().remove();
30
    update_rule_count();
31
}
32
33
function clone_block(block) {
34
    var new_block = $(block).clone(1);
35
    clear_inputs(block, new_block);
36
    $(new_block).find('a.remove_block').show();
37
    var blocks = $(block).parent();
38
    $(blocks).append(new_block);
39
    $(blocks).find('a.remove_block').click(function(e){
40
        e.preventDefault();
41
        remove_block_action($(this));
42
    }).show();
43
}
44
45
function update_rule_count(){
46
    rules = $(".rulecount");
47
    rules.each( function( i ){
48
        $(this).text( i + 1 );
49
    });
50
}
51
52
$(document).ready(function() {
53
    $("#new_rule .remove_rule").hide();
54
    $("#new_rule a.remove_block").hide();
55
    $("#rules a.remove_block").click(function(e){
56
        e.preventDefault();
57
        remove_block_action($(this));
58
    });
59
    $("#rules .remove_rule").click(function(e){
60
        e.preventDefault();
61
        remove_rule_action($(this));
62
    });
63
64
    var unique_id = $(".rule").length + 1;
65
    $(".add_rule").click(function(e){
66
        e.preventDefault();
67
        var rule = $("#new_rule");
68
        var rules = $("#rules");
69
        var new_rule = rule.clone(1);
70
        new_rule.removeAttr('id');
71
        new_rule.attr('class', 'rule');
72
        clear_inputs(rule, new_rule);
73
        new_rule.find("select[name='condition_field']").attr('name', 'condition_field_' + unique_id);
74
        new_rule.find("select[name='substitution_field']").attr('name', 'substitution_field_' + unique_id);
75
        new_rule.find("input[name='condition_value']").attr('name', 'condition_value_' + unique_id);
76
        new_rule.find("input[name='substitution_value']").attr('name', 'substitution_value_' + unique_id);
77
        new_rule.find("select[name='event']").attr('name', 'event_' + unique_id);
78
        new_rule.find("input[name='unique_id']").val(unique_id);
79
80
        $("#rules").append(new_rule);
81
        update_rule_count();
82
        var scrollToPoint = new_rule.position();
83
        window.scroll(0, scrollToPoint.top - $("#toolbar").height() );
84
85
        if( $("#rules").find(".rule").length > 0 ) {
86
                $("#rules").show();
87
                $("#norules").hide();
88
        }
89
        if( $("#rules").find(".conditions > .condition").length > 1 ) {
90
91
        }
92
        if( $("#rules").find(".conditions > .condition").length > 1 ) {
93
94
        }
95
        new_rule.find('.remove_rule').click(function(e){
96
            e.preventDefault();
97
            remove_rule_action( $(this) );
98
        }).show();
99
        new_rule.find('.add_rule').remove();
100
        unique_id++;
101
    });
102
103
    $("a.add_block").click(function(e){
104
        e.preventDefault();
105
        clone_block( $(this).parent() );
106
    });
107
108
    if( $("#rules").find(".rule").length < 1 ) {
109
            $("#rules").hide();
110
            $("#norules").show();
111
    }
112
113
    $("#rules .rule .blocks").each(function(){
114
        if ( $(this).find(".block").length == 1 ) {
115
            $(this).find("a.remove_block").hide();
116
        }
117
    });
118
119
    $("#rules_form").validate();
120
});
(-)a/t/db_dependent/Koha/Item.t (-62 / +1 lines)
Lines 20-26 Link Here
20
use Modern::Perl;
20
use Modern::Perl;
21
use utf8;
21
use utf8;
22
22
23
use Test::More tests => 38;
23
use Test::More tests => 37;
24
use Test::Exception;
24
use Test::Exception;
25
use Test::MockModule;
25
use Test::MockModule;
26
use Test::Warn;
26
use Test::Warn;
Lines 2795-2861 subtest 'current_branchtransfers relationship' => sub { Link Here
2795
    $schema->storage->txn_rollback;
2795
    $schema->storage->txn_rollback;
2796
};
2796
};
2797
2797
2798
subtest 'location_update_trigger() tests' => sub {
2799
2800
    plan tests => 10;
2801
2802
    $schema->storage->txn_begin;
2803
2804
    my $location           = 'YA';
2805
    my $permanent_location = 'HEY';
2806
2807
    foreach my $action (qw{ checkin checkout }) {
2808
2809
        my $pref = $action eq 'checkin' ? 'UpdateItemLocationOnCheckin' : 'UpdateItemLocationOnCheckout';
2810
2811
        t::lib::Mocks::mock_preference( 'UpdateItemLocationOnCheckin',  q{} );
2812
        t::lib::Mocks::mock_preference( 'UpdateItemLocationOnCheckout', q{} );
2813
2814
        my $item = $builder->build_sample_item( { location => $location, permanent_location => $permanent_location } );
2815
2816
        $item->location_update_trigger($action);
2817
2818
        is( $item->location, $location, "$pref does not modify value when not enabled" );
2819
2820
        t::lib::Mocks::mock_preference( $pref, qq{$location: GEN} );
2821
2822
        $item->location_update_trigger($action);
2823
2824
        is( $item->location, 'GEN', qq{'location' value set from '$location' to 'GEN' with setting `$location: GEN`} );
2825
2826
        t::lib::Mocks::mock_preference( $pref, q{_ALL_: BOO} );
2827
2828
        $item->location_update_trigger($action);
2829
2830
        is( $item->location, 'BOO', q{`_ALL_` does the job} );
2831
2832
        t::lib::Mocks::mock_preference( $pref, qq{$location: _BLANK_} );
2833
        $item->location($location)->store();
2834
2835
        $item->location_update_trigger($action);
2836
        is( $item->location, q{}, q{`_BLANK_` does the job} );
2837
2838
        t::lib::Mocks::mock_preference( $pref, qq{GEN: _BLANK_\n_BLANK_: PROC\n$location: _PERM_} );
2839
2840
        $item->set(
2841
            {
2842
                location           => $location,
2843
                permanent_location =>
2844
                    $permanent_location,    # setting `permanent_location` explicitly because ->store messes with it.
2845
            }
2846
        )->store;
2847
2848
        $item->location_update_trigger($action);
2849
2850
        is(
2851
            $item->location, $permanent_location,
2852
            q{_PERM_ does the job"}
2853
        );
2854
    }
2855
2856
    $schema->storage->txn_rollback;
2857
};
2858
2859
subtest 'bookings' => sub {
2798
subtest 'bookings' => sub {
2860
    plan tests => 4;
2799
    plan tests => 4;
2861
2800
(-)a/t/db_dependent/Koha/Item/trigger_automatic_modifications_by_event.t (+168 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use JSON qw(encode_json);
6
use Test::More;
7
8
use t::lib::Mocks;
9
use t::lib::TestBuilder;
10
11
my $builder = t::lib::TestBuilder->new;
12
13
subtest 'simple substitution' => sub {
14
    plan tests => 1;
15
16
    my $rules = [
17
        {
18
            event         => 'checkin',
19
            conditions    => [],
20
            substitutions => [
21
                { field => 'items.damaged', value => '1' },
22
            ],
23
        },
24
    ];
25
    t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) );
26
27
    my $item = Koha::Item->new;
28
    $item->damaged('0');
29
    $item->trigger_automatic_modifications_by_event('checkin');
30
    is( $item->damaged, '1' );
31
};
32
33
subtest 'field substitution' => sub {
34
    plan tests => 1;
35
36
    my $rules = [
37
        {
38
            event         => 'checkin',
39
            conditions    => [],
40
            substitutions => [
41
                { field => 'items.location', value => '$items.permanent_location' },
42
            ],
43
        },
44
    ];
45
    t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) );
46
47
    my $item = Koha::Item->new;
48
    $item->location('CART');
49
    $item->permanent_location('CHILD');
50
    $item->trigger_automatic_modifications_by_event('checkin');
51
    is( $item->location, $item->permanent_location );
52
};
53
54
subtest 'blank substitution' => sub {
55
    plan tests => 2;
56
57
    my $rules = [
58
        {
59
            event         => 'checkin',
60
            conditions    => [],
61
            substitutions => [
62
                { field => 'items.location', value => '' },
63
                { field => 'items.damaged' },                 # no value is the same as an empty string
64
            ],
65
        },
66
    ];
67
    t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) );
68
69
    my $item = Koha::Item->new;
70
    $item->location('CART');
71
    $item->damaged('1');
72
    $item->trigger_automatic_modifications_by_event('checkin');
73
    is( $item->location, '' );
74
    is( $item->damaged,  '' );
75
};
76
77
subtest 'conditions' => sub {
78
    plan tests => 2;
79
80
    my $rules = [
81
        {
82
            event      => 'checkin',
83
            conditions => [
84
                { field => 'items.location',           value => 'NEW' },
85
                { field => 'items.permanent_location', value => 'CHILD' },
86
            ],
87
            substitutions => [
88
                { field => 'items.location', value => 'CART' },
89
            ],
90
        },
91
    ];
92
    t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) );
93
94
    my $item = Koha::Item->new;
95
96
    # Conditions are not met, item is not modified
97
    $item->location('PROC');
98
    $item->permanent_location('CHILD');
99
    $item->trigger_automatic_modifications_by_event('checkin');
100
    is( $item->location, 'PROC' );
101
102
    # Conditions are met, item is modified
103
    $item->location('NEW');
104
    $item->permanent_location('CHILD');
105
    $item->trigger_automatic_modifications_by_event('checkin');
106
    is( $item->location, 'CART' );
107
};
108
109
subtest 'blank condition' => sub {
110
    plan tests => 2;
111
112
    my $rules = [
113
        {
114
            event      => 'checkin',
115
            conditions => [
116
                { field => 'items.location', value => '' },
117
            ],
118
            substitutions => [
119
                { field => 'items.location', value => 'CART' },
120
            ],
121
        },
122
    ];
123
    t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) );
124
125
    my $item = Koha::Item->new;
126
127
    # Conditions are not met, item is not modified
128
    $item->location('PROC');
129
    $item->trigger_automatic_modifications_by_event('checkin');
130
    is( $item->location, 'PROC' );
131
132
    # Conditions are met, item is modified
133
    $item->location('');
134
    $item->trigger_automatic_modifications_by_event('checkin');
135
    is( $item->location, 'CART' );
136
};
137
138
subtest 'condition on biblio and biblioitems fields' => sub {
139
    plan tests => 2;
140
141
    my $rules = [
142
        {
143
            event      => 'checkin',
144
            conditions => [
145
                { field => 'biblio.title',       value => 'No title' },
146
                { field => 'biblioitems.volume', value => '1' },
147
            ],
148
            substitutions => [
149
                { field => 'items.location', value => 'CART' },
150
            ],
151
        },
152
    ];
153
    t::lib::Mocks::mock_preference( 'automatic_item_modification_by_event_configuration', encode_json($rules) );
154
155
    my $item = $builder->build_object( { class => 'Koha::Items' } );
156
157
    $item->location('PROC');
158
    $item->biblio->title('A title');
159
    $item->biblioitem->volume('1');
160
    $item->trigger_automatic_modifications_by_event('checkin');
161
    is( $item->location, 'PROC' );
162
163
    $item->biblio->title('No title');
164
    $item->trigger_automatic_modifications_by_event('checkin');
165
    is( $item->location, 'CART' );
166
};
167
168
done_testing;
(-)a/tools/automatic_item_modification_by_event.pl (-1 / +114 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
=head1 NAME
19
20
automatic_item_modification_by_event.pl: Configuration for automatic update of items at checkin/checkout
21
22
=cut
23
24
use Modern::Perl;
25
26
use CGI;
27
use JSON qw( encode_json decode_json );
28
29
use C4::Auth qw( get_template_and_user );
30
use C4::Context;
31
use C4::Output qw( output_html_with_http_headers );
32
33
use Koha::Items;
34
use Koha::Biblioitems;
35
use Koha::Biblios;
36
37
my $cgi = CGI->new;
38
39
# open template
40
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
41
    {
42
        template_name => "tools/automatic_item_modification_by_event.tt",
43
        query         => $cgi,
44
        type          => "intranet",
45
        flagsrequired => { tools => 'items_batchmod' },
46
    }
47
);
48
49
my $op = $cgi->param('op') // 'cud-show';
50
51
my $syspref_name = q|automatic_item_modification_by_event_configuration|;
52
if ( $op eq 'cud-update' ) {
53
    my @rules;
54
    my @unique_ids = $cgi->multi_param('unique_id');
55
    for my $unique_id (@unique_ids) {
56
        my @substitution_fields = $cgi->multi_param("substitution_field_$unique_id");
57
        my @substitution_values = $cgi->multi_param("substitution_value_$unique_id");
58
        my @condition_fields    = $cgi->multi_param("condition_field_$unique_id");
59
        my @condition_values    = $cgi->multi_param("condition_value_$unique_id");
60
        my $rule                = {
61
            substitutions => [],
62
            conditions    => [],
63
        };
64
        for my $value (@substitution_values) {
65
            my $field = shift @substitution_fields;
66
            last unless $field;
67
            push @{ $rule->{substitutions} }, { field => $field, value => $value };
68
        }
69
        push @{ $rule->{substitutions} }, {}
70
            unless @{ $rule->{substitutions} };
71
        for my $value (@condition_values) {
72
            my $field = shift @condition_fields;
73
            last unless $field;
74
            push @{ $rule->{conditions} }, { field => $field, value => $value };
75
        }
76
        push @{ $rule->{conditions} }, {}
77
            unless @{ $rule->{conditions} };
78
        $rule->{event} = $cgi->param("event_$unique_id");
79
80
        push @rules, $rule;
81
    }
82
    my $syspref_content = encode_json( \@rules );
83
    C4::Context->set_preference( $syspref_name, $syspref_content );
84
85
    $op = 'cud-show';
86
}
87
88
my @messages;
89
my $syspref_content = C4::Context->preference($syspref_name);
90
my $rules;
91
$rules = eval { decode_json($syspref_content) }
92
    if $syspref_content;
93
if ($@) {
94
    push @messages, {
95
        type => 'error',
96
        code => 'unable_to_load_configuration'
97
    };
98
    $template->param( messages => \@messages );
99
    output_html_with_http_headers $cgi, $cookie, $template->output;
100
    exit;
101
}
102
103
my @item_fields       = map { "items.$_" } Koha::Items->columns;
104
my @biblioitem_fields = map { "biblioitems.$_" } Koha::Biblioitems->columns;
105
my @biblio_fields     = map { "biblio.$_" } Koha::Biblios->columns;
106
$template->param(
107
    op                  => $op,
108
    messages            => \@messages,
109
    condition_fields    => [ @item_fields, @biblioitem_fields, @biblio_fields ],
110
    substitution_fields => \@item_fields,
111
    rules               => $rules,
112
);
113
114
output_html_with_http_headers $cgi, $cookie, $template->output;

Return to bug 38387