Bug 30728: Add unit tests
[srvgit] / t / db_dependent / Koha / Item.t
1 #!/usr/bin/perl
2
3 # Copyright 2019 Koha Development team
4 #
5 # This file is part of Koha
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use Modern::Perl;
21 use utf8;
22
23 use Test::More tests => 15;
24 use Test::Exception;
25 use Test::MockModule;
26
27 use C4::Biblio qw( GetMarcSubfieldStructure );
28 use C4::Circulation qw( AddIssue AddReturn );
29
30 use Koha::Caches;
31 use Koha::Items;
32 use Koha::Database;
33 use Koha::DateUtils qw( dt_from_string );
34 use Koha::Old::Items;
35
36 use List::MoreUtils qw(all);
37
38 use t::lib::TestBuilder;
39 use t::lib::Mocks;
40
41 my $schema  = Koha::Database->new->schema;
42 my $builder = t::lib::TestBuilder->new;
43
44 subtest 'tracked_links relationship' => sub {
45     plan tests => 3;
46
47     my $biblio = $builder->build_sample_biblio();
48     my $item   = $builder->build_sample_item({
49         biblionumber => $biblio->biblionumber,
50     });
51     my $tracked_links = $item->tracked_links;
52     is( ref($tracked_links), 'Koha::TrackedLinks', 'tracked_links returns a Koha::TrackedLinks object set' );
53     is($item->tracked_links->count, 0, "Empty Koha::TrackedLinks set returned if no tracked_links");
54     my $link1 = $builder->build({ source => 'Linktracker', value => { itemnumber => $item->itemnumber }});
55     my $link2 = $builder->build({ source => 'Linktracker', value => { itemnumber => $item->itemnumber }});
56
57     is($item->tracked_links()->count,2,"Two tracked links found");
58 };
59
60 subtest 'hidden_in_opac() tests' => sub {
61
62     plan tests => 4;
63
64     $schema->storage->txn_begin;
65
66     my $item  = $builder->build_sample_item({ itemlost => 2 });
67     my $rules = {};
68
69     # disable hidelostitems as it interteres with OpachiddenItems for the calculation
70     t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
71
72     ok( !$item->hidden_in_opac, 'No rules passed, shouldn\'t hide' );
73     ok( !$item->hidden_in_opac({ rules => $rules }), 'Empty rules passed, shouldn\'t hide' );
74
75     # enable hidelostitems to verify correct behaviour
76     t::lib::Mocks::mock_preference( 'hidelostitems', 1 );
77     ok( $item->hidden_in_opac, 'Even with no rules, item should hide because of hidelostitems syspref' );
78
79     # disable hidelostitems
80     t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
81     my $withdrawn = $item->withdrawn + 1; # make sure this attribute doesn't match
82
83     $rules = { withdrawn => [$withdrawn], itype => [ $item->itype ] };
84
85     ok( $item->hidden_in_opac({ rules => $rules }), 'Rule matching itype passed, should hide' );
86
87
88
89     $schema->storage->txn_rollback;
90 };
91
92 subtest 'has_pending_hold() tests' => sub {
93
94     plan tests => 2;
95
96     $schema->storage->txn_begin;
97
98     my $dbh = C4::Context->dbh;
99     my $item  = $builder->build_sample_item({ itemlost => 0 });
100     my $itemnumber = $item->itemnumber;
101
102     $dbh->do("INSERT INTO tmp_holdsqueue (surname,borrowernumber,itemnumber) VALUES ('Clamp',42,$itemnumber)");
103     ok( $item->has_pending_hold, "Yes, we have a pending hold");
104     $dbh->do("DELETE FROM tmp_holdsqueue WHERE itemnumber=$itemnumber");
105     ok( !$item->has_pending_hold, "We don't have a pending hold if nothing in the tmp_holdsqueue");
106
107     $schema->storage->txn_rollback;
108 };
109
110 subtest "as_marc_field() tests" => sub {
111
112     my $mss = C4::Biblio::GetMarcSubfieldStructure( '' );
113     my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
114
115     my @schema_columns = $schema->resultset('Item')->result_source->columns;
116     my @mapped_columns = grep { exists $mss->{'items.'.$_} } @schema_columns;
117
118     plan tests => 2 * (scalar @mapped_columns + 1) + 3;
119
120     $schema->storage->txn_begin;
121
122     my $item = $builder->build_sample_item;
123     # Make sure it has at least one undefined attribute
124     $item->set({ replacementprice => undef })->store->discard_changes;
125
126     # Tests with the mss parameter
127     my $marc_field = $item->as_marc_field({ mss => $mss });
128
129     is(
130         $marc_field->tag,
131         $itemtag,
132         'Generated field set the right tag number'
133     );
134
135     foreach my $column ( @mapped_columns ) {
136         my $tagsubfield = $mss->{ 'items.' . $column }[0]->{tagsubfield};
137         is( $marc_field->subfield($tagsubfield),
138             $item->$column, "Value is mapped correctly for column $column" );
139     }
140
141     # Tests without the mss parameter
142     $marc_field = $item->as_marc_field();
143
144     is(
145         $marc_field->tag,
146         $itemtag,
147         'Generated field set the right tag number'
148     );
149
150     foreach my $column (@mapped_columns) {
151         my $tagsubfield = $mss->{ 'items.' . $column }[0]->{tagsubfield};
152         is( $marc_field->subfield($tagsubfield),
153             $item->$column, "Value is mapped correctly for column $column" );
154     }
155
156     my $unmapped_subfield = Koha::MarcSubfieldStructure->new(
157         {
158             frameworkcode => '',
159             tagfield      => $itemtag,
160             tagsubfield   => 'X',
161         }
162     )->store;
163
164     my @unlinked_subfields;
165     push @unlinked_subfields, X => 'Something weird';
166     $item->more_subfields_xml( C4::Items::_get_unlinked_subfields_xml( \@unlinked_subfields ) )->store;
167
168     Koha::Caches->get_instance->clear_from_cache( "MarcStructure-1-" );
169     Koha::MarcSubfieldStructures->search(
170         { frameworkcode => '', tagfield => $itemtag } )
171       ->update( { display_order => \['FLOOR( 1 + RAND( ) * 10 )'] } );
172
173     $marc_field = $item->as_marc_field;
174
175     my $tagslib = C4::Biblio::GetMarcStructure(1, '');
176     my @subfields = $marc_field->subfields;
177     my $result = all { defined $_->[1] } @subfields;
178     ok( $result, 'There are no undef subfields' );
179     my @ordered_subfields = sort {
180             $tagslib->{$itemtag}->{ $a->[0] }->{display_order}
181         <=> $tagslib->{$itemtag}->{ $b->[0] }->{display_order}
182     } @subfields;
183     is_deeply(\@subfields, \@ordered_subfields);
184
185     is( scalar $marc_field->subfield('X'), 'Something weird', 'more_subfield_xml is considered' );
186
187     $schema->storage->txn_rollback;
188     Koha::Caches->get_instance->clear_from_cache( "MarcStructure-1-" );
189 };
190
191 subtest 'pickup_locations' => sub {
192     plan tests => 66;
193
194     $schema->storage->txn_begin;
195
196     my $dbh = C4::Context->dbh;
197
198     my $root1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { ft_local_hold_group => 1, branchcode => undef } } );
199     my $root2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { ft_local_hold_group => 1, branchcode => undef } } );
200     my $library1 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, } } );
201     my $library2 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, } } );
202     my $library3 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 0, } } );
203     my $library4 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, } } );
204     my $group1_1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root1->id, branchcode => $library1->branchcode } } );
205     my $group1_2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root1->id, branchcode => $library2->branchcode } } );
206
207     my $group2_1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root2->id, branchcode => $library3->branchcode } } );
208     my $group2_2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root2->id, branchcode => $library4->branchcode } } );
209
210     our @branchcodes = (
211         $library1->branchcode, $library2->branchcode,
212         $library3->branchcode, $library4->branchcode
213     );
214
215     my $item1 = $builder->build_sample_item(
216         {
217             homebranch    => $library1->branchcode,
218             holdingbranch => $library2->branchcode,
219             copynumber    => 1,
220             ccode         => 'Gollum'
221         }
222     )->store;
223
224     my $item3 = $builder->build_sample_item(
225         {
226             homebranch    => $library3->branchcode,
227             holdingbranch => $library4->branchcode,
228             copynumber    => 3,
229             itype         => $item1->itype,
230         }
231     )->store;
232
233     Koha::CirculationRules->set_rules(
234         {
235             categorycode => undef,
236             itemtype     => $item1->itype,
237             branchcode   => undef,
238             rules        => {
239                 reservesallowed => 25,
240             }
241         }
242     );
243
244
245     my $patron1 = $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $library1->branchcode, firstname => '1' } } );
246     my $patron4 = $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $library4->branchcode, firstname => '4' } } );
247
248     my $results = {
249         "1-1-from_home_library-any"               => 3,
250         "1-1-from_home_library-holdgroup"         => 2,
251         "1-1-from_home_library-patrongroup"       => 2,
252         "1-1-from_home_library-homebranch"        => 1,
253         "1-1-from_home_library-holdingbranch"     => 1,
254         "1-1-from_any_library-any"                => 3,
255         "1-1-from_any_library-holdgroup"          => 2,
256         "1-1-from_any_library-patrongroup"        => 2,
257         "1-1-from_any_library-homebranch"         => 1,
258         "1-1-from_any_library-holdingbranch"      => 1,
259         "1-1-from_local_hold_group-any"           => 3,
260         "1-1-from_local_hold_group-holdgroup"     => 2,
261         "1-1-from_local_hold_group-patrongroup"   => 2,
262         "1-1-from_local_hold_group-homebranch"    => 1,
263         "1-1-from_local_hold_group-holdingbranch" => 1,
264         "1-4-from_home_library-any"               => 0,
265         "1-4-from_home_library-holdgroup"         => 0,
266         "1-4-from_home_library-patrongroup"       => 0,
267         "1-4-from_home_library-homebranch"        => 0,
268         "1-4-from_home_library-holdingbranch"     => 0,
269         "1-4-from_any_library-any"                => 3,
270         "1-4-from_any_library-holdgroup"          => 2,
271         "1-4-from_any_library-patrongroup"        => 1,
272         "1-4-from_any_library-homebranch"         => 1,
273         "1-4-from_any_library-holdingbranch"      => 1,
274         "1-4-from_local_hold_group-any"           => 0,
275         "1-4-from_local_hold_group-holdgroup"     => 0,
276         "1-4-from_local_hold_group-patrongroup"   => 0,
277         "1-4-from_local_hold_group-homebranch"    => 0,
278         "1-4-from_local_hold_group-holdingbranch" => 0,
279         "3-1-from_home_library-any"               => 0,
280         "3-1-from_home_library-holdgroup"         => 0,
281         "3-1-from_home_library-patrongroup"       => 0,
282         "3-1-from_home_library-homebranch"        => 0,
283         "3-1-from_home_library-holdingbranch"     => 0,
284         "3-1-from_any_library-any"                => 3,
285         "3-1-from_any_library-holdgroup"          => 1,
286         "3-1-from_any_library-patrongroup"        => 2,
287         "3-1-from_any_library-homebranch"         => 0,
288         "3-1-from_any_library-holdingbranch"      => 1,
289         "3-1-from_local_hold_group-any"           => 0,
290         "3-1-from_local_hold_group-holdgroup"     => 0,
291         "3-1-from_local_hold_group-patrongroup"   => 0,
292         "3-1-from_local_hold_group-homebranch"    => 0,
293         "3-1-from_local_hold_group-holdingbranch" => 0,
294         "3-4-from_home_library-any"               => 0,
295         "3-4-from_home_library-holdgroup"         => 0,
296         "3-4-from_home_library-patrongroup"       => 0,
297         "3-4-from_home_library-homebranch"        => 0,
298         "3-4-from_home_library-holdingbranch"     => 0,
299         "3-4-from_any_library-any"                => 3,
300         "3-4-from_any_library-holdgroup"          => 1,
301         "3-4-from_any_library-patrongroup"        => 1,
302         "3-4-from_any_library-homebranch"         => 0,
303         "3-4-from_any_library-holdingbranch"      => 1,
304         "3-4-from_local_hold_group-any"           => 3,
305         "3-4-from_local_hold_group-holdgroup"     => 1,
306         "3-4-from_local_hold_group-patrongroup"   => 1,
307         "3-4-from_local_hold_group-homebranch"    => 0,
308         "3-4-from_local_hold_group-holdingbranch" => 1
309     };
310
311     sub _doTest {
312         my ( $item, $patron, $ha, $hfp, $results ) = @_;
313
314         Koha::CirculationRules->set_rules(
315             {
316                 branchcode => undef,
317                 itemtype   => undef,
318                 rules => {
319                     holdallowed => $ha,
320                     hold_fulfillment_policy => $hfp,
321                     returnbranch => 'any'
322                 }
323             }
324         );
325         my $ha_value =
326           $ha eq 'from_local_hold_group' ? 'holdgroup'
327           : (
328             $ha eq 'from_any_library' ? 'any'
329             : 'homebranch'
330           );
331
332         my @pl = map {
333             my $pickup_location = $_;
334             grep { $pickup_location->branchcode eq $_ } @branchcodes
335         } $item->pickup_locations( { patron => $patron } )->as_list;
336
337         ok(
338             scalar(@pl) eq $results->{
339                     $item->copynumber . '-'
340                   . $patron->firstname . '-'
341                   . $ha . '-'
342                   . $hfp
343             },
344             'item'
345               . $item->copynumber
346               . ', patron'
347               . $patron->firstname
348               . ', holdallowed: '
349               . $ha_value
350               . ', hold_fulfillment_policy: '
351               . $hfp
352               . ' should return '
353               . $results->{
354                     $item->copynumber . '-'
355                   . $patron->firstname . '-'
356                   . $ha . '-'
357                   . $hfp
358               }
359               . ' and returns '
360               . scalar(@pl)
361         );
362
363     }
364
365
366     foreach my $item ($item1, $item3) {
367         foreach my $patron ($patron1, $patron4) {
368             #holdallowed 1: homebranch, 2: any, 3: holdgroup
369             foreach my $ha ('from_home_library', 'from_any_library', 'from_local_hold_group') {
370                 foreach my $hfp ('any', 'holdgroup', 'patrongroup', 'homebranch', 'holdingbranch') {
371                     _doTest($item, $patron, $ha, $hfp, $results);
372                 }
373             }
374         }
375     }
376
377     # Now test that branchtransferlimits will further filter the pickup locations
378
379     my $item_no_ccode = $builder->build_sample_item(
380         {
381             homebranch    => $library1->branchcode,
382             holdingbranch => $library2->branchcode,
383             itype         => $item1->itype,
384         }
385     )->store;
386
387     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
388     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
389     Koha::CirculationRules->set_rules(
390         {
391             branchcode => undef,
392             itemtype   => $item1->itype,
393             rules      => {
394                 holdallowed             => 'from_home_library',
395                 hold_fulfillment_policy => 1,
396                 returnbranch            => 'any'
397             }
398         }
399     );
400     $builder->build_object(
401         {
402             class => 'Koha::Item::Transfer::Limits',
403             value => {
404                 toBranch   => $library1->branchcode,
405                 fromBranch => $library2->branchcode,
406                 itemtype   => $item1->itype,
407                 ccode      => undef,
408             }
409         }
410     );
411
412     my @pickup_locations = map {
413         my $pickup_location = $_;
414         grep { $pickup_location->branchcode eq $_ } @branchcodes
415     } $item1->pickup_locations( { patron => $patron1 } )->as_list;
416
417     is( scalar @pickup_locations, 3 - 1, "With a transfer limits we get back the libraries that are pickup locations minus 1 limited library");
418
419     $builder->build_object(
420         {
421             class => 'Koha::Item::Transfer::Limits',
422             value => {
423                 toBranch   => $library4->branchcode,
424                 fromBranch => $library2->branchcode,
425                 itemtype   => $item1->itype,
426                 ccode      => undef,
427             }
428         }
429     );
430
431     @pickup_locations = map {
432         my $pickup_location = $_;
433         grep { $pickup_location->branchcode eq $_ } @branchcodes
434     } $item1->pickup_locations( { patron => $patron1 } )->as_list;
435
436     is( scalar @pickup_locations, 3 - 2, "With 2 transfer limits we get back the libraries that are pickup locations minus 2 limited libraries");
437
438     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'ccode');
439     @pickup_locations = map {
440         my $pickup_location = $_;
441         grep { $pickup_location->branchcode eq $_ } @branchcodes
442     } $item1->pickup_locations( { patron => $patron1 } )->as_list;
443     is( scalar @pickup_locations, 3, "With no transfer limits of type ccode we get back the libraries that are pickup locations");
444
445     @pickup_locations = map {
446         my $pickup_location = $_;
447         grep { $pickup_location->branchcode eq $_ } @branchcodes
448     } $item_no_ccode->pickup_locations( { patron => $patron1 } )->as_list;
449     is( scalar @pickup_locations, 3, "With no transfer limits of type ccode and an item with no ccode we get back the libraries that are pickup locations");
450
451     $builder->build_object(
452         {
453             class => 'Koha::Item::Transfer::Limits',
454             value => {
455                 toBranch   => $library2->branchcode,
456                 fromBranch => $library2->branchcode,
457                 itemtype   => undef,
458                 ccode      => $item1->ccode,
459             }
460         }
461     );
462
463     @pickup_locations = map {
464         my $pickup_location = $_;
465         grep { $pickup_location->branchcode eq $_ } @branchcodes
466     } $item1->pickup_locations( { patron => $patron1 } )->as_list;
467     is( scalar @pickup_locations, 3 - 1, "With a transfer limits we get back the libraries that are pickup locations minus 1 limited library");
468
469     $builder->build_object(
470         {
471             class => 'Koha::Item::Transfer::Limits',
472             value => {
473                 toBranch   => $library4->branchcode,
474                 fromBranch => $library2->branchcode,
475                 itemtype   => undef,
476                 ccode      => $item1->ccode,
477             }
478         }
479     );
480
481     @pickup_locations = map {
482         my $pickup_location = $_;
483         grep { $pickup_location->branchcode eq $_ } @branchcodes
484     } $item1->pickup_locations( { patron => $patron1 } )->as_list;
485     is( scalar @pickup_locations, 3 - 2, "With 2 transfer limits we get back the libraries that are pickup locations minus 2 limited libraries");
486
487     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 0);
488
489     $schema->storage->txn_rollback;
490 };
491
492 subtest 'request_transfer' => sub {
493     plan tests => 13;
494     $schema->storage->txn_begin;
495
496     my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
497     my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
498     my $item     = $builder->build_sample_item(
499         {
500             homebranch    => $library1->branchcode,
501             holdingbranch => $library2->branchcode,
502         }
503     );
504
505     # Mandatory fields tests
506     throws_ok { $item->request_transfer( { to => $library1 } ) }
507     'Koha::Exceptions::MissingParameter',
508       'Exception thrown if `reason` parameter is missing';
509
510     throws_ok { $item->request_transfer( { reason => 'Manual' } ) }
511     'Koha::Exceptions::MissingParameter',
512       'Exception thrown if `to` parameter is missing';
513
514     # Successful request
515     my $transfer = $item->request_transfer({ to => $library1, reason => 'Manual' });
516     is( ref($transfer), 'Koha::Item::Transfer',
517         'Koha::Item->request_transfer should return a Koha::Item::Transfer object'
518     );
519     my $original_transfer = $transfer->get_from_storage;
520
521     # Transfer already in progress
522     throws_ok { $item->request_transfer( { to => $library2, reason => 'Manual' } ) }
523     'Koha::Exceptions::Item::Transfer::InQueue',
524       'Exception thrown if transfer is already in progress';
525
526     my $exception = $@;
527     is( ref( $exception->transfer ),
528         'Koha::Item::Transfer',
529         'The exception contains the found Koha::Item::Transfer' );
530
531     # Queue transfer
532     my $queued_transfer = $item->request_transfer(
533         { to => $library2, reason => 'Manual', enqueue => 1 } );
534     is( ref($queued_transfer), 'Koha::Item::Transfer',
535         'Koha::Item->request_transfer allowed when enqueue is set' );
536     my $transfers = $item->get_transfers;
537     is($transfers->count, 2, "There are now 2 live transfers in the queue");
538     $transfer = $transfer->get_from_storage;
539     is_deeply($transfer->unblessed, $original_transfer->unblessed, "Original transfer unchanged");
540     $queued_transfer->datearrived(dt_from_string)->store();
541
542     # Replace transfer
543     my $replaced_transfer = $item->request_transfer(
544         { to => $library2, reason => 'Manual', replace => 1 } );
545     is( ref($replaced_transfer), 'Koha::Item::Transfer',
546         'Koha::Item->request_transfer allowed when replace is set' );
547     $original_transfer->discard_changes;
548     ok($original_transfer->datecancelled, "Original transfer cancelled");
549     $transfers = $item->get_transfers;
550     is($transfers->count, 1, "There is only 1 live transfer in the queue");
551     $replaced_transfer->datearrived(dt_from_string)->store();
552
553     # BranchTransferLimits
554     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
555     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
556     my $limit = Koha::Item::Transfer::Limit->new({
557         fromBranch => $library2->branchcode,
558         toBranch => $library1->branchcode,
559         itemtype => $item->effective_itemtype,
560     })->store;
561
562     throws_ok { $item->request_transfer( { to => $library1, reason => 'Manual' } ) }
563     'Koha::Exceptions::Item::Transfer::Limit',
564       'Exception thrown if transfer is prevented by limits';
565
566     my $forced_transfer = $item->request_transfer( { to => $library1, reason => 'Manual', ignore_limits => 1 } );
567     is( ref($forced_transfer), 'Koha::Item::Transfer',
568         'Koha::Item->request_transfer allowed when ignore_limits is set'
569     );
570
571     $schema->storage->txn_rollback;
572 };
573
574 subtest 'deletion' => sub {
575     plan tests => 13;
576
577     $schema->storage->txn_begin;
578
579     my $biblio = $builder->build_sample_biblio();
580
581     my $item = $builder->build_sample_item(
582         {
583             biblionumber => $biblio->biblionumber,
584         }
585     );
586
587     is( ref( $item->move_to_deleted ), 'Koha::Schema::Result::Deleteditem', 'Koha::Item->move_to_deleted should return the Deleted item' )
588       ;    # FIXME This should be Koha::Deleted::Item
589     is( Koha::Old::Items->search({itemnumber => $item->itemnumber})->count, 1, '->move_to_deleted must have moved the item to deleteditem' );
590     $item = $builder->build_sample_item(
591         {
592             biblionumber => $biblio->biblionumber,
593         }
594     );
595     $item->delete;
596     is( Koha::Old::Items->search({itemnumber => $item->itemnumber})->count, 0, '->move_to_deleted must not have moved the item to deleteditem' );
597
598
599     my $library   = $builder->build_object({ class => 'Koha::Libraries' });
600     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
601     t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
602
603     my $patron = $builder->build_object({class => 'Koha::Patrons'});
604     $item = $builder->build_sample_item({ library => $library->branchcode });
605
606     # book_on_loan
607     C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
608
609     is(
610         @{$item->safe_to_delete->messages}[0]->message,
611         'book_on_loan',
612         'Koha::Item->safe_to_delete reports item on loan',
613     );
614
615     is(
616         @{$item->safe_to_delete->messages}[0]->message,
617         'book_on_loan',
618         'item that is on loan cannot be deleted',
619     );
620
621     ok(
622         ! $item->safe_to_delete,
623         'Koha::Item->safe_to_delete shows item NOT safe to delete'
624     );
625
626     AddReturn( $item->barcode, $library->branchcode );
627
628     # book_reserved is tested in t/db_dependent/Reserves.t
629
630     # not_same_branch
631     t::lib::Mocks::mock_preference('IndependentBranches', 1);
632     my $item_2 = $builder->build_sample_item({ library => $library_2->branchcode });
633
634     is(
635         @{$item_2->safe_to_delete->messages}[0]->message,
636         'not_same_branch',
637         'Koha::Item->safe_to_delete reports IndependentBranches restriction',
638     );
639
640     is(
641         @{$item_2->safe_to_delete->messages}[0]->message,
642         'not_same_branch',
643         'IndependentBranches prevents deletion at another branch',
644     );
645
646     # linked_analytics
647
648     { # codeblock to limit scope of $module->mock
649
650         my $module = Test::MockModule->new('C4::Items');
651         $module->mock( GetAnalyticsCount => sub { return 1 } );
652
653         $item->discard_changes;
654         is(
655             @{$item->safe_to_delete->messages}[0]->message,
656             'linked_analytics',
657             'Koha::Item->safe_to_delete reports linked analytics',
658         );
659
660         is(
661             @{$item->safe_to_delete->messages}[0]->message,
662             'linked_analytics',
663             'Linked analytics prevents deletion of item',
664         );
665
666     }
667
668     { # last_item_for_hold
669         C4::Reserves::AddReserve({ branchcode => $patron->branchcode, borrowernumber => $patron->borrowernumber, biblionumber => $item->biblionumber });
670         is(
671             @{$item->safe_to_delete->messages}[0]->message,
672             'last_item_for_hold',
673             'Item cannot be deleted if a biblio-level is placed on the biblio and there is only 1 item attached to the biblio'
674         );
675         # With another item attached to the biblio, the item can be deleted
676         $builder->build_sample_item({ biblionumber => $item->biblionumber });
677     }
678
679     ok(
680         $item->safe_to_delete,
681         'Koha::Item->safe_to_delete shows item safe to delete'
682     );
683
684     $item->safe_delete,
685
686     my $test_item = Koha::Items->find( $item->itemnumber );
687
688     is( $test_item, undef,
689         "Koha::Item->safe_delete should delete item if safe_to_delete returns true"
690     );
691
692     $schema->storage->txn_rollback;
693 };
694
695 subtest 'renewal_branchcode' => sub {
696     plan tests => 13;
697
698     $schema->storage->txn_begin;
699
700     my $item = $builder->build_sample_item();
701     my $branch = $builder->build_object({ class => 'Koha::Libraries' });
702     my $checkout = $builder->build_object({
703         class => 'Koha::Checkouts',
704         value => {
705             itemnumber => $item->itemnumber,
706         }
707     });
708
709
710     C4::Context->interface( 'intranet' );
711     t::lib::Mocks::mock_userenv({ branchcode => $branch->branchcode });
712
713     is( $item->renewal_branchcode, $branch->branchcode, "If interface not opac, we get the branch from context");
714     is( $item->renewal_branchcode({ branch => "PANDA"}), $branch->branchcode, "If interface not opac, we get the branch from context even if we pass one in");
715     C4::Context->set_userenv(51, 'userid4tests', undef, 'firstname', 'surname', undef, undef, 0, undef, undef, undef ); #mock userenv doesn't let us set null branch
716     is( $item->renewal_branchcode({ branch => "PANDA"}), "PANDA", "If interface not opac, we get the branch we pass one in if context not set");
717
718     C4::Context->interface( 'opac' );
719
720     t::lib::Mocks::mock_preference('OpacRenewalBranch', undef);
721     is( $item->renewal_branchcode, 'OPACRenew', "If interface opac and OpacRenewalBranch undef, we get OPACRenew");
722     is( $item->renewal_branchcode({branch=>'COW'}), 'OPACRenew', "If interface opac and OpacRenewalBranch undef, we get OPACRenew even if branch passed");
723
724     t::lib::Mocks::mock_preference('OpacRenewalBranch', 'none');
725     is( $item->renewal_branchcode, '', "If interface opac and OpacRenewalBranch is none, we get blank string");
726     is( $item->renewal_branchcode({branch=>'COW'}), '', "If interface opac and OpacRenewalBranch is none, we get blank string even if branch passed");
727
728     t::lib::Mocks::mock_preference('OpacRenewalBranch', 'checkoutbranch');
729     is( $item->renewal_branchcode, $checkout->branchcode, "If interface opac and OpacRenewalBranch set to checkoutbranch, we get branch of checkout");
730     is( $item->renewal_branchcode({branch=>'MONKEY'}), $checkout->branchcode, "If interface opac and OpacRenewalBranch set to checkoutbranch, we get branch of checkout even if branch passed");
731
732     t::lib::Mocks::mock_preference('OpacRenewalBranch','patronhomebranch');
733     is( $item->renewal_branchcode, $checkout->patron->branchcode, "If interface opac and OpacRenewalBranch set to patronbranch, we get branch of patron");
734     is( $item->renewal_branchcode({branch=>'TURKEY'}), $checkout->patron->branchcode, "If interface opac and OpacRenewalBranch set to patronbranch, we get branch of patron even if branch passed");
735
736     t::lib::Mocks::mock_preference('OpacRenewalBranch','itemhomebranch');
737     is( $item->renewal_branchcode, $item->homebranch, "If interface opac and OpacRenewalBranch set to itemhomebranch, we get homebranch of item");
738     is( $item->renewal_branchcode({branch=>'MANATEE'}), $item->homebranch, "If interface opac and OpacRenewalBranch set to itemhomebranch, we get homebranch of item even if branch passed");
739
740     $schema->storage->txn_rollback;
741 };
742
743 subtest 'Tests for itemtype' => sub {
744     plan tests => 2;
745     $schema->storage->txn_begin;
746
747     my $biblio = $builder->build_sample_biblio;
748     my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' });
749     my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber, itype => $itemtype->itemtype });
750
751     t::lib::Mocks::mock_preference('item-level_itypes', 1);
752     is( $item->itemtype->itemtype, $item->itype, 'Pref enabled' );
753     t::lib::Mocks::mock_preference('item-level_itypes', 0);
754     is( $item->itemtype->itemtype, $biblio->biblioitem->itemtype, 'Pref disabled' );
755
756     $schema->storage->txn_rollback;
757 };
758
759 subtest 'get_transfers' => sub {
760     plan tests => 16;
761     $schema->storage->txn_begin;
762
763     my $item = $builder->build_sample_item();
764
765     my $transfers = $item->get_transfers();
766     is(ref($transfers), 'Koha::Item::Transfers', 'Koha::Item->get_transfer should return a Koha::Item::Transfers object' );
767     is($transfers->count, 0, 'When no transfers exist, the Koha::Item:Transfers object should be empty');
768
769     my $library_to = $builder->build_object( { class => 'Koha::Libraries' } );
770
771     my $transfer_1 = $builder->build_object(
772         {
773             class => 'Koha::Item::Transfers',
774             value => {
775                 itemnumber    => $item->itemnumber,
776                 frombranch    => $item->holdingbranch,
777                 tobranch      => $library_to->branchcode,
778                 reason        => 'Manual',
779                 datesent      => undef,
780                 datearrived   => undef,
781                 datecancelled => undef,
782                 daterequested => \'NOW()'
783             }
784         }
785     );
786
787     $transfers = $item->get_transfers();
788     is($transfers->count, 1, 'When one transfer has been requested, the Koha::Item:Transfers object should contain one result');
789
790     my $transfer_2 = $builder->build_object(
791         {
792             class => 'Koha::Item::Transfers',
793             value => {
794                 itemnumber    => $item->itemnumber,
795                 frombranch    => $item->holdingbranch,
796                 tobranch      => $library_to->branchcode,
797                 reason        => 'Manual',
798                 datesent      => undef,
799                 datearrived   => undef,
800                 datecancelled => undef,
801                 daterequested => \'NOW()'
802             }
803         }
804     );
805
806     my $transfer_3 = $builder->build_object(
807         {
808             class => 'Koha::Item::Transfers',
809             value => {
810                 itemnumber    => $item->itemnumber,
811                 frombranch    => $item->holdingbranch,
812                 tobranch      => $library_to->branchcode,
813                 reason        => 'Manual',
814                 datesent      => undef,
815                 datearrived   => undef,
816                 datecancelled => undef,
817                 daterequested => \'NOW()'
818             }
819         }
820     );
821
822     $transfers = $item->get_transfers();
823     is($transfers->count, 3, 'When there are multiple open transfer requests, the Koha::Item::Transfers object contains them all');
824     my $result_1 = $transfers->next;
825     my $result_2 = $transfers->next;
826     my $result_3 = $transfers->next;
827     is( $result_1->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfers returns the oldest transfer request first');
828     is( $result_2->branchtransfer_id, $transfer_2->branchtransfer_id, 'Koha::Item->get_transfers returns the newer transfer request second');
829     is( $result_3->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfers returns the newest transfer request last');
830
831     $transfer_2->datesent(\'NOW()')->store;
832     $transfers = $item->get_transfers();
833     is($transfers->count, 3, 'When one transfer is set to in_transit, the Koha::Item::Transfers object still contains them all');
834     $result_1 = $transfers->next;
835     $result_2 = $transfers->next;
836     $result_3 = $transfers->next;
837     is( $result_1->branchtransfer_id, $transfer_2->branchtransfer_id, 'Koha::Item->get_transfers returns the active transfer request first');
838     is( $result_2->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfers returns the other transfers oldest to newest');
839     is( $result_3->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfers returns the other transfers oldest to newest');
840
841     $transfer_2->datearrived(\'NOW()')->store;
842     $transfers = $item->get_transfers();
843     is($transfers->count, 2, 'Once a transfer is received, it no longer appears in the list from ->get_transfers()');
844     $result_1 = $transfers->next;
845     $result_2 = $transfers->next;
846     is( $result_1->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfers returns the other transfers oldest to newest');
847     is( $result_2->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfers returns the other transfers oldest to newest');
848
849     $transfer_1->datecancelled(\'NOW()')->store;
850     $transfers = $item->get_transfers();
851     is($transfers->count, 1, 'Once a transfer is cancelled, it no longer appears in the list from ->get_transfers()');
852     $result_1 = $transfers->next;
853     is( $result_1->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfers returns the only transfer that remains');
854
855     $schema->storage->txn_rollback;
856 };
857
858 subtest 'Tests for relationship between item and item_orders via aqorders_item' => sub {
859     plan tests => 3;
860
861     $schema->storage->txn_begin;
862
863     my $biblio = $builder->build_sample_biblio();
864     my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
865
866     my $orders = $item->orders;
867     is ($orders->count, 0, 'No order on this item yet');
868
869     my $order_note = 'Order for ' . $item->itemnumber;
870
871     my $aq_order1 = $builder->build_object({
872         class => 'Koha::Acquisition::Orders',
873         value  => {
874             biblionumber => $biblio->biblionumber,
875             order_internalnote => $order_note,
876         },
877     });
878     my $aq_order2 = $builder->build_object({
879         class => 'Koha::Acquisition::Orders',
880         value  => {
881             biblionumber => $biblio->biblionumber,
882         },
883     });
884     my $aq_order_item1 = $builder->build({
885         source => 'AqordersItem',
886         value  => {
887             ordernumber => $aq_order1->ordernumber,
888             itemnumber => $item->itemnumber,
889         },
890     });
891
892     $orders = $item->orders;
893     is ($orders->count, 1, 'One order found by item with the relationship');
894     is ($orders->next->order_internalnote, $order_note, 'Correct order found by item with the relationship');
895 };
896
897 subtest 'move_to_biblio() tests' => sub {
898     plan tests => 16;
899
900     $schema->storage->txn_begin;
901
902     my $dbh = C4::Context->dbh;
903
904     my $source_biblio = $builder->build_sample_biblio();
905     my $target_biblio = $builder->build_sample_biblio();
906
907     my $source_biblionumber = $source_biblio->biblionumber;
908     my $target_biblionumber = $target_biblio->biblionumber;
909
910     my $item1 = $builder->build_sample_item({ biblionumber => $source_biblionumber });
911     my $item2 = $builder->build_sample_item({ biblionumber => $source_biblionumber });
912     my $item3 = $builder->build_sample_item({ biblionumber => $source_biblionumber });
913
914     my $itemnumber1 = $item1->itemnumber;
915     my $itemnumber2 = $item2->itemnumber;
916
917     my $library = $builder->build_object({ class => 'Koha::Libraries' });
918
919     my $patron = $builder->build_object({
920         class => 'Koha::Patrons',
921         value => { branchcode => $library->branchcode }
922     });
923     my $borrowernumber = $patron->borrowernumber;
924
925     my $aq_budget = $builder->build({
926         source => 'Aqbudget',
927         value  => {
928             budget_notes => 'test',
929         },
930     });
931
932     my $aq_order1 = $builder->build_object({
933         class => 'Koha::Acquisition::Orders',
934         value  => {
935             biblionumber => $source_biblionumber,
936             budget_id => $aq_budget->{budget_id},
937         },
938     });
939     my $aq_order_item1 = $builder->build({
940         source => 'AqordersItem',
941         value  => {
942             ordernumber => $aq_order1->ordernumber,
943             itemnumber => $itemnumber1,
944         },
945     });
946     my $aq_order2 = $builder->build_object({
947         class => 'Koha::Acquisition::Orders',
948         value  => {
949             biblionumber => $source_biblionumber,
950             budget_id => $aq_budget->{budget_id},
951         },
952     });
953     my $aq_order_item2 = $builder->build({
954         source => 'AqordersItem',
955         value  => {
956             ordernumber => $aq_order2->ordernumber,
957             itemnumber => $itemnumber2,
958         },
959     });
960
961     my $bib_level_hold = $builder->build_object({
962         class => 'Koha::Holds',
963         value  => {
964             biblionumber => $source_biblionumber,
965             itemnumber => undef,
966         },
967     });
968     my $item_level_hold1 = $builder->build_object({
969         class => 'Koha::Holds',
970         value  => {
971             biblionumber => $source_biblionumber,
972             itemnumber => $itemnumber1,
973         },
974     });
975     my $item_level_hold2 = $builder->build_object({
976         class => 'Koha::Holds',
977         value  => {
978             biblionumber => $source_biblionumber,
979             itemnumber => $itemnumber2,
980         }
981     });
982
983     my $tmp_holdsqueue1 = $builder->build({
984         source => 'TmpHoldsqueue',
985         value  => {
986             borrowernumber => $borrowernumber,
987             biblionumber   => $source_biblionumber,
988             itemnumber     => $itemnumber1,
989         }
990     });
991     my $tmp_holdsqueue2 = $builder->build({
992         source => 'TmpHoldsqueue',
993         value  => {
994             borrowernumber => $borrowernumber,
995             biblionumber   => $source_biblionumber,
996             itemnumber     => $itemnumber2,
997         }
998     });
999     my $hold_fill_target1 = $builder->build({
1000         source => 'HoldFillTarget',
1001         value  => {
1002             borrowernumber     => $borrowernumber,
1003             biblionumber       => $source_biblionumber,
1004             itemnumber         => $itemnumber1,
1005         }
1006     });
1007     my $hold_fill_target2 = $builder->build({
1008         source => 'HoldFillTarget',
1009         value  => {
1010             borrowernumber     => $borrowernumber,
1011             biblionumber       => $source_biblionumber,
1012             itemnumber         => $itemnumber2,
1013         }
1014     });
1015     my $linktracker1 = $builder->build({
1016         source => 'Linktracker',
1017         value  => {
1018             borrowernumber     => $borrowernumber,
1019             biblionumber       => $source_biblionumber,
1020             itemnumber         => $itemnumber1,
1021         }
1022     });
1023     my $linktracker2 = $builder->build({
1024         source => 'Linktracker',
1025         value  => {
1026             borrowernumber     => $borrowernumber,
1027             biblionumber       => $source_biblionumber,
1028             itemnumber         => $itemnumber2,
1029         }
1030     });
1031
1032     my $to_biblionumber_after_move = $item1->move_to_biblio($target_biblio);
1033     is($to_biblionumber_after_move, $target_biblionumber, 'move_to_biblio returns the target biblionumber if success');
1034
1035     $to_biblionumber_after_move = $item1->move_to_biblio($target_biblio);
1036     is($to_biblionumber_after_move, undef, 'move_to_biblio returns undef if the move has failed. If called twice, the item is not attached to the first biblio anymore');
1037
1038     my $get_item1 = Koha::Items->find( $item1->itemnumber );
1039     is($get_item1->biblionumber, $target_biblionumber, 'item1 is moved');
1040     my $get_item2 = Koha::Items->find( $item2->itemnumber );
1041     is($get_item2->biblionumber, $source_biblionumber, 'item2 is not moved');
1042     my $get_item3 = Koha::Items->find( $item3->itemnumber );
1043     is($get_item3->biblionumber, $source_biblionumber, 'item3 is not moved');
1044
1045     $aq_order1->discard_changes;
1046     $aq_order2->discard_changes;
1047     is($aq_order1->biblionumber, $target_biblionumber, 'move_to_biblio moves aq_orders for item 1');
1048     is($aq_order2->biblionumber, $source_biblionumber, 'move_to_biblio does not move aq_orders for item 2');
1049
1050     $bib_level_hold->discard_changes;
1051     $item_level_hold1->discard_changes;
1052     $item_level_hold2->discard_changes;
1053     is($bib_level_hold->biblionumber,   $source_biblionumber, 'move_to_biblio does not move the biblio-level hold');
1054     is($item_level_hold1->biblionumber, $target_biblionumber, 'move_to_biblio moves the item-level hold placed on item 1');
1055     is($item_level_hold2->biblionumber, $source_biblionumber, 'move_to_biblio does not move the item-level hold placed on item 2');
1056
1057     my $get_tmp_holdsqueue1 = $schema->resultset('TmpHoldsqueue')->search({ itemnumber => $tmp_holdsqueue1->{itemnumber} })->single;
1058     my $get_tmp_holdsqueue2 = $schema->resultset('TmpHoldsqueue')->search({ itemnumber => $tmp_holdsqueue2->{itemnumber} })->single;
1059     is($get_tmp_holdsqueue1->biblionumber->biblionumber, $target_biblionumber, 'move_to_biblio moves tmp_holdsqueue for item 1');
1060     is($get_tmp_holdsqueue2->biblionumber->biblionumber, $source_biblionumber, 'move_to_biblio does not move tmp_holdsqueue for item 2');
1061
1062     my $get_hold_fill_target1 = $schema->resultset('HoldFillTarget')->search({ itemnumber => $hold_fill_target1->{itemnumber} })->single;
1063     my $get_hold_fill_target2 = $schema->resultset('HoldFillTarget')->search({ itemnumber => $hold_fill_target2->{itemnumber} })->single;
1064     # Why does ->biblionumber return a Biblio object???
1065     is($get_hold_fill_target1->biblionumber->biblionumber, $target_biblionumber, 'move_to_biblio moves hold_fill_targets for item 1');
1066     is($get_hold_fill_target2->biblionumber->biblionumber, $source_biblionumber, 'move_to_biblio does not move hold_fill_targets for item 2');
1067
1068     my $get_linktracker1 = $schema->resultset('Linktracker')->search({ itemnumber => $linktracker1->{itemnumber} })->single;
1069     my $get_linktracker2 = $schema->resultset('Linktracker')->search({ itemnumber => $linktracker2->{itemnumber} })->single;
1070     is($get_linktracker1->biblionumber->biblionumber, $target_biblionumber, 'move_to_biblio moves linktracker for item 1');
1071     is($get_linktracker2->biblionumber->biblionumber, $source_biblionumber, 'move_to_biblio does not move linktracker for item 2');
1072
1073     $schema->storage->txn_rollback;
1074 };
1075
1076 subtest 'columns_to_str' => sub {
1077     plan tests => 4;
1078
1079     $schema->storage->txn_begin;
1080
1081     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1082
1083     my $cache = Koha::Caches->get_instance();
1084     $cache->clear_from_cache("MarcStructure-0-");
1085     $cache->clear_from_cache("MarcStructure-1-");
1086     $cache->clear_from_cache("default_value_for_mod_marc-");
1087     $cache->clear_from_cache("MarcSubfieldStructure-");
1088
1089     # Creating subfields 'é', 'è' that are not linked with a kohafield
1090     Koha::MarcSubfieldStructures->search(
1091         {
1092             frameworkcode => '',
1093             tagfield => $itemtag,
1094             tagsubfield => ['é', 'è'],
1095         }
1096     )->delete;    # In case it exist already
1097
1098     # Ã© is not linked with a AV
1099     # Ã¨ is linked with AV branches
1100     Koha::MarcSubfieldStructure->new(
1101         {
1102             frameworkcode => '',
1103             tagfield      => $itemtag,
1104             tagsubfield   => 'é',
1105             kohafield     => undef,
1106             repeatable    => 1,
1107             defaultvalue  => 'ééé',
1108             tab           => 10,
1109         }
1110     )->store;
1111     Koha::MarcSubfieldStructure->new(
1112         {
1113             frameworkcode    => '',
1114             tagfield         => $itemtag,
1115             tagsubfield      => 'è',
1116             kohafield        => undef,
1117             repeatable       => 1,
1118             defaultvalue     => 'èèè',
1119             tab              => 10,
1120             authorised_value => 'branches',
1121         }
1122     )->store;
1123
1124     my $biblio = $builder->build_sample_biblio({ frameworkcode => '' });
1125     my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1126     my $lost_av = $builder->build_object({ class => 'Koha::AuthorisedValues', value => { category => 'LOST', authorised_value => '42' }});
1127     my $dateaccessioned = '2020-12-15';
1128     my $library = Koha::Libraries->search->next;
1129     my $branchcode = $library->branchcode;
1130
1131     my $some_marc_xml = qq{<?xml version="1.0" encoding="UTF-8"?>
1132 <collection
1133   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1134   xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1135   xmlns="http://www.loc.gov/MARC21/slim">
1136
1137 <record>
1138   <leader>         a              </leader>
1139   <datafield tag="999" ind1=" " ind2=" ">
1140     <subfield code="é">value Ã©</subfield>
1141     <subfield code="è">$branchcode</subfield>
1142   </datafield>
1143 </record>
1144
1145 </collection>};
1146
1147     $item->update(
1148         {
1149             itemlost           => $lost_av->authorised_value,
1150             dateaccessioned    => $dateaccessioned,
1151             more_subfields_xml => $some_marc_xml,
1152         }
1153     );
1154
1155     $item = $item->get_from_storage;
1156
1157     my $s = $item->columns_to_str;
1158     is( $s->{itemlost}, $lost_av->lib, 'Attributes linked with AV replaced with description' );
1159     is( $s->{dateaccessioned}, '2020-12-15', 'Date attributes iso formatted');
1160     is( $s->{'é'}, 'value Ã©', 'subfield ok with more than a-Z');
1161     is( $s->{'è'}, $library->branchname );
1162
1163     $cache->clear_from_cache("MarcStructure-0-");
1164     $cache->clear_from_cache("MarcStructure-1-");
1165     $cache->clear_from_cache("default_value_for_mod_marc-");
1166     $cache->clear_from_cache("MarcSubfieldStructure-");
1167
1168     $schema->storage->txn_rollback;
1169
1170 };
1171
1172 subtest 'store() tests' => sub {
1173
1174     plan tests => 2;
1175
1176     subtest '_set_found_trigger() tests' => sub {
1177
1178         plan tests => 6;
1179
1180         $schema->storage->txn_begin;
1181
1182         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1183         my $item   = $builder->build_sample_item({ itemlost => 1, itemlost_on => dt_from_string() });
1184
1185         # Add a lost item debit
1186         my $debit = $patron->account->add_debit(
1187             {
1188                 amount    => 10,
1189                 type      => 'LOST',
1190                 item_id   => $item->id,
1191                 interface => 'intranet',
1192             }
1193         );
1194
1195         my $lostreturn_policy = 'charge';
1196
1197         my $mocked_circ_rules = Test::MockModule->new('Koha::CirculationRules');
1198         $mocked_circ_rules->mock( 'get_lostreturn_policy', sub { return $lostreturn_policy; } );
1199
1200         # simulate it was found
1201         $item->set( { itemlost => 0 } )->store;
1202
1203         my $messages = $item->object_messages;
1204
1205         my $message_1 = $messages->[0];
1206
1207         is( $message_1->type,    'info',          'type is correct' );
1208         is( $message_1->message, 'lost_refunded', 'message is correct' );
1209
1210         # Find the refund credit
1211         my $credit = $debit->credits->next;
1212
1213         is_deeply(
1214             $message_1->payload,
1215             { credit_id => $credit->id },
1216             'type is correct'
1217         );
1218
1219         my $message_2 = $messages->[1];
1220
1221         is( $message_2->type,    'info',        'type is correct' );
1222         is( $message_2->message, 'lost_charge', 'message is correct' );
1223         is( $message_2->payload, undef,         'no payload' );
1224
1225         $schema->storage->txn_rollback;
1226     };
1227
1228     subtest 'holds_queue update tests' => sub {
1229
1230         plan tests => 2;
1231
1232         $schema->storage->txn_begin;
1233
1234         my $biblio = $builder->build_sample_biblio;
1235
1236         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
1237         $mock->mock( 'enqueue', sub {
1238             my ( $self, $args ) = @_;
1239             is_deeply(
1240                 $args->{biblio_ids},
1241                 [ $biblio->id ],
1242                 '->store triggers a holds queue update for the related biblio'
1243             );
1244         } );
1245
1246         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
1247
1248         # new item
1249         my $item = $builder->build_sample_item({ biblionumber => $biblio->id });
1250
1251         # updated item
1252         $item->set({ reserves => 1 })->store;
1253
1254         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
1255         # updated item
1256         $item->set({ reserves => 0 })->store;
1257
1258         $schema->storage->txn_rollback;
1259     };
1260 };
1261
1262 subtest 'Recalls tests' => sub {
1263
1264     plan tests => 20;
1265
1266     $schema->storage->txn_begin;
1267
1268     my $item1 = $builder->build_sample_item;
1269     my $biblio = $item1->biblio;
1270     my $branchcode = $item1->holdingbranch;
1271     my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
1272     my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
1273     my $patron3 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
1274     my $item2 = $builder->build_object(
1275         {   class => 'Koha::Items',
1276             value => { holdingbranch => $branchcode, homebranch => $branchcode, biblionumber => $biblio->biblionumber, itype => $item1->effective_itemtype }
1277         }
1278     );
1279
1280     t::lib::Mocks::mock_userenv( { patron => $patron1 } );
1281     t::lib::Mocks::mock_preference('UseRecalls', 1);
1282
1283     my $recall1 = Koha::Recall->new(
1284         {   patron_id         => $patron1->borrowernumber,
1285             created_date      => \'NOW()',
1286             biblio_id         => $biblio->biblionumber,
1287             pickup_library_id => $branchcode,
1288             item_id           => $item1->itemnumber,
1289             expiration_date   => undef,
1290             item_level        => 1
1291         }
1292     )->store;
1293     my $recall2 = Koha::Recall->new(
1294         {   patron_id         => $patron2->borrowernumber,
1295             created_date      => \'NOW()',
1296             biblio_id         => $biblio->biblionumber,
1297             pickup_library_id => $branchcode,
1298             item_id           => $item1->itemnumber,
1299             expiration_date   => undef,
1300             item_level        => 1
1301         }
1302     )->store;
1303
1304     is( $item1->recall->patron_id, $patron1->borrowernumber, 'Correctly returns most relevant recall' );
1305
1306     $recall2->set_cancelled;
1307
1308     t::lib::Mocks::mock_preference('UseRecalls', 0);
1309     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall with UseRecalls disabled" );
1310
1311     t::lib::Mocks::mock_preference("UseRecalls", 1);
1312
1313     $item1->update({ notforloan => 1 });
1314     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is not for loan" );
1315     $item1->update({ notforloan => 0, itemlost => 1 });
1316     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is marked lost" );
1317     $item1->update({ itemlost => 0, withdrawn => 1 });
1318     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is withdrawn" );
1319     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall item if not checked out" );
1320
1321     $item1->update({ withdrawn => 0 });
1322     C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
1323
1324     Koha::CirculationRules->set_rules({
1325         branchcode => $branchcode,
1326         categorycode => $patron1->categorycode,
1327         itemtype => $item1->effective_itemtype,
1328         rules => {
1329             recalls_allowed => 0,
1330             recalls_per_record => 1,
1331             on_shelf_recalls => 'all',
1332         },
1333     });
1334     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if recalls_allowed = 0" );
1335
1336     Koha::CirculationRules->set_rules({
1337         branchcode => $branchcode,
1338         categorycode => $patron1->categorycode,
1339         itemtype => $item1->effective_itemtype,
1340         rules => {
1341             recalls_allowed => 1,
1342             recalls_per_record => 1,
1343             on_shelf_recalls => 'all',
1344         },
1345     });
1346     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_allowed" );
1347     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_per_record" );
1348     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has already recalled this item" );
1349
1350     my $reserve_id = C4::Reserves::AddReserve({ branchcode => $branchcode, borrowernumber => $patron1->borrowernumber, biblionumber => $item1->biblionumber, itemnumber => $item1->itemnumber });
1351     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall item if patron has already reserved it" );
1352     C4::Reserves::ModReserve({ rank => 'del', reserve_id => $reserve_id, branchcode => $branchcode, itemnumber => $item1->itemnumber, borrowernumber => $patron1->borrowernumber, biblionumber => $item1->biblionumber });
1353
1354     $recall1->set_cancelled;
1355     is( $item1->can_be_recalled({ patron => $patron2 }), 0, "Can't recall if patron has already checked out an item attached to this biblio" );
1356
1357     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if on_shelf_recalls = all and items are still available" );
1358
1359     Koha::CirculationRules->set_rules({
1360         branchcode => $branchcode,
1361         categorycode => $patron1->categorycode,
1362         itemtype => $item1->effective_itemtype,
1363         rules => {
1364             recalls_allowed => 1,
1365             recalls_per_record => 1,
1366             on_shelf_recalls => 'any',
1367         },
1368     });
1369     C4::Circulation::AddReturn( $item1->barcode, $branchcode );
1370     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if no items are checked out" );
1371
1372     C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
1373     is( $item1->can_be_recalled({ patron => $patron1 }), 1, "Can recall item" );
1374
1375     $recall1 = Koha::Recall->new(
1376         {   patron_id         => $patron1->borrowernumber,
1377             created_date      => \'NOW()',
1378             biblio_id         => $biblio->biblionumber,
1379             pickup_library_id => $branchcode,
1380             item_id           => undef,
1381             expiration_date   => undef,
1382             item_level        => 0
1383         }
1384     )->store;
1385
1386     # Patron2 has Item1 checked out. Patron1 has placed a biblio-level recall on Biblio1, so check if Item1 can fulfill Patron1's recall.
1387
1388     Koha::CirculationRules->set_rules({
1389         branchcode => undef,
1390         categorycode => undef,
1391         itemtype => $item1->effective_itemtype,
1392         rules => {
1393             recalls_allowed => 0,
1394             recalls_per_record => 1,
1395             on_shelf_recalls => 'any',
1396         },
1397     });
1398     is( $item1->can_be_waiting_recall, 0, "Recalls not allowed for this itemtype" );
1399
1400     Koha::CirculationRules->set_rules({
1401         branchcode => undef,
1402         categorycode => undef,
1403         itemtype => $item1->effective_itemtype,
1404         rules => {
1405             recalls_allowed => 1,
1406             recalls_per_record => 1,
1407             on_shelf_recalls => 'any',
1408         },
1409     });
1410     is( $item1->can_be_waiting_recall, 1, "Recalls are allowed for this itemtype" );
1411
1412     # check_recalls tests
1413
1414     $recall1 = Koha::Recall->new(
1415         {   patron_id         => $patron2->borrowernumber,
1416             created_date      => \'NOW()',
1417             biblio_id         => $biblio->biblionumber,
1418             pickup_library_id => $branchcode,
1419             item_id           => $item1->itemnumber,
1420             expiration_date   => undef,
1421             item_level        => 1
1422         }
1423     )->store;
1424     $recall2 = Koha::Recall->new(
1425         {   patron_id         => $patron1->borrowernumber,
1426             created_date      => \'NOW()',
1427             biblio_id         => $biblio->biblionumber,
1428             pickup_library_id => $branchcode,
1429             item_id           => undef,
1430             expiration_date   => undef,
1431             item_level        => 0
1432         }
1433     )->store;
1434     $recall2->set_waiting( { item => $item1 } );
1435
1436     # return a waiting recall
1437     my $check_recall = $item1->check_recalls;
1438     is( $check_recall->patron_id, $patron1->borrowernumber, "Waiting recall is highest priority and returned" );
1439
1440     $recall2->revert_waiting;
1441
1442     # return recall based on recalldate
1443     $check_recall = $item1->check_recalls;
1444     is( $check_recall->patron_id, $patron1->borrowernumber, "No waiting recall, so oldest recall is returned" );
1445
1446     $recall1->set_cancelled;
1447
1448     # return a biblio-level recall
1449     $check_recall = $item1->check_recalls;
1450     is( $check_recall->patron_id, $patron1->borrowernumber, "Only remaining recall is returned" );
1451
1452     $recall2->set_cancelled;
1453
1454     $schema->storage->txn_rollback;
1455 };
1456