Bug 35248: Add unit tests for Koha::Item->check_booking
[koha.git] / 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 => 34;
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 use Koha::Recalls;
36 use Koha::AuthorisedValues;
37
38 use List::MoreUtils qw(all);
39
40 use t::lib::TestBuilder;
41 use t::lib::Mocks;
42 use t::lib::Dates;
43
44 my $schema  = Koha::Database->new->schema;
45 my $builder = t::lib::TestBuilder->new;
46
47 subtest 'return_claims relationship' => sub {
48     plan tests => 3;
49
50     $schema->storage->txn_begin;
51
52     my $biblio = $builder->build_sample_biblio();
53     my $item   = $builder->build_sample_item({
54         biblionumber => $biblio->biblionumber,
55     });
56     my $return_claims = $item->return_claims;
57     is( ref($return_claims), 'Koha::Checkouts::ReturnClaims', 'return_claims returns a Koha::Checkouts::ReturnClaims object set' );
58     is($item->return_claims->count, 0, "Empty Koha::Checkouts::ReturnClaims set returned if no return_claims");
59     my $claim1 = $builder->build({ source => 'ReturnClaim', value => { itemnumber => $item->itemnumber }});
60     my $claim2 = $builder->build({ source => 'ReturnClaim', value => { itemnumber => $item->itemnumber }});
61
62     is($item->return_claims()->count,2,"Two ReturnClaims found for item");
63
64     $schema->storage->txn_rollback;
65 };
66
67 subtest 'return_claim accessor' => sub {
68     plan tests => 5;
69
70     $schema->storage->txn_begin;
71
72     my $biblio = $builder->build_sample_biblio();
73     my $item   = $builder->build_sample_item({
74         biblionumber => $biblio->biblionumber,
75     });
76     my $return_claim = $item->return_claim;
77     is( $return_claim, undef, 'return_claim returned undefined if there are no claims for this item' );
78
79     my $claim1 = $builder->build_object(
80         {
81             class => 'Koha::Checkouts::ReturnClaims',
82             value => { itemnumber => $item->itemnumber, resolution => undef, created_on => dt_from_string()->subtract( minutes => 10 ) }
83         }
84     );
85     my $claim2 = $builder->build_object(
86         {
87             class => 'Koha::Checkouts::ReturnClaims',
88             value  => { itemnumber => $item->itemnumber, resolution => undef, created_on => dt_from_string()->subtract( minutes => 5 ) }
89         }
90     );
91
92     $return_claim = $item->return_claim;
93     is( ref($return_claim), 'Koha::Checkouts::ReturnClaim', 'return_claim returned a Koha::Checkouts::ReturnClaim object' );
94     is( $return_claim->id, $claim2->id, 'return_claim returns the most recent unresolved claim');
95
96     $claim2->resolution('test')->store();
97     $return_claim = $item->return_claim;
98     is( $return_claim->id, $claim1->id, 'return_claim returns the only unresolved claim');
99
100     $claim1->resolution('test')->store();
101     $return_claim = $item->return_claim;
102     is( $return_claim, undef, 'return_claim returned undefined if there are no active claims for this item' );
103
104     $schema->storage->txn_rollback;
105 };
106
107 subtest 'tracked_links relationship' => sub {
108     plan tests => 3;
109
110     my $biblio = $builder->build_sample_biblio();
111     my $item   = $builder->build_sample_item({
112         biblionumber => $biblio->biblionumber,
113     });
114     my $tracked_links = $item->tracked_links;
115     is( ref($tracked_links), 'Koha::TrackedLinks', 'tracked_links returns a Koha::TrackedLinks object set' );
116     is($item->tracked_links->count, 0, "Empty Koha::TrackedLinks set returned if no tracked_links");
117     my $link1 = $builder->build({ source => 'Linktracker', value => { itemnumber => $item->itemnumber }});
118     my $link2 = $builder->build({ source => 'Linktracker', value => { itemnumber => $item->itemnumber }});
119
120     is($item->tracked_links()->count,2,"Two tracked links found");
121 };
122
123 subtest 'is_bundle tests' => sub {
124     plan tests => 2;
125
126     $schema->storage->txn_begin;
127
128     my $item   = $builder->build_sample_item();
129
130     my $is_bundle = $item->is_bundle;
131     is($is_bundle, 0, 'is_bundle returns 0 when there are no items attached');
132
133     my $item2 = $builder->build_sample_item();
134     $schema->resultset('ItemBundle')
135       ->create( { host => $item->itemnumber, item => $item2->itemnumber } );
136
137     $is_bundle = $item->is_bundle;
138     is($is_bundle, 1, 'is_bundle returns 1 when there is at least one item attached');
139
140     $schema->storage->txn_rollback;
141 };
142
143 subtest 'in_bundle tests' => sub {
144     plan tests => 2;
145
146     $schema->storage->txn_begin;
147
148     my $item   = $builder->build_sample_item();
149
150     my $in_bundle = $item->in_bundle;
151     is($in_bundle, 0, 'in_bundle returns 0 when the item is not in a bundle');
152
153     my $host_item = $builder->build_sample_item();
154     $schema->resultset('ItemBundle')
155       ->create( { host => $host_item->itemnumber, item => $item->itemnumber } );
156
157     $in_bundle = $item->in_bundle;
158     is($in_bundle, 1, 'in_bundle returns 1 when the item is in a bundle');
159
160     $schema->storage->txn_rollback;
161 };
162
163 subtest 'bundle_items tests' => sub {
164     plan tests => 3;
165
166     $schema->storage->txn_begin;
167
168     my $host_item = $builder->build_sample_item();
169     my $bundle_items = $host_item->bundle_items;
170     is( ref($bundle_items), 'Koha::Items',
171         'bundle_items returns a Koha::Items object set' );
172     is( $bundle_items->count, 0,
173         'bundle_items set is empty when no items are bundled' );
174
175     my $bundle_item1 = $builder->build_sample_item();
176     my $bundle_item2 = $builder->build_sample_item();
177     my $bundle_item3 = $builder->build_sample_item();
178     $schema->resultset('ItemBundle')
179       ->create(
180         { host => $host_item->itemnumber, item => $bundle_item1->itemnumber } );
181     $schema->resultset('ItemBundle')
182       ->create(
183         { host => $host_item->itemnumber, item => $bundle_item2->itemnumber } );
184     $schema->resultset('ItemBundle')
185       ->create(
186         { host => $host_item->itemnumber, item => $bundle_item3->itemnumber } );
187
188     $bundle_items = $host_item->bundle_items;
189     is( $bundle_items->count, 3,
190         'bundle_items returns all the bundled items in the set' );
191
192     $schema->storage->txn_rollback;
193 };
194
195 subtest 'bundle_host tests' => sub {
196     plan tests => 3;
197
198     $schema->storage->txn_begin;
199
200     my $host_item = $builder->build_sample_item();
201     my $bundle_item1 = $builder->build_sample_item();
202     my $bundle_item2 = $builder->build_sample_item();
203     $schema->resultset('ItemBundle')
204       ->create(
205         { host => $host_item->itemnumber, item => $bundle_item2->itemnumber } );
206
207     my $bundle_host = $bundle_item1->bundle_host;
208     is( $bundle_host, undef, 'bundle_host returns undefined when the item it not part of a bundle');
209     $bundle_host = $bundle_item2->bundle_host;
210     is( ref($bundle_host), 'Koha::Item', 'bundle_host returns a Koha::Item object when the item is in a bundle');
211     is( $bundle_host->id, $host_item->id, 'bundle_host returns the host item when called against an item in a bundle');
212
213     $schema->storage->txn_rollback;
214 };
215
216 subtest 'add_to_bundle tests' => sub {
217     plan tests => 12;
218
219     $schema->storage->txn_begin;
220
221     t::lib::Mocks::mock_preference( 'BundleNotLoanValue', 1 );
222
223     my $library = $builder->build_object({ class => 'Koha::Libraries' });
224     t::lib::Mocks::mock_userenv({
225         branchcode => $library->branchcode
226     });
227
228     my $host_item = $builder->build_sample_item();
229     my $bundle_item1 = $builder->build_sample_item();
230     my $bundle_item2 = $builder->build_sample_item();
231     my $bundle_item3 = $builder->build_sample_item();
232
233     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
234
235     throws_ok { $host_item->add_to_bundle($host_item) }
236     'Koha::Exceptions::Item::Bundle::IsBundle',
237       'Exception thrown if you try to add the item to itself';
238
239     my $reserve_id = C4::Reserves::AddReserve(
240         {
241             branchcode     => $library->branchcode,
242             borrowernumber => $patron->borrowernumber,
243             biblionumber   => $bundle_item3->biblionumber,
244             itemnumber     => $bundle_item3->itemnumber,
245         }
246     );
247     throws_ok { $host_item->add_to_bundle($bundle_item3) }
248     'Koha::Exceptions::Item::Bundle::ItemHasHolds',
249       'Exception thrown if you try to add an item with holds to a bundle';
250
251     ok($host_item->add_to_bundle($bundle_item1), 'bundle_item1 added to bundle');
252     is($bundle_item1->notforloan, 1, 'add_to_bundle sets notforloan to BundleNotLoanValue');
253
254     throws_ok { $host_item->add_to_bundle($bundle_item1) }
255     'Koha::Exceptions::Object::DuplicateID',
256       'Exception thrown if you try to add the same item twice';
257
258     throws_ok { $bundle_item1->add_to_bundle($bundle_item2) }
259     'Koha::Exceptions::Item::Bundle::IsBundle',
260       'Exception thrown if you try to add an item to a bundled item';
261
262     throws_ok { $bundle_item2->add_to_bundle($host_item) }
263     'Koha::Exceptions::Item::Bundle::IsBundle',
264       'Exception thrown if you try to add a bundle host to a bundle item';
265
266     C4::Circulation::AddIssue( $patron, $host_item->barcode );
267     throws_ok { $host_item->add_to_bundle($bundle_item2) }
268     'Koha::Exceptions::Item::Bundle::BundleIsCheckedOut',
269       'Exception thrown if you try to add an item to a checked out bundle';
270     C4::Circulation::AddReturn( $host_item->barcode, $host_item->homebranch );
271     $host_item->discard_changes;
272
273     C4::Circulation::AddIssue( $patron, $bundle_item2->barcode );
274     throws_ok { $host_item->add_to_bundle($bundle_item2) }
275     'Koha::Exceptions::Item::Bundle::ItemIsCheckedOut',
276       'Exception thrown if you try to add a checked out item';
277
278     $bundle_item2->withdrawn(1)->store;
279     t::lib::Mocks::mock_preference( 'BlockReturnOfWithdrawnItems', 1 );
280     throws_ok { $host_item->add_to_bundle( $bundle_item2, { force_checkin => 1 } ) }
281     'Koha::Exceptions::Checkin::FailedCheckin',
282       'Exception thrown if you try to add a checked out item using
283       "force_checkin" and the return is not possible';
284
285     $bundle_item2->withdrawn(0)->store;
286     lives_ok { $host_item->add_to_bundle( $bundle_item2, { force_checkin => 1 } ) }
287     'No exception if you try to add a checked out item using "force_checkin" and the return is possible';
288
289     $bundle_item2->discard_changes;
290     ok( !$bundle_item2->checkout, 'Item is not checked out after being added to a bundle' );
291
292     $schema->storage->txn_rollback;
293 };
294
295 subtest 'remove_from_bundle tests' => sub {
296     plan tests => 4;
297
298     $schema->storage->txn_begin;
299
300     my $host_item = $builder->build_sample_item();
301     my $bundle_item1 = $builder->build_sample_item({ notforloan => 1 });
302     $host_item->add_to_bundle($bundle_item1);
303
304     my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
305     t::lib::Mocks::mock_userenv( { branchcode => $patron->branchcode } );
306
307     C4::Circulation::AddIssue( $patron, $host_item->barcode );
308     throws_ok { $bundle_item1->remove_from_bundle }
309     'Koha::Exceptions::Item::Bundle::BundleIsCheckedOut',
310       'Exception thrown if you try to add an item to a checked out bundle';
311     my ( $doreturn, $messages, $issue ) = C4::Circulation::AddReturn( $host_item->barcode, $host_item->homebranch );
312     $bundle_item1->discard_changes;
313
314     is($bundle_item1->remove_from_bundle(), 1, 'remove_from_bundle returns 1 when item is removed from a bundle');
315     is($bundle_item1->notforloan, 0, 'remove_from_bundle resets notforloan to 0');
316     $bundle_item1->discard_changes;
317     is($bundle_item1->remove_from_bundle(), 0, 'remove_from_bundle returns 0 when item is not in a bundle');
318
319     $schema->storage->txn_rollback;
320 };
321
322 subtest 'hidden_in_opac() tests' => sub {
323
324     plan tests => 4;
325
326     $schema->storage->txn_begin;
327
328     my $item  = $builder->build_sample_item({ itemlost => 2 });
329     my $rules = {};
330
331     # disable hidelostitems as it interteres with OpachiddenItems for the calculation
332     t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
333
334     ok( !$item->hidden_in_opac, 'No rules passed, shouldn\'t hide' );
335     ok( !$item->hidden_in_opac({ rules => $rules }), 'Empty rules passed, shouldn\'t hide' );
336
337     # enable hidelostitems to verify correct behaviour
338     t::lib::Mocks::mock_preference( 'hidelostitems', 1 );
339     ok( $item->hidden_in_opac, 'Even with no rules, item should hide because of hidelostitems syspref' );
340
341     # disable hidelostitems
342     t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
343     my $withdrawn = $item->withdrawn + 1; # make sure this attribute doesn't match
344
345     $rules = { withdrawn => [$withdrawn], itype => [ $item->itype ] };
346
347     ok( $item->hidden_in_opac({ rules => $rules }), 'Rule matching itype passed, should hide' );
348
349
350
351     $schema->storage->txn_rollback;
352 };
353
354 subtest 'has_pending_hold() tests' => sub {
355
356     plan tests => 2;
357
358     $schema->storage->txn_begin;
359
360     my $dbh = C4::Context->dbh;
361     my $item  = $builder->build_sample_item({ itemlost => 0 });
362     my $itemnumber = $item->itemnumber;
363
364     my $patron         = $builder->build_object( { class => 'Koha::Patrons' } );
365     my $borrowernumber = $patron->id;
366
367     $dbh->do(
368         "INSERT INTO tmp_holdsqueue (surname,borrowernumber,itemnumber) VALUES ('Clamp',$borrowernumber,$itemnumber)");
369     ok( $item->has_pending_hold, "Yes, we have a pending hold");
370     $dbh->do("DELETE FROM tmp_holdsqueue WHERE itemnumber=$itemnumber");
371     $item->discard_changes;
372     ok( !$item->has_pending_hold, "We don't have a pending hold if nothing in the tmp_holdsqueue");
373
374     $schema->storage->txn_rollback;
375 };
376
377 subtest "as_marc_field() tests" => sub {
378
379     my $mss = C4::Biblio::GetMarcSubfieldStructure( '' );
380     my ( $itemtag, $itemtagsubfield) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
381
382     my @schema_columns = $schema->resultset('Item')->result_source->columns;
383     my @mapped_columns = grep { exists $mss->{'items.'.$_} } @schema_columns;
384
385     plan tests => scalar @mapped_columns + 5;
386
387     $schema->storage->txn_begin;
388
389     my $item = $builder->build_sample_item;
390     # Make sure it has at least one undefined attribute
391     $item->set({ replacementprice => undef })->store->discard_changes;
392
393     my $marc_field = $item->as_marc_field;
394
395     is(
396         $marc_field->tag,
397         $itemtag,
398         'Generated field set the right tag number'
399     );
400
401     foreach my $column (@mapped_columns) {
402         my $tagsubfield = $mss->{ 'items.' . $column }[0]->{tagsubfield};
403         is( $marc_field->subfield($tagsubfield),
404             $item->$column, "Value is mapped correctly for column $column" );
405     }
406
407     my $unmapped_subfield = Koha::MarcSubfieldStructure->new(
408         {
409             frameworkcode => '',
410             tagfield      => $itemtag,
411             tagsubfield   => 'X',
412         }
413     )->store;
414     Koha::MarcSubfieldStructure->new(
415         {
416             frameworkcode => '',
417             tagfield      => $itemtag,
418             tagsubfield   => 'Y',
419             kohafield     => '',
420         }
421     )->store;
422
423     my @unlinked_subfields;
424     push @unlinked_subfields, X => 'Something weird', Y => 'Something else';
425     $item->more_subfields_xml( C4::Items::_get_unlinked_subfields_xml( \@unlinked_subfields ) )->store;
426
427     Koha::Caches->get_instance->clear_from_cache( "MarcStructure-1-" );
428     Koha::MarcSubfieldStructures->search(
429         { frameworkcode => '', tagfield => $itemtag } )
430       ->update( { display_order => \['FLOOR( 1 + RAND( ) * 10 )'] } );
431
432     $marc_field = $item->as_marc_field;
433
434     my $tagslib = C4::Biblio::GetMarcStructure(1, '');
435     my @subfields = $marc_field->subfields;
436     my $result = all { defined $_->[1] } @subfields;
437     ok( $result, 'There are no undef subfields' );
438     my @ordered_subfields = sort {
439             $tagslib->{$itemtag}->{ $a->[0] }->{display_order}
440         <=> $tagslib->{$itemtag}->{ $b->[0] }->{display_order}
441     } @subfields;
442     is_deeply(\@subfields, \@ordered_subfields);
443
444     is( scalar $marc_field->subfield('X'), 'Something weird', 'more_subfield_xml is considered when kohafield is NULL' );
445     is( scalar $marc_field->subfield('Y'), 'Something else', 'more_subfield_xml is considered when kohafield = ""' );
446
447     $schema->storage->txn_rollback;
448     Koha::Caches->get_instance->clear_from_cache( "MarcStructure-1-" );
449 };
450
451 subtest 'pickup_locations() tests' => sub {
452
453     plan tests => 68;
454
455     $schema->storage->txn_begin;
456
457     my $dbh = C4::Context->dbh;
458
459     my $root1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { ft_local_hold_group => 1, branchcode => undef } } );
460     my $root2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { ft_local_hold_group => 1, branchcode => undef } } );
461     my $library1 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, } } );
462     my $library2 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, } } );
463     my $library3 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 0, } } );
464     my $library4 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, } } );
465     my $group1_1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root1->id, branchcode => $library1->branchcode } } );
466     my $group1_2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root1->id, branchcode => $library2->branchcode } } );
467
468     my $group2_1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root2->id, branchcode => $library3->branchcode } } );
469     my $group2_2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root2->id, branchcode => $library4->branchcode } } );
470
471     our @branchcodes = (
472         $library1->branchcode, $library2->branchcode,
473         $library3->branchcode, $library4->branchcode
474     );
475
476     my $item1 = $builder->build_sample_item(
477         {
478             homebranch    => $library1->branchcode,
479             holdingbranch => $library2->branchcode,
480             copynumber    => 1,
481             ccode         => 'Gollum'
482         }
483     )->store;
484
485     my $item3 = $builder->build_sample_item(
486         {
487             homebranch    => $library3->branchcode,
488             holdingbranch => $library4->branchcode,
489             copynumber    => 3,
490             itype         => $item1->itype,
491         }
492     )->store;
493
494     Koha::CirculationRules->set_rules(
495         {
496             categorycode => undef,
497             itemtype     => $item1->itype,
498             branchcode   => undef,
499             rules        => {
500                 reservesallowed => 25,
501             }
502         }
503     );
504
505     throws_ok
506       { $item1->pickup_locations }
507       'Koha::Exceptions::MissingParameter',
508       'Exception thrown on missing parameter';
509
510     is( $@->parameter, 'patron', 'Exception param correctly set' );
511
512     my $patron1 = $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $library1->branchcode, firstname => '1' } } );
513     my $patron4 = $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $library4->branchcode, firstname => '4' } } );
514
515     my $results = {
516         "1-1-from_home_library-any"               => 3,
517         "1-1-from_home_library-holdgroup"         => 2,
518         "1-1-from_home_library-patrongroup"       => 2,
519         "1-1-from_home_library-homebranch"        => 1,
520         "1-1-from_home_library-holdingbranch"     => 1,
521         "1-1-from_any_library-any"                => 3,
522         "1-1-from_any_library-holdgroup"          => 2,
523         "1-1-from_any_library-patrongroup"        => 2,
524         "1-1-from_any_library-homebranch"         => 1,
525         "1-1-from_any_library-holdingbranch"      => 1,
526         "1-1-from_local_hold_group-any"           => 3,
527         "1-1-from_local_hold_group-holdgroup"     => 2,
528         "1-1-from_local_hold_group-patrongroup"   => 2,
529         "1-1-from_local_hold_group-homebranch"    => 1,
530         "1-1-from_local_hold_group-holdingbranch" => 1,
531         "1-4-from_home_library-any"               => 0,
532         "1-4-from_home_library-holdgroup"         => 0,
533         "1-4-from_home_library-patrongroup"       => 0,
534         "1-4-from_home_library-homebranch"        => 0,
535         "1-4-from_home_library-holdingbranch"     => 0,
536         "1-4-from_any_library-any"                => 3,
537         "1-4-from_any_library-holdgroup"          => 2,
538         "1-4-from_any_library-patrongroup"        => 1,
539         "1-4-from_any_library-homebranch"         => 1,
540         "1-4-from_any_library-holdingbranch"      => 1,
541         "1-4-from_local_hold_group-any"           => 0,
542         "1-4-from_local_hold_group-holdgroup"     => 0,
543         "1-4-from_local_hold_group-patrongroup"   => 0,
544         "1-4-from_local_hold_group-homebranch"    => 0,
545         "1-4-from_local_hold_group-holdingbranch" => 0,
546         "3-1-from_home_library-any"               => 0,
547         "3-1-from_home_library-holdgroup"         => 0,
548         "3-1-from_home_library-patrongroup"       => 0,
549         "3-1-from_home_library-homebranch"        => 0,
550         "3-1-from_home_library-holdingbranch"     => 0,
551         "3-1-from_any_library-any"                => 3,
552         "3-1-from_any_library-holdgroup"          => 1,
553         "3-1-from_any_library-patrongroup"        => 2,
554         "3-1-from_any_library-homebranch"         => 0,
555         "3-1-from_any_library-holdingbranch"      => 1,
556         "3-1-from_local_hold_group-any"           => 0,
557         "3-1-from_local_hold_group-holdgroup"     => 0,
558         "3-1-from_local_hold_group-patrongroup"   => 0,
559         "3-1-from_local_hold_group-homebranch"    => 0,
560         "3-1-from_local_hold_group-holdingbranch" => 0,
561         "3-4-from_home_library-any"               => 0,
562         "3-4-from_home_library-holdgroup"         => 0,
563         "3-4-from_home_library-patrongroup"       => 0,
564         "3-4-from_home_library-homebranch"        => 0,
565         "3-4-from_home_library-holdingbranch"     => 0,
566         "3-4-from_any_library-any"                => 3,
567         "3-4-from_any_library-holdgroup"          => 1,
568         "3-4-from_any_library-patrongroup"        => 1,
569         "3-4-from_any_library-homebranch"         => 0,
570         "3-4-from_any_library-holdingbranch"      => 1,
571         "3-4-from_local_hold_group-any"           => 3,
572         "3-4-from_local_hold_group-holdgroup"     => 1,
573         "3-4-from_local_hold_group-patrongroup"   => 1,
574         "3-4-from_local_hold_group-homebranch"    => 0,
575         "3-4-from_local_hold_group-holdingbranch" => 1
576     };
577
578     sub _doTest {
579         my ( $item, $patron, $ha, $hfp, $results ) = @_;
580
581         Koha::CirculationRules->set_rules(
582             {
583                 branchcode => undef,
584                 itemtype   => undef,
585                 rules => {
586                     holdallowed => $ha,
587                     hold_fulfillment_policy => $hfp,
588                     returnbranch => 'any'
589                 }
590             }
591         );
592         my $ha_value =
593           $ha eq 'from_local_hold_group' ? 'holdgroup'
594           : (
595             $ha eq 'from_any_library' ? 'any'
596             : 'homebranch'
597           );
598
599         my @pl = map {
600             my $pickup_location = $_;
601             grep { $pickup_location->branchcode eq $_ } @branchcodes
602         } $item->pickup_locations( { patron => $patron } )->as_list;
603
604         ok(
605             scalar(@pl) eq $results->{
606                     $item->copynumber . '-'
607                   . $patron->firstname . '-'
608                   . $ha . '-'
609                   . $hfp
610             },
611             'item'
612               . $item->copynumber
613               . ', patron'
614               . $patron->firstname
615               . ', holdallowed: '
616               . $ha_value
617               . ', hold_fulfillment_policy: '
618               . $hfp
619               . ' should return '
620               . $results->{
621                     $item->copynumber . '-'
622                   . $patron->firstname . '-'
623                   . $ha . '-'
624                   . $hfp
625               }
626               . ' and returns '
627               . scalar(@pl)
628         );
629
630     }
631
632
633     foreach my $item ($item1, $item3) {
634         foreach my $patron ($patron1, $patron4) {
635             #holdallowed 1: homebranch, 2: any, 3: holdgroup
636             foreach my $ha ('from_home_library', 'from_any_library', 'from_local_hold_group') {
637                 foreach my $hfp ('any', 'holdgroup', 'patrongroup', 'homebranch', 'holdingbranch') {
638                     _doTest($item, $patron, $ha, $hfp, $results);
639                 }
640             }
641         }
642     }
643
644     # Now test that branchtransferlimits will further filter the pickup locations
645
646     my $item_no_ccode = $builder->build_sample_item(
647         {
648             homebranch    => $library1->branchcode,
649             holdingbranch => $library2->branchcode,
650             itype         => $item1->itype,
651         }
652     )->store;
653
654     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
655     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
656     Koha::CirculationRules->set_rules(
657         {
658             branchcode => undef,
659             itemtype   => $item1->itype,
660             rules      => {
661                 holdallowed             => 'from_home_library',
662                 hold_fulfillment_policy => 1,
663                 returnbranch            => 'any'
664             }
665         }
666     );
667     $builder->build_object(
668         {
669             class => 'Koha::Item::Transfer::Limits',
670             value => {
671                 toBranch   => $library1->branchcode,
672                 fromBranch => $library2->branchcode,
673                 itemtype   => $item1->itype,
674                 ccode      => undef,
675             }
676         }
677     );
678
679     my @pickup_locations = map {
680         my $pickup_location = $_;
681         grep { $pickup_location->branchcode eq $_ } @branchcodes
682     } $item1->pickup_locations( { patron => $patron1 } )->as_list;
683
684     is( scalar @pickup_locations, 3 - 1, "With a transfer limits we get back the libraries that are pickup locations minus 1 limited library");
685
686     $builder->build_object(
687         {
688             class => 'Koha::Item::Transfer::Limits',
689             value => {
690                 toBranch   => $library4->branchcode,
691                 fromBranch => $library2->branchcode,
692                 itemtype   => $item1->itype,
693                 ccode      => undef,
694             }
695         }
696     );
697
698     @pickup_locations = map {
699         my $pickup_location = $_;
700         grep { $pickup_location->branchcode eq $_ } @branchcodes
701     } $item1->pickup_locations( { patron => $patron1 } )->as_list;
702
703     is( scalar @pickup_locations, 3 - 2, "With 2 transfer limits we get back the libraries that are pickup locations minus 2 limited libraries");
704
705     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'ccode');
706     @pickup_locations = map {
707         my $pickup_location = $_;
708         grep { $pickup_location->branchcode eq $_ } @branchcodes
709     } $item1->pickup_locations( { patron => $patron1 } )->as_list;
710     is( scalar @pickup_locations, 3, "With no transfer limits of type ccode we get back the libraries that are pickup locations");
711
712     @pickup_locations = map {
713         my $pickup_location = $_;
714         grep { $pickup_location->branchcode eq $_ } @branchcodes
715     } $item_no_ccode->pickup_locations( { patron => $patron1 } )->as_list;
716     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");
717
718     $builder->build_object(
719         {
720             class => 'Koha::Item::Transfer::Limits',
721             value => {
722                 toBranch   => $library2->branchcode,
723                 fromBranch => $library2->branchcode,
724                 itemtype   => undef,
725                 ccode      => $item1->ccode,
726             }
727         }
728     );
729
730     @pickup_locations = map {
731         my $pickup_location = $_;
732         grep { $pickup_location->branchcode eq $_ } @branchcodes
733     } $item1->pickup_locations( { patron => $patron1 } )->as_list;
734     is( scalar @pickup_locations, 3 - 1, "With a transfer limits we get back the libraries that are pickup locations minus 1 limited library");
735
736     $builder->build_object(
737         {
738             class => 'Koha::Item::Transfer::Limits',
739             value => {
740                 toBranch   => $library4->branchcode,
741                 fromBranch => $library2->branchcode,
742                 itemtype   => undef,
743                 ccode      => $item1->ccode,
744             }
745         }
746     );
747
748     @pickup_locations = map {
749         my $pickup_location = $_;
750         grep { $pickup_location->branchcode eq $_ } @branchcodes
751     } $item1->pickup_locations( { patron => $patron1 } )->as_list;
752     is( scalar @pickup_locations, 3 - 2, "With 2 transfer limits we get back the libraries that are pickup locations minus 2 limited libraries");
753
754     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 0);
755
756     $schema->storage->txn_rollback;
757 };
758
759 subtest 'request_transfer' => sub {
760     plan tests => 13;
761     $schema->storage->txn_begin;
762
763     my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
764     my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
765     my $item     = $builder->build_sample_item(
766         {
767             homebranch    => $library1->branchcode,
768             holdingbranch => $library2->branchcode,
769         }
770     );
771
772     # Mandatory fields tests
773     throws_ok { $item->request_transfer( { to => $library1 } ) }
774     'Koha::Exceptions::MissingParameter',
775       'Exception thrown if `reason` parameter is missing';
776
777     throws_ok { $item->request_transfer( { reason => 'Manual' } ) }
778     'Koha::Exceptions::MissingParameter',
779       'Exception thrown if `to` parameter is missing';
780
781     # Successful request
782     my $transfer = $item->request_transfer({ to => $library1, reason => 'Manual' });
783     is( ref($transfer), 'Koha::Item::Transfer',
784         'Koha::Item->request_transfer should return a Koha::Item::Transfer object'
785     );
786     my $original_transfer = $transfer->get_from_storage;
787
788     # Transfer already in progress
789     throws_ok { $item->request_transfer( { to => $library2, reason => 'Manual' } ) }
790     'Koha::Exceptions::Item::Transfer::InQueue',
791       'Exception thrown if transfer is already in progress';
792
793     my $exception = $@;
794     is( ref( $exception->transfer ),
795         'Koha::Item::Transfer',
796         'The exception contains the found Koha::Item::Transfer' );
797
798     # Queue transfer
799     my $queued_transfer = $item->request_transfer(
800         { to => $library2, reason => 'Manual', enqueue => 1 } );
801     is( ref($queued_transfer), 'Koha::Item::Transfer',
802         'Koha::Item->request_transfer allowed when enqueue is set' );
803     my $transfers = $item->get_transfers;
804     is($transfers->count, 2, "There are now 2 live transfers in the queue");
805     $transfer = $transfer->get_from_storage;
806     is_deeply($transfer->unblessed, $original_transfer->unblessed, "Original transfer unchanged");
807     $queued_transfer->datearrived(dt_from_string)->store();
808
809     # Replace transfer
810     my $replaced_transfer = $item->request_transfer(
811         { to => $library2, reason => 'Manual', replace => 1 } );
812     is( ref($replaced_transfer), 'Koha::Item::Transfer',
813         'Koha::Item->request_transfer allowed when replace is set' );
814     $original_transfer->discard_changes;
815     ok($original_transfer->datecancelled, "Original transfer cancelled");
816     $transfers = $item->get_transfers;
817     is($transfers->count, 1, "There is only 1 live transfer in the queue");
818     $replaced_transfer->datearrived(dt_from_string)->store();
819
820     # BranchTransferLimits
821     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
822     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
823     my $limit = Koha::Item::Transfer::Limit->new({
824         fromBranch => $library2->branchcode,
825         toBranch => $library1->branchcode,
826         itemtype => $item->effective_itemtype,
827     })->store;
828
829     throws_ok { $item->request_transfer( { to => $library1, reason => 'Manual' } ) }
830     'Koha::Exceptions::Item::Transfer::Limit',
831       'Exception thrown if transfer is prevented by limits';
832
833     my $forced_transfer = $item->request_transfer( { to => $library1, reason => 'Manual', ignore_limits => 1 } );
834     is( ref($forced_transfer), 'Koha::Item::Transfer',
835         'Koha::Item->request_transfer allowed when ignore_limits is set'
836     );
837
838     $schema->storage->txn_rollback;
839 };
840
841 subtest 'deletion' => sub {
842     plan tests => 15;
843
844     $schema->storage->txn_begin;
845
846     my $biblio = $builder->build_sample_biblio();
847
848     my $item = $builder->build_sample_item(
849         {
850             biblionumber => $biblio->biblionumber,
851         }
852     );
853     is( $item->deleted_on, undef, 'deleted_on not set for new item' );
854
855     my $deleted_item = $item->move_to_deleted;
856     is( ref( $deleted_item ), 'Koha::Schema::Result::Deleteditem', 'Koha::Item->move_to_deleted should return the Deleted item' )
857       ;    # FIXME This should be Koha::Deleted::Item
858     is( t::lib::Dates::compare( $deleted_item->deleted_on, dt_from_string() ), 0 );
859
860     is( Koha::Old::Items->search({itemnumber => $item->itemnumber})->count, 1, '->move_to_deleted must have moved the item to deleteditem' );
861     $item = $builder->build_sample_item(
862         {
863             biblionumber => $biblio->biblionumber,
864         }
865     );
866     $item->delete;
867     is( Koha::Old::Items->search({itemnumber => $item->itemnumber})->count, 0, '->move_to_deleted must not have moved the item to deleteditem' );
868
869
870     my $library   = $builder->build_object({ class => 'Koha::Libraries' });
871     my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
872
873     my $patron = $builder->build_object({class => 'Koha::Patrons', value => { flags => 2^9 } });    # allow edit items
874     t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode, borrowernumber => $patron->id });
875
876     $item = $builder->build_sample_item({ library => $library->branchcode });
877
878     # book_on_loan
879     C4::Circulation::AddIssue( $patron, $item->barcode );
880
881     is(
882         @{$item->safe_to_delete->messages}[0]->message,
883         'book_on_loan',
884         'Koha::Item->safe_to_delete reports item on loan',
885     );
886
887     is(
888         @{$item->safe_to_delete->messages}[0]->message,
889         'book_on_loan',
890         'item that is on loan cannot be deleted',
891     );
892
893     ok(
894         ! $item->safe_to_delete,
895         'Koha::Item->safe_to_delete shows item NOT safe to delete'
896     );
897
898     AddReturn( $item->barcode, $library->branchcode );
899
900     # not_same_branch
901     t::lib::Mocks::mock_preference('IndependentBranches', 1);
902     my $item_2 = $builder->build_sample_item({ library => $library_2->branchcode });
903
904     is(
905         @{$item_2->safe_to_delete->messages}[0]->message,
906         'not_same_branch',
907         'Koha::Item->safe_to_delete reports IndependentBranches restriction',
908     );
909
910     is(
911         @{$item_2->safe_to_delete->messages}[0]->message,
912         'not_same_branch',
913         'IndependentBranches prevents deletion at another branch',
914     );
915
916     # linked_analytics
917
918     { # codeblock to limit scope of $module->mock
919
920         my $module = Test::MockModule->new('C4::Items');
921         $module->mock( GetAnalyticsCount => sub { return 1 } );
922
923         $item->discard_changes;
924         is(
925             @{$item->safe_to_delete->messages}[0]->message,
926             'linked_analytics',
927             'Koha::Item->safe_to_delete reports linked analytics',
928         );
929
930         is(
931             @{$item->safe_to_delete->messages}[0]->message,
932             'linked_analytics',
933             'Linked analytics prevents deletion of item',
934         );
935
936     }
937
938     ok(
939         $item->safe_to_delete,
940         'Koha::Item->safe_to_delete shows item safe to delete'
941     );
942
943     $item->safe_delete,
944
945     my $test_item = Koha::Items->find( $item->itemnumber );
946
947     is( $test_item, undef,
948         "Koha::Item->safe_delete should delete item if safe_to_delete returns true"
949     );
950
951     subtest 'holds tests' => sub {
952
953         plan tests => 9;
954
955         # to avoid noise
956         t::lib::Mocks::mock_preference( 'IndependentBranches', 0 );
957
958         $schema->storage->txn_begin;
959
960         my $item = $builder->build_sample_item;
961
962         my $processing     = $builder->build_object( { class => 'Koha::Holds', value => { itemnumber => $item->id, itemnumber => $item->id, found => 'P' } } );
963         my $safe_to_delete = $item->safe_to_delete;
964
965         ok( !$safe_to_delete, 'Cannot delete' );
966         is(
967             @{ $safe_to_delete->messages }[0]->message,
968             'book_reserved',
969             'Koha::Item->safe_to_delete reports a in processing hold blocks deletion'
970         );
971
972         $processing->delete;
973
974         my $in_transit = $builder->build_object( { class => 'Koha::Holds', value => { itemnumber => $item->id, itemnumber => $item->id, found => 'T' } } );
975         $safe_to_delete = $item->safe_to_delete;
976
977         ok( !$safe_to_delete, 'Cannot delete' );
978         is(
979             @{ $safe_to_delete->messages }[0]->message,
980             'book_reserved',
981             'Koha::Item->safe_to_delete reports a in transit hold blocks deletion'
982         );
983
984         $in_transit->delete;
985
986         my $waiting = $builder->build_object( { class => 'Koha::Holds', value => { itemnumber => $item->id, itemnumber => $item->id, found => 'W' } } );
987         $safe_to_delete = $item->safe_to_delete;
988
989         ok( !$safe_to_delete, 'Cannot delete' );
990         is(
991             @{ $safe_to_delete->messages }[0]->message,
992             'book_reserved',
993             'Koha::Item->safe_to_delete reports a waiting hold blocks deletion'
994         );
995
996         $waiting->delete;
997
998         # Add am unfilled biblio-level hold to catch the 'last_item_for_hold' use case
999         $builder->build_object( { class => 'Koha::Holds', value => { biblionumber => $item->biblionumber, itemnumber => undef, found => undef } } );
1000
1001         $safe_to_delete = $item->safe_to_delete;
1002
1003         ok( !$safe_to_delete );
1004
1005         is(
1006             @{ $safe_to_delete->messages}[0]->message,
1007             'last_item_for_hold',
1008             'Item cannot be deleted if a biblio-level is placed on the biblio and there is only 1 item attached to the biblio'
1009         );
1010
1011         my $extra_item = $builder->build_sample_item({ biblionumber => $item->biblionumber });
1012
1013         ok( $item->safe_to_delete );
1014
1015         $schema->storage->txn_rollback;
1016     };
1017
1018     $schema->storage->txn_rollback;
1019 };
1020
1021 subtest 'renewal_branchcode' => sub {
1022     plan tests => 13;
1023
1024     $schema->storage->txn_begin;
1025
1026     my $item = $builder->build_sample_item();
1027     my $branch = $builder->build_object({ class => 'Koha::Libraries' });
1028     my $checkout = $builder->build_object({
1029         class => 'Koha::Checkouts',
1030         value => {
1031             itemnumber => $item->itemnumber,
1032         }
1033     });
1034
1035
1036     C4::Context->interface( 'intranet' );
1037     t::lib::Mocks::mock_userenv({ branchcode => $branch->branchcode });
1038
1039     is( $item->renewal_branchcode, $branch->branchcode, "If interface not opac, we get the branch from context");
1040     is( $item->renewal_branchcode({ branch => "PANDA"}), $branch->branchcode, "If interface not opac, we get the branch from context even if we pass one in");
1041     C4::Context->set_userenv(51, 'userid4tests', undef, 'firstname', 'surname', undef, undef, 0, undef, undef, undef ); #mock userenv doesn't let us set null branch
1042     is( $item->renewal_branchcode({ branch => "PANDA"}), "PANDA", "If interface not opac, we get the branch we pass one in if context not set");
1043
1044     C4::Context->interface( 'opac' );
1045
1046     t::lib::Mocks::mock_preference('OpacRenewalBranch', undef);
1047     is( $item->renewal_branchcode, 'OPACRenew', "If interface opac and OpacRenewalBranch undef, we get OPACRenew");
1048     is( $item->renewal_branchcode({branch=>'COW'}), 'OPACRenew', "If interface opac and OpacRenewalBranch undef, we get OPACRenew even if branch passed");
1049
1050     t::lib::Mocks::mock_preference('OpacRenewalBranch', 'none');
1051     is( $item->renewal_branchcode, '', "If interface opac and OpacRenewalBranch is none, we get blank string");
1052     is( $item->renewal_branchcode({branch=>'COW'}), '', "If interface opac and OpacRenewalBranch is none, we get blank string even if branch passed");
1053
1054     t::lib::Mocks::mock_preference('OpacRenewalBranch', 'checkoutbranch');
1055     is( $item->renewal_branchcode, $checkout->branchcode, "If interface opac and OpacRenewalBranch set to checkoutbranch, we get branch of checkout");
1056     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");
1057
1058     t::lib::Mocks::mock_preference('OpacRenewalBranch','patronhomebranch');
1059     is( $item->renewal_branchcode, $checkout->patron->branchcode, "If interface opac and OpacRenewalBranch set to patronbranch, we get branch of patron");
1060     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");
1061
1062     t::lib::Mocks::mock_preference('OpacRenewalBranch','itemhomebranch');
1063     is( $item->renewal_branchcode, $item->homebranch, "If interface opac and OpacRenewalBranch set to itemhomebranch, we get homebranch of item");
1064     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");
1065
1066     $schema->storage->txn_rollback;
1067 };
1068
1069 subtest 'Tests for itemtype' => sub {
1070     plan tests => 2;
1071     $schema->storage->txn_begin;
1072
1073     my $biblio = $builder->build_sample_biblio;
1074     my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' });
1075     my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber, itype => $itemtype->itemtype });
1076
1077     t::lib::Mocks::mock_preference('item-level_itypes', 1);
1078     is( $item->itemtype->itemtype, $item->itype, 'Pref enabled' );
1079     t::lib::Mocks::mock_preference('item-level_itypes', 0);
1080     is( $item->itemtype->itemtype, $biblio->biblioitem->itemtype, 'Pref disabled' );
1081
1082     $schema->storage->txn_rollback;
1083 };
1084
1085 subtest 'get_transfers' => sub {
1086     plan tests => 16;
1087     $schema->storage->txn_begin;
1088
1089     my $item = $builder->build_sample_item();
1090
1091     my $transfers = $item->get_transfers();
1092     is(ref($transfers), 'Koha::Item::Transfers', 'Koha::Item->get_transfer should return a Koha::Item::Transfers object' );
1093     is($transfers->count, 0, 'When no transfers exist, the Koha::Item:Transfers object should be empty');
1094
1095     my $library_to = $builder->build_object( { class => 'Koha::Libraries' } );
1096
1097     my $transfer_1 = $builder->build_object(
1098         {
1099             class => 'Koha::Item::Transfers',
1100             value => {
1101                 itemnumber    => $item->itemnumber,
1102                 frombranch    => $item->holdingbranch,
1103                 tobranch      => $library_to->branchcode,
1104                 reason        => 'Manual',
1105                 datesent      => undef,
1106                 datearrived   => undef,
1107                 datecancelled => undef,
1108                 daterequested => \'NOW()'
1109             }
1110         }
1111     );
1112
1113     $transfers = $item->get_transfers();
1114     is($transfers->count, 1, 'When one transfer has been requested, the Koha::Item:Transfers object should contain one result');
1115
1116     my $transfer_2 = $builder->build_object(
1117         {
1118             class => 'Koha::Item::Transfers',
1119             value => {
1120                 itemnumber    => $item->itemnumber,
1121                 frombranch    => $item->holdingbranch,
1122                 tobranch      => $library_to->branchcode,
1123                 reason        => 'Manual',
1124                 datesent      => undef,
1125                 datearrived   => undef,
1126                 datecancelled => undef,
1127                 daterequested => \'NOW()'
1128             }
1129         }
1130     );
1131
1132     my $transfer_3 = $builder->build_object(
1133         {
1134             class => 'Koha::Item::Transfers',
1135             value => {
1136                 itemnumber    => $item->itemnumber,
1137                 frombranch    => $item->holdingbranch,
1138                 tobranch      => $library_to->branchcode,
1139                 reason        => 'Manual',
1140                 datesent      => undef,
1141                 datearrived   => undef,
1142                 datecancelled => undef,
1143                 daterequested => \'NOW()'
1144             }
1145         }
1146     );
1147
1148     $transfers = $item->get_transfers();
1149     is($transfers->count, 3, 'When there are multiple open transfer requests, the Koha::Item::Transfers object contains them all');
1150     my $result_1 = $transfers->next;
1151     my $result_2 = $transfers->next;
1152     my $result_3 = $transfers->next;
1153     is( $result_1->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfers returns the oldest transfer request first');
1154     is( $result_2->branchtransfer_id, $transfer_2->branchtransfer_id, 'Koha::Item->get_transfers returns the newer transfer request second');
1155     is( $result_3->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfers returns the newest transfer request last');
1156
1157     $transfer_2->datesent(\'NOW()')->store;
1158     $transfers = $item->get_transfers();
1159     is($transfers->count, 3, 'When one transfer is set to in_transit, the Koha::Item::Transfers object still contains them all');
1160     $result_1 = $transfers->next;
1161     $result_2 = $transfers->next;
1162     $result_3 = $transfers->next;
1163     is( $result_1->branchtransfer_id, $transfer_2->branchtransfer_id, 'Koha::Item->get_transfers returns the active transfer request first');
1164     is( $result_2->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfers returns the other transfers oldest to newest');
1165     is( $result_3->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfers returns the other transfers oldest to newest');
1166
1167     $transfer_2->datearrived(\'NOW()')->store;
1168     $transfers = $item->get_transfers();
1169     is($transfers->count, 2, 'Once a transfer is received, it no longer appears in the list from ->get_transfers()');
1170     $result_1 = $transfers->next;
1171     $result_2 = $transfers->next;
1172     is( $result_1->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfers returns the other transfers oldest to newest');
1173     is( $result_2->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfers returns the other transfers oldest to newest');
1174
1175     $transfer_1->datecancelled(\'NOW()')->store;
1176     $transfers = $item->get_transfers();
1177     is($transfers->count, 1, 'Once a transfer is cancelled, it no longer appears in the list from ->get_transfers()');
1178     $result_1 = $transfers->next;
1179     is( $result_1->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfers returns the only transfer that remains');
1180
1181     $schema->storage->txn_rollback;
1182 };
1183
1184 subtest 'Test for relationship between item and current_branchtransfers' => sub {
1185     plan tests => 4;
1186
1187     $schema->storage->txn_begin;
1188
1189     my $item     = $builder->build_sample_item();
1190     my $transfer = $builder->build_object(
1191         {
1192             class => 'Koha::Item::Transfers',
1193             value => {
1194                 itemnumber    => $item->itemnumber,
1195                 datesent      => dt_from_string,
1196                 datearrived   => dt_from_string,
1197                 datecancelled => undef,
1198             }
1199         }
1200     );
1201
1202     my $transfer_item = $transfer->item;
1203     my $biblio        = Koha::Biblios->find( $transfer_item->biblionumber );
1204
1205     my $current_branchtransfers = Koha::Items->search(
1206         { 'me.itemnumber' => $transfer_item->itemnumber },
1207         { prefetch        => ['current_branchtransfers'] }
1208     );
1209
1210     my $item_with_branchtransfers = $current_branchtransfers->next;
1211
1212     is(
1213         $transfer_item->itemnumber,
1214         $item_with_branchtransfers->itemnumber,
1215         'following two items are the same'
1216     );
1217
1218     # following two tests should produce the same result
1219     is(
1220         $transfer_item->get_transfer,
1221         undef,
1222         'Koha::Item->get_transfer returns undef with no active transfers'
1223     );
1224     is(
1225         $item_with_branchtransfers->get_transfer, undef,
1226         'prefetched result->get_transfer returns undef with no active transfers'
1227     );
1228
1229     $transfer->set(
1230         {
1231             datearrived => undef,
1232         }
1233     )->store;
1234
1235     $current_branchtransfers = Koha::Items->search(
1236         { 'me.itemnumber' => $transfer_item->itemnumber },
1237         { prefetch        => ['current_branchtransfers'] }
1238     );
1239
1240     $item_with_branchtransfers = $current_branchtransfers->next;
1241
1242     is(
1243         $transfer_item->get_transfer->branchtransfer_id,
1244         $item_with_branchtransfers->get_transfer->branchtransfer_id,
1245         'an active transfer produces same branchtransfer_id for both methods'
1246     );
1247
1248     $schema->storage->txn_rollback;
1249 };
1250
1251 subtest 'Tests for relationship between item and item_orders via aqorders_item' => sub {
1252     plan tests => 3;
1253
1254     $schema->storage->txn_begin;
1255
1256     my $biblio = $builder->build_sample_biblio();
1257     my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1258
1259     my $orders = $item->orders;
1260     is ($orders->count, 0, 'No order on this item yet');
1261
1262     my $order_note = 'Order for ' . $item->itemnumber;
1263
1264     my $aq_order1 = $builder->build_object({
1265         class => 'Koha::Acquisition::Orders',
1266         value  => {
1267             biblionumber => $biblio->biblionumber,
1268             order_internalnote => $order_note,
1269         },
1270     });
1271     my $aq_order2 = $builder->build_object({
1272         class => 'Koha::Acquisition::Orders',
1273         value  => {
1274             biblionumber => $biblio->biblionumber,
1275         },
1276     });
1277     my $aq_order_item1 = $builder->build({
1278         source => 'AqordersItem',
1279         value  => {
1280             ordernumber => $aq_order1->ordernumber,
1281             itemnumber => $item->itemnumber,
1282         },
1283     });
1284
1285     $orders = $item->orders;
1286     is ($orders->count, 1, 'One order found by item with the relationship');
1287     is ($orders->next->order_internalnote, $order_note, 'Correct order found by item with the relationship');
1288 };
1289
1290 subtest 'move_to_biblio() tests' => sub {
1291     plan tests => 16;
1292
1293     $schema->storage->txn_begin;
1294
1295     my $dbh = C4::Context->dbh;
1296
1297     my $source_biblio = $builder->build_sample_biblio();
1298     my $target_biblio = $builder->build_sample_biblio();
1299
1300     my $source_biblionumber = $source_biblio->biblionumber;
1301     my $target_biblionumber = $target_biblio->biblionumber;
1302
1303     my $item1 = $builder->build_sample_item({ biblionumber => $source_biblionumber });
1304     my $item2 = $builder->build_sample_item({ biblionumber => $source_biblionumber });
1305     my $item3 = $builder->build_sample_item({ biblionumber => $source_biblionumber });
1306
1307     my $itemnumber1 = $item1->itemnumber;
1308     my $itemnumber2 = $item2->itemnumber;
1309
1310     my $library = $builder->build_object({ class => 'Koha::Libraries' });
1311
1312     my $patron = $builder->build_object({
1313         class => 'Koha::Patrons',
1314         value => { branchcode => $library->branchcode }
1315     });
1316     my $borrowernumber = $patron->borrowernumber;
1317
1318     my $aq_budget = $builder->build({
1319         source => 'Aqbudget',
1320         value  => {
1321             budget_notes => 'test',
1322         },
1323     });
1324
1325     my $aq_order1 = $builder->build_object({
1326         class => 'Koha::Acquisition::Orders',
1327         value  => {
1328             biblionumber => $source_biblionumber,
1329             budget_id => $aq_budget->{budget_id},
1330         },
1331     });
1332     my $aq_order_item1 = $builder->build({
1333         source => 'AqordersItem',
1334         value  => {
1335             ordernumber => $aq_order1->ordernumber,
1336             itemnumber => $itemnumber1,
1337         },
1338     });
1339     my $aq_order2 = $builder->build_object({
1340         class => 'Koha::Acquisition::Orders',
1341         value  => {
1342             biblionumber => $source_biblionumber,
1343             budget_id => $aq_budget->{budget_id},
1344         },
1345     });
1346     my $aq_order_item2 = $builder->build({
1347         source => 'AqordersItem',
1348         value  => {
1349             ordernumber => $aq_order2->ordernumber,
1350             itemnumber => $itemnumber2,
1351         },
1352     });
1353
1354     my $bib_level_hold = $builder->build_object({
1355         class => 'Koha::Holds',
1356         value  => {
1357             biblionumber => $source_biblionumber,
1358             itemnumber => undef,
1359         },
1360     });
1361     my $item_level_hold1 = $builder->build_object({
1362         class => 'Koha::Holds',
1363         value  => {
1364             biblionumber => $source_biblionumber,
1365             itemnumber => $itemnumber1,
1366         },
1367     });
1368     my $item_level_hold2 = $builder->build_object({
1369         class => 'Koha::Holds',
1370         value  => {
1371             biblionumber => $source_biblionumber,
1372             itemnumber => $itemnumber2,
1373         }
1374     });
1375
1376     my $tmp_holdsqueue1 = $builder->build({
1377         source => 'TmpHoldsqueue',
1378         value  => {
1379             borrowernumber => $borrowernumber,
1380             biblionumber   => $source_biblionumber,
1381             itemnumber     => $itemnumber1,
1382         }
1383     });
1384     my $tmp_holdsqueue2 = $builder->build({
1385         source => 'TmpHoldsqueue',
1386         value  => {
1387             borrowernumber => $borrowernumber,
1388             biblionumber   => $source_biblionumber,
1389             itemnumber     => $itemnumber2,
1390         }
1391     });
1392     my $hold_fill_target1 = $builder->build({
1393         source => 'HoldFillTarget',
1394         value  => {
1395             borrowernumber     => $borrowernumber,
1396             biblionumber       => $source_biblionumber,
1397             itemnumber         => $itemnumber1,
1398         }
1399     });
1400     my $hold_fill_target2 = $builder->build({
1401         source => 'HoldFillTarget',
1402         value  => {
1403             borrowernumber     => $borrowernumber,
1404             biblionumber       => $source_biblionumber,
1405             itemnumber         => $itemnumber2,
1406         }
1407     });
1408     my $linktracker1 = $builder->build({
1409         source => 'Linktracker',
1410         value  => {
1411             borrowernumber     => $borrowernumber,
1412             biblionumber       => $source_biblionumber,
1413             itemnumber         => $itemnumber1,
1414         }
1415     });
1416     my $linktracker2 = $builder->build({
1417         source => 'Linktracker',
1418         value  => {
1419             borrowernumber     => $borrowernumber,
1420             biblionumber       => $source_biblionumber,
1421             itemnumber         => $itemnumber2,
1422         }
1423     });
1424
1425     my $to_biblionumber_after_move = $item1->move_to_biblio($target_biblio);
1426     is($to_biblionumber_after_move, $target_biblionumber, 'move_to_biblio returns the target biblionumber if success');
1427
1428     $to_biblionumber_after_move = $item1->move_to_biblio($target_biblio);
1429     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');
1430
1431     my $get_item1 = Koha::Items->find( $item1->itemnumber );
1432     is($get_item1->biblionumber, $target_biblionumber, 'item1 is moved');
1433     my $get_item2 = Koha::Items->find( $item2->itemnumber );
1434     is($get_item2->biblionumber, $source_biblionumber, 'item2 is not moved');
1435     my $get_item3 = Koha::Items->find( $item3->itemnumber );
1436     is($get_item3->biblionumber, $source_biblionumber, 'item3 is not moved');
1437
1438     $aq_order1->discard_changes;
1439     $aq_order2->discard_changes;
1440     is($aq_order1->biblionumber, $target_biblionumber, 'move_to_biblio moves aq_orders for item 1');
1441     is($aq_order2->biblionumber, $source_biblionumber, 'move_to_biblio does not move aq_orders for item 2');
1442
1443     $bib_level_hold->discard_changes;
1444     $item_level_hold1->discard_changes;
1445     $item_level_hold2->discard_changes;
1446     is($bib_level_hold->biblionumber,   $source_biblionumber, 'move_to_biblio does not move the biblio-level hold');
1447     is($item_level_hold1->biblionumber, $target_biblionumber, 'move_to_biblio moves the item-level hold placed on item 1');
1448     is($item_level_hold2->biblionumber, $source_biblionumber, 'move_to_biblio does not move the item-level hold placed on item 2');
1449
1450     my $get_tmp_holdsqueue1 = $schema->resultset('TmpHoldsqueue')->search({ itemnumber => $tmp_holdsqueue1->{itemnumber} })->single;
1451     my $get_tmp_holdsqueue2 = $schema->resultset('TmpHoldsqueue')->search({ itemnumber => $tmp_holdsqueue2->{itemnumber} })->single;
1452     is($get_tmp_holdsqueue1->biblionumber->biblionumber, $target_biblionumber, 'move_to_biblio moves tmp_holdsqueue for item 1');
1453     is($get_tmp_holdsqueue2->biblionumber->biblionumber, $source_biblionumber, 'move_to_biblio does not move tmp_holdsqueue for item 2');
1454
1455     my $get_hold_fill_target1 = $schema->resultset('HoldFillTarget')->search({ itemnumber => $hold_fill_target1->{itemnumber} })->single;
1456     my $get_hold_fill_target2 = $schema->resultset('HoldFillTarget')->search({ itemnumber => $hold_fill_target2->{itemnumber} })->single;
1457     # Why does ->biblionumber return a Biblio object???
1458     is($get_hold_fill_target1->biblionumber->biblionumber, $target_biblionumber, 'move_to_biblio moves hold_fill_targets for item 1');
1459     is($get_hold_fill_target2->biblionumber->biblionumber, $source_biblionumber, 'move_to_biblio does not move hold_fill_targets for item 2');
1460
1461     my $get_linktracker1 = $schema->resultset('Linktracker')->search({ itemnumber => $linktracker1->{itemnumber} })->single;
1462     my $get_linktracker2 = $schema->resultset('Linktracker')->search({ itemnumber => $linktracker2->{itemnumber} })->single;
1463     is($get_linktracker1->biblionumber->biblionumber, $target_biblionumber, 'move_to_biblio moves linktracker for item 1');
1464     is($get_linktracker2->biblionumber->biblionumber, $source_biblionumber, 'move_to_biblio does not move linktracker for item 2');
1465
1466     $schema->storage->txn_rollback;
1467 };
1468
1469 subtest 'columns_to_str' => sub {
1470     plan tests => 4;
1471
1472     $schema->storage->txn_begin;
1473
1474     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField( "items.itemnumber" );
1475
1476     my $cache = Koha::Caches->get_instance();
1477     $cache->clear_from_cache("MarcStructure-0-");
1478     $cache->clear_from_cache("MarcStructure-1-");
1479     $cache->clear_from_cache("MarcSubfieldStructure-");
1480     $cache->clear_from_cache("libraries:name");
1481     $cache->clear_from_cache("itemtype:description:en");
1482     $cache->clear_from_cache("cn_sources:description");
1483     $cache->clear_from_cache("AV_descriptions:LOST");
1484
1485     # Creating subfields 'é', 'è' that are not linked with a kohafield
1486     Koha::MarcSubfieldStructures->search(
1487         {
1488             frameworkcode => '',
1489             tagfield => $itemtag,
1490             tagsubfield => ['é', 'è'],
1491         }
1492     )->delete;    # In case it exist already
1493
1494     # Ã© is not linked with a AV
1495     # Ã¨ is linked with AV branches
1496     Koha::MarcSubfieldStructure->new(
1497         {
1498             frameworkcode => '',
1499             tagfield      => $itemtag,
1500             tagsubfield   => 'é',
1501             kohafield     => undef,
1502             repeatable    => 1,
1503             defaultvalue  => 'ééé',
1504             tab           => 10,
1505         }
1506     )->store;
1507     Koha::MarcSubfieldStructure->new(
1508         {
1509             frameworkcode    => '',
1510             tagfield         => $itemtag,
1511             tagsubfield      => 'è',
1512             kohafield        => undef,
1513             repeatable       => 1,
1514             defaultvalue     => 'èèè',
1515             tab              => 10,
1516             authorised_value => 'branches',
1517         }
1518     )->store;
1519
1520     my $biblio = $builder->build_sample_biblio({ frameworkcode => '' });
1521     my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1522     my $lost_av = $builder->build_object({ class => 'Koha::AuthorisedValues', value => { category => 'LOST', authorised_value => '42' }});
1523     my $dateaccessioned = '2020-12-15';
1524     my $library = Koha::Libraries->search->next;
1525     my $branchcode = $library->branchcode;
1526
1527     my $some_marc_xml = qq{<?xml version="1.0" encoding="UTF-8"?>
1528 <collection
1529   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1530   xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1531   xmlns="http://www.loc.gov/MARC21/slim">
1532
1533 <record>
1534   <leader>         a              </leader>
1535   <datafield tag="999" ind1=" " ind2=" ">
1536     <subfield code="é">value Ã©</subfield>
1537     <subfield code="è">$branchcode</subfield>
1538   </datafield>
1539 </record>
1540
1541 </collection>};
1542
1543     $item->update(
1544         {
1545             itemlost           => $lost_av->authorised_value,
1546             dateaccessioned    => $dateaccessioned,
1547             more_subfields_xml => $some_marc_xml,
1548         }
1549     );
1550
1551     Koha::Caches->get_instance->flush_all;
1552
1553     $item = $item->get_from_storage;
1554
1555     my $s = $item->columns_to_str;
1556     is( $s->{itemlost}, $lost_av->lib, 'Attributes linked with AV replaced with description' );
1557     is( $s->{dateaccessioned}, '2020-12-15', 'Date attributes iso formatted');
1558     is( $s->{'é'}, 'value Ã©', 'subfield ok with more than a-Z');
1559     is( $s->{'è'}, $library->branchname );
1560
1561     $cache->clear_from_cache("MarcStructure-0-");
1562     $cache->clear_from_cache("MarcStructure-1-");
1563     $cache->clear_from_cache("MarcSubfieldStructure-");
1564     $cache->clear_from_cache("libraries:name");
1565     $cache->clear_from_cache("itemtype:description:en");
1566     $cache->clear_from_cache("cn_sources:description");
1567     $cache->clear_from_cache("AV_descriptions:LOST");
1568
1569     $schema->storage->txn_rollback;
1570 };
1571
1572 subtest 'strings_map() tests' => sub {
1573
1574     plan tests => 6;
1575
1576     $schema->storage->txn_begin;
1577
1578     my ( $itemtag, $itemsubfield ) = C4::Biblio::GetMarcFromKohaField("items.itemnumber");
1579
1580     my $cache = Koha::Caches->get_instance();
1581     $cache->clear_from_cache("MarcStructure-0-");
1582     $cache->clear_from_cache("MarcStructure-1-");
1583     $cache->clear_from_cache("MarcSubfieldStructure-");
1584     $cache->clear_from_cache("libraries:name");
1585     $cache->clear_from_cache("itemtype:description:en");
1586     $cache->clear_from_cache("cn_sources:description");
1587     $cache->clear_from_cache("AV_descriptions:LOST");
1588
1589     # Recreating subfields just to be sure tests will be ok
1590     # 1 => av (LOST)
1591     # 3 => no link
1592     # a => branches
1593     # y => itemtypes
1594     Koha::MarcSubfieldStructures->search(
1595         {
1596             frameworkcode => '',
1597             tagfield      => $itemtag,
1598             tagsubfield   => [ '1', '2', '3', 'a', 'y' ],
1599         }
1600     )->delete;    # In case it exist already
1601
1602     Koha::MarcSubfieldStructure->new(
1603         {
1604             authorised_value => 'LOST',
1605             defaultvalue     => '',
1606             frameworkcode    => '',
1607             kohafield        => 'items.itemlost',
1608             repeatable       => 1,
1609             tab              => 10,
1610             tagfield         => $itemtag,
1611             tagsubfield      => '1',
1612         }
1613     )->store;
1614     Koha::MarcSubfieldStructure->new(
1615         {
1616             authorised_value => 'cn_source',
1617             defaultvalue     => '',
1618             frameworkcode    => '',
1619             kohafield        => 'items.cn_source',
1620             repeatable       => 1,
1621             tab              => 10,
1622             tagfield         => $itemtag,
1623             tagsubfield      => '2',
1624         }
1625     )->store;
1626     Koha::MarcSubfieldStructure->new(
1627         {
1628             authorised_value => '',
1629             defaultvalue     => '',
1630             frameworkcode    => '',
1631             kohafield        => 'items.materials',
1632             repeatable       => 1,
1633             tab              => 10,
1634             tagfield         => $itemtag,
1635             tagsubfield      => '3',
1636         }
1637     )->store;
1638     Koha::MarcSubfieldStructure->new(
1639         {
1640             authorised_value => 'branches',
1641             defaultvalue     => '',
1642             frameworkcode    => '',
1643             kohafield        => 'items.homebranch',
1644             repeatable       => 1,
1645             tab              => 10,
1646             tagfield         => $itemtag,
1647             tagsubfield      => 'a',
1648         }
1649     )->store;
1650     Koha::MarcSubfieldStructure->new(
1651         {
1652             authorised_value => 'itemtypes',
1653             defaultvalue     => '',
1654             frameworkcode    => '',
1655             kohafield        => 'items.itype',
1656             repeatable       => 1,
1657             tab              => 10,
1658             tagfield         => $itemtag,
1659             tagsubfield      => 'y',
1660         }
1661     )->store;
1662
1663     my $itype   = $builder->build_object( { class => 'Koha::ItemTypes' } );
1664     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1665     my $biblio  = $builder->build_sample_biblio( { frameworkcode => '' } );
1666     my $item    = $builder->build_sample_item(
1667         {
1668             biblionumber => $biblio->id,
1669             library      => $library->id
1670         }
1671     );
1672
1673     Koha::AuthorisedValues->search( { authorised_value => 3, category => 'LOST' } )->delete;
1674     my $lost_av = $builder->build_object(
1675         {
1676             class => 'Koha::AuthorisedValues',
1677             value => {
1678                 authorised_value => 3,
1679                 category         => 'LOST',
1680                 lib              => 'internal description',
1681                 lib_opac         => 'public description',
1682             }
1683         }
1684     );
1685
1686     my $class_sort_rule  = $builder->build_object( { class => 'Koha::ClassSortRules', value => { sort_routine => 'Generic' } } );
1687     my $class_split_rule = $builder->build_object( { class => 'Koha::ClassSplitRules' } );
1688     my $class_source     = $builder->build_object(
1689         {
1690             class => 'Koha::ClassSources',
1691             value => {
1692                 class_sort_rule  => $class_sort_rule->class_sort_rule,
1693                 class_split_rule => $class_split_rule->class_split_rule,
1694             }
1695         }
1696     )->store();
1697
1698     Koha::Caches->get_instance->flush_all;
1699
1700     $item->set(
1701         {
1702             cn_source => $class_source->id,
1703             itemlost  => $lost_av->authorised_value,
1704             itype     => $itype->itemtype,
1705             materials => 'Suff',
1706         }
1707     )->store->discard_changes;
1708
1709     my $strings = $item->strings_map;
1710
1711     subtest 'unmapped field tests' => sub {
1712
1713         plan tests => 1;
1714
1715         ok( !exists $strings->{materials}, "Unmapped field not present" );
1716     };
1717
1718     subtest 'av handling' => sub {
1719
1720         plan tests => 4;
1721
1722         ok( exists $strings->{itemlost}, "'itemlost' entry exists" );
1723         is( $strings->{itemlost}->{str},      $lost_av->lib, "'str' set to av->lib" );
1724         is( $strings->{itemlost}->{type},     'av',          "'type' is 'av'" );
1725         is( $strings->{itemlost}->{category}, 'LOST',        "'category' exists and set to 'LOST'" );
1726     };
1727
1728     subtest 'cn_source handling' => sub {
1729
1730         plan tests => 3;
1731
1732         ok( exists $strings->{cn_source}, "'cn_source' entry exists" );
1733         is( $strings->{cn_source}->{str},  $class_source->description,    "'str' set to \$class_source->description" );
1734         is( $strings->{cn_source}->{type}, 'call_number_source', "type is 'library'" );
1735     };
1736
1737     subtest 'branches handling' => sub {
1738
1739         plan tests => 3;
1740
1741         ok( exists $strings->{homebranch}, "'homebranch' entry exists" );
1742         is( $strings->{homebranch}->{str},  $library->branchname, "'str' set to 'branchname'" );
1743         is( $strings->{homebranch}->{type}, 'library',            "type is 'library'" );
1744     };
1745
1746     subtest 'itemtype handling' => sub {
1747
1748         plan tests => 3;
1749
1750         ok( exists $strings->{itype}, "'itype' entry exists" );
1751         is( $strings->{itype}->{str},  $itype->description, "'str' correctly set" );
1752         is( $strings->{itype}->{type}, 'item_type',         "'type' is 'item_type'" );
1753     };
1754
1755     subtest 'public flag tests' => sub {
1756
1757         plan tests => 4;
1758
1759         $strings = $item->strings_map( { public => 1 } );
1760
1761         ok( exists $strings->{itemlost}, "'itemlost' entry exists" );
1762         is( $strings->{itemlost}->{str},      $lost_av->lib_opac, "'str' set to av->lib" );
1763         is( $strings->{itemlost}->{type},     'av',               "'type' is 'av'" );
1764         is( $strings->{itemlost}->{category}, 'LOST',             "'category' exists and set to 'LOST'" );
1765     };
1766
1767     $cache->clear_from_cache("MarcStructure-0-");
1768     $cache->clear_from_cache("MarcStructure-1-");
1769     $cache->clear_from_cache("MarcSubfieldStructure-");
1770     $cache->clear_from_cache("libraries:name");
1771     $cache->clear_from_cache("itemtype:description:en");
1772     $cache->clear_from_cache("cn_sources:description");
1773
1774     $schema->storage->txn_rollback;
1775 };
1776
1777 subtest 'store() tests' => sub {
1778
1779     plan tests => 3;
1780
1781     subtest 'dateaccessioned handling' => sub {
1782
1783         plan tests => 3;
1784
1785         $schema->storage->txn_begin;
1786
1787         my $item = $builder->build_sample_item;
1788
1789         ok( defined $item->dateaccessioned, 'dateaccessioned is set' );
1790
1791         # reset dateaccessioned on the DB
1792         $schema->resultset('Item')->find({ itemnumber => $item->id })->update({ dateaccessioned => undef });
1793         $item->discard_changes;
1794
1795         ok( !defined $item->dateaccessioned );
1796
1797         # update something
1798         $item->replacementprice(100)->store->discard_changes;
1799
1800         ok( !defined $item->dateaccessioned, 'dateaccessioned not set on update if undefined' );
1801
1802         $schema->storage->txn_rollback;
1803     };
1804
1805     subtest '_set_found_trigger() tests' => sub {
1806
1807         plan tests => 9;
1808
1809         $schema->storage->txn_begin;
1810
1811         my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1812         my $item   = $builder->build_sample_item({ itemlost => 1, itemlost_on => dt_from_string() });
1813
1814         # Add a lost item debit
1815         my $debit = $patron->account->add_debit(
1816             {
1817                 amount    => 10,
1818                 type      => 'LOST',
1819                 item_id   => $item->id,
1820                 interface => 'intranet',
1821             }
1822         );
1823
1824         # Add a lost item processing fee
1825         my $processing_debit = $patron->account->add_debit(
1826             {
1827                 amount    => 2,
1828                 type      => 'PROCESSING',
1829                 item_id   => $item->id,
1830                 interface => 'intranet',
1831             }
1832         );
1833
1834         my $lostreturn_policy = {
1835             lostreturn       => 'charge',
1836             processingreturn => 'refund'
1837         };
1838
1839         my $mocked_circ_rules = Test::MockModule->new('Koha::CirculationRules');
1840         $mocked_circ_rules->mock( 'get_lostreturn_policy', sub { return $lostreturn_policy; } );
1841
1842         # simulate it was found
1843         $item->set( { itemlost => 0 } )->store;
1844
1845         my $messages = $item->object_messages;
1846
1847         my $message_1 = $messages->[0];
1848
1849         is( $message_1->type,    'info',          'type is correct' );
1850         is( $message_1->message, 'lost_refunded', 'message is correct' );
1851
1852         # Find the refund credit
1853         my $credit = $debit->credits->next;
1854
1855         is_deeply(
1856             $message_1->payload,
1857             { credit_id => $credit->id },
1858             'type is correct'
1859         );
1860
1861         my $message_2 = $messages->[1];
1862
1863         is( $message_2->type,    'info',        'type is correct' );
1864         is( $message_2->message, 'lost_charge', 'message is correct' );
1865         is( $message_2->payload, undef,         'no payload' );
1866
1867         my $message_3 = $messages->[2];
1868         is( $message_3->message, 'processing_refunded', 'message is correct' );
1869
1870         my $processing_credit = $processing_debit->credits->next;
1871         is_deeply(
1872             $message_3->payload,
1873             { credit_id => $processing_credit->id },
1874             'type is correct'
1875         );
1876
1877         # Let's build a new item
1878         $item   = $builder->build_sample_item({ itemlost => 1, itemlost_on => dt_from_string() });
1879         $item->set( { itemlost => 0 } )->store;
1880
1881         $messages = $item->object_messages;
1882         is( scalar @{$messages}, 0, 'This item has no history, no associated lost fines, presumed not lost by patron, no messages returned');
1883
1884         $schema->storage->txn_rollback;
1885     };
1886
1887     subtest 'holds_queue update tests' => sub {
1888
1889         plan tests => 2;
1890
1891         $schema->storage->txn_begin;
1892
1893         my $biblio = $builder->build_sample_biblio;
1894
1895         my $mock = Test::MockModule->new('Koha::BackgroundJob::BatchUpdateBiblioHoldsQueue');
1896         $mock->mock( 'enqueue', sub {
1897             my ( $self, $args ) = @_;
1898             is_deeply(
1899                 $args->{biblio_ids},
1900                 [ $biblio->id ],
1901                 '->store triggers a holds queue update for the related biblio'
1902             );
1903         } );
1904
1905         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 1 );
1906
1907         # new item
1908         my $item = $builder->build_sample_item({ biblionumber => $biblio->id });
1909
1910         # updated item
1911         $item->set({ reserves => 1 })->store;
1912
1913         t::lib::Mocks::mock_preference( 'RealTimeHoldsQueue', 0 );
1914         # updated item
1915         $item->set({ reserves => 0 })->store;
1916
1917         $schema->storage->txn_rollback;
1918     };
1919 };
1920
1921 subtest 'Recalls tests' => sub {
1922
1923     plan tests => 23;
1924
1925     $schema->storage->txn_begin;
1926
1927     my $item1 = $builder->build_sample_item;
1928     my $biblio = $item1->biblio;
1929     my $branchcode = $item1->holdingbranch;
1930     my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
1931     my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
1932     my $patron3 = $builder->build_object({ class => 'Koha::Patrons', value => { branchcode => $branchcode } });
1933     my $item2 = $builder->build_object(
1934         {   class => 'Koha::Items',
1935             value => { holdingbranch => $branchcode, homebranch => $branchcode, biblionumber => $biblio->biblionumber, itype => $item1->effective_itemtype }
1936         }
1937     );
1938
1939     t::lib::Mocks::mock_userenv( { patron => $patron1 } );
1940     t::lib::Mocks::mock_preference('UseRecalls', 1);
1941
1942     my $recall1 = Koha::Recall->new(
1943         {   patron_id         => $patron1->borrowernumber,
1944             created_date      => \'NOW()',
1945             biblio_id         => $biblio->biblionumber,
1946             pickup_library_id => $branchcode,
1947             item_id           => $item1->itemnumber,
1948             expiration_date   => undef,
1949             item_level        => 1
1950         }
1951     )->store;
1952     my $recall2 = Koha::Recall->new(
1953         {   patron_id         => $patron2->borrowernumber,
1954             created_date      => \'NOW()',
1955             biblio_id         => $biblio->biblionumber,
1956             pickup_library_id => $branchcode,
1957             item_id           => $item1->itemnumber,
1958             expiration_date   => undef,
1959             item_level        => 1
1960         }
1961     )->store;
1962
1963     is( $item1->recall->patron_id, $patron1->borrowernumber, 'Correctly returns most relevant recall' );
1964     is( $item2->recall,            undef,                    'Other items are not returned for item-level recalls' );
1965
1966     $recall2->set_cancelled;
1967
1968     t::lib::Mocks::mock_preference('UseRecalls', 0);
1969     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall with UseRecalls disabled" );
1970
1971     t::lib::Mocks::mock_preference("UseRecalls", 1);
1972
1973     $item1->update({ notforloan => 1 });
1974     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is not for loan" );
1975     $item1->update({ notforloan => 0, itemlost => 1 });
1976     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is marked lost" );
1977     $item1->update({ itemlost => 0, withdrawn => 1 });
1978     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall that is withdrawn" );
1979     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall item if not checked out" );
1980
1981     $item1->update({ withdrawn => 0 });
1982     C4::Circulation::AddIssue( $patron2, $item1->barcode );
1983
1984     Koha::CirculationRules->set_rules({
1985         branchcode => $branchcode,
1986         categorycode => $patron1->categorycode,
1987         itemtype => $item1->effective_itemtype,
1988         rules => {
1989             recalls_allowed => 0,
1990             recalls_per_record => 1,
1991             on_shelf_recalls => 'all',
1992         },
1993     });
1994     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if recalls_allowed = 0" );
1995
1996     Koha::CirculationRules->set_rules({
1997         branchcode => $branchcode,
1998         categorycode => $patron1->categorycode,
1999         itemtype => $item1->effective_itemtype,
2000         rules => {
2001             recalls_allowed => 1,
2002             recalls_per_record => 1,
2003             on_shelf_recalls => 'all',
2004         },
2005     });
2006     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_allowed" );
2007     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has more existing recall(s) than recalls_per_record" );
2008     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if patron has already recalled this item" );
2009
2010     my $reserve_id = C4::Reserves::AddReserve({ branchcode => $branchcode, borrowernumber => $patron1->borrowernumber, biblionumber => $item1->biblionumber, itemnumber => $item1->itemnumber });
2011     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall item if patron has already reserved it" );
2012     C4::Reserves::ModReserve({ rank => 'del', reserve_id => $reserve_id, branchcode => $branchcode, itemnumber => $item1->itemnumber, borrowernumber => $patron1->borrowernumber, biblionumber => $item1->biblionumber });
2013
2014     $recall1->set_cancelled;
2015     is( $item1->can_be_recalled({ patron => $patron2 }), 0, "Can't recall if patron has already checked out an item attached to this biblio" );
2016
2017     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if on_shelf_recalls = all and items are still available" );
2018
2019     Koha::CirculationRules->set_rules({
2020         branchcode => $branchcode,
2021         categorycode => $patron1->categorycode,
2022         itemtype => $item1->effective_itemtype,
2023         rules => {
2024             recalls_allowed => 1,
2025             recalls_per_record => 1,
2026             on_shelf_recalls => 'any',
2027         },
2028     });
2029     C4::Circulation::AddReturn( $item1->barcode, $branchcode );
2030     is( $item1->can_be_recalled({ patron => $patron1 }), 0, "Can't recall if no items are checked out" );
2031
2032     C4::Circulation::AddIssue( $patron2, $item1->barcode );
2033     is( $item1->can_be_recalled({ patron => $patron1 }), 1, "Can recall item" );
2034
2035     $recall1 = Koha::Recall->new(
2036         {   patron_id         => $patron1->borrowernumber,
2037             created_date      => \'NOW()',
2038             biblio_id         => $biblio->biblionumber,
2039             pickup_library_id => $branchcode,
2040             item_id           => undef,
2041             expiration_date   => undef,
2042             item_level        => 0
2043         }
2044     )->store;
2045
2046     # Patron2 has Item1 checked out. Patron1 has placed a biblio-level recall on Biblio1, so check if Item1 can fulfill Patron1's recall.
2047
2048     Koha::CirculationRules->set_rules({
2049         branchcode => $branchcode,
2050         categorycode => $patron1->categorycode,
2051         itemtype => $item1->effective_itemtype,
2052         rules => {
2053             recalls_allowed => 0,
2054             recalls_per_record => 1,
2055             on_shelf_recalls => 'any',
2056         },
2057     });
2058     is( $item1->can_be_waiting_recall, 0, "Recalls not allowed for this itemtype" );
2059
2060     Koha::CirculationRules->set_rules({
2061         branchcode => $branchcode,
2062         categorycode => $patron1->categorycode,
2063         itemtype => $item1->effective_itemtype,
2064         rules => {
2065             recalls_allowed => 1,
2066             recalls_per_record => 1,
2067             on_shelf_recalls => 'any',
2068         },
2069     });
2070     is( $item1->can_be_waiting_recall, 1, "Recalls are allowed for this itemtype" );
2071
2072     # check_recalls tests
2073
2074     $recall1 = Koha::Recall->new(
2075         {   patron_id         => $patron2->borrowernumber,
2076             created_date      => \'NOW()',
2077             biblio_id         => $biblio->biblionumber,
2078             pickup_library_id => $branchcode,
2079             item_id           => $item1->itemnumber,
2080             expiration_date   => undef,
2081             item_level        => 1
2082         }
2083     )->store;
2084     $recall2 = Koha::Recall->new(
2085         {   patron_id         => $patron1->borrowernumber,
2086             created_date      => \'NOW()',
2087             biblio_id         => $biblio->biblionumber,
2088             pickup_library_id => $branchcode,
2089             item_id           => undef,
2090             expiration_date   => undef,
2091             item_level        => 0
2092         }
2093     )->store;
2094     $recall2->set_waiting( { item => $item1 } );
2095     is( $item1->has_pending_recall, 1, 'Item has pending recall' );
2096
2097     # return a waiting recall
2098     my $check_recall = $item1->check_recalls;
2099     is( $check_recall->patron_id, $patron1->borrowernumber, "Waiting recall is highest priority and returned" );
2100
2101     $recall2->revert_waiting;
2102
2103     is( $item1->has_pending_recall, 0, 'Item does not have pending recall' );
2104
2105     # return recall based on recalldate
2106     $check_recall = $item1->check_recalls;
2107     is( $check_recall->patron_id, $patron1->borrowernumber, "No waiting recall, so oldest recall is returned" );
2108
2109     $recall1->set_cancelled;
2110
2111     # return a biblio-level recall
2112     $check_recall = $item1->check_recalls;
2113     is( $check_recall->patron_id, $patron1->borrowernumber, "Only remaining recall is returned" );
2114
2115     $recall2->set_cancelled;
2116
2117     $schema->storage->txn_rollback;
2118 };
2119
2120 subtest 'Notforloan tests' => sub {
2121
2122     plan tests => 3;
2123
2124     $schema->storage->txn_begin;
2125
2126     my $item1 = $builder->build_sample_item;
2127     $item1->update({ notforloan => 0 });
2128     $item1->itemtype->notforloan(0);
2129     is ( $item1->is_notforloan, 0, 'Notforloan is correctly false by item status and item type');
2130     $item1->update({ notforloan => 1 });
2131     is ( $item1->is_notforloan, 1, 'Notforloan is correctly true by item status');
2132     $item1->update({ notforloan => 0 });
2133     $item1->itemtype->update({ notforloan => 1 });
2134     is ( $item1->is_notforloan, 1, 'Notforloan is correctly true by item type');
2135
2136     $schema->storage->txn_rollback;
2137 };
2138
2139 subtest 'item_group() tests' => sub {
2140
2141     plan tests => 4;
2142
2143     $schema->storage->txn_begin;
2144
2145     my $biblio = $builder->build_sample_biblio();
2146     my $item_1 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
2147     my $item_2 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
2148
2149     is( $item_1->item_group, undef, 'Item 1 has no item group');
2150     is( $item_2->item_group, undef, 'Item 2 has no item group');
2151
2152     my $item_group_1 = Koha::Biblio::ItemGroup->new( { biblio_id => $biblio->id } )->store();
2153     my $item_group_2 = Koha::Biblio::ItemGroup->new( { biblio_id => $biblio->id } )->store();
2154
2155     $item_group_1->add_item({ item_id => $item_1->id });
2156     $item_group_2->add_item({ item_id => $item_2->id });
2157
2158     is( $item_1->item_group->id, $item_group_1->id, 'Got item group 1 correctly' );
2159     is( $item_2->item_group->id, $item_group_2->id, 'Got item group 2 correctly' );
2160
2161     $schema->storage->txn_rollback;
2162 };
2163
2164 subtest 'has_pending_recall() tests' => sub {
2165
2166     plan tests => 2;
2167
2168     $schema->storage->txn_begin;
2169
2170     my $library = $builder->build_object({ class => 'Koha::Libraries' });
2171     my $item    = $builder->build_sample_item;
2172     my $patron  = $builder->build_object({ class => 'Koha::Patrons' });
2173
2174     t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
2175     t::lib::Mocks::mock_preference( 'UseRecalls', 1 );
2176
2177     C4::Circulation::AddIssue( $patron, $item->barcode );
2178
2179     my ($recall) = Koha::Recalls->add_recall({ biblio => $item->biblio, item => $item, patron => $patron });
2180
2181     ok( !$item->has_pending_recall, 'The item has no pending recalls' );
2182
2183     $recall->status('waiting')->store;
2184
2185     ok( $item->has_pending_recall, 'The item has a pending recall' );
2186
2187     $schema->storage->txn_rollback;
2188 };
2189
2190 subtest 'is_denied_renewal' => sub {
2191     plan tests => 11;
2192
2193     $schema->storage->txn_begin;
2194
2195     my $library = $builder->build_object({ class => 'Koha::Libraries'});
2196
2197     my $deny_book = $builder->build_object({ class => 'Koha::Items', value => {
2198         homebranch => $library->branchcode,
2199         withdrawn => 1,
2200         itype => 'HIDE',
2201         location => 'PROC',
2202         itemcallnumber => undef,
2203         itemnotes => "",
2204         }
2205     });
2206
2207     my $allow_book = $builder->build_object({ class => 'Koha::Items', value => {
2208         homebranch => $library->branchcode,
2209         withdrawn => 0,
2210         itype => 'NOHIDE',
2211         location => 'NOPROC'
2212         }
2213     });
2214
2215     my $idr_rules = "";
2216     C4::Context->set_preference('ItemsDeniedRenewal', $idr_rules);
2217     is( $deny_book->is_denied_renewal, 0, 'Renewal allowed when no rules' );
2218
2219     # The wrong column delete should be silently ignored and not trigger item delete
2220     $idr_rules="delete: [yes]\nwithdrawn: [1]";
2221     C4::Context->set_preference('ItemsDeniedRenewal', $idr_rules);
2222     is( $deny_book->is_denied_renewal, 1, 'Renewal blocked when 1 rules (withdrawn)' );
2223     is( $allow_book->is_denied_renewal, 0, 'Renewal allowed when 1 rules not matched (withdrawn)' );
2224
2225     $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]";
2226     is( $deny_book->is_denied_renewal, 1, 'Renewal blocked when 2 rules matched (withdrawn, itype)' );
2227     is( $allow_book->is_denied_renewal, 0, 'Renewal allowed when 2 rules not matched (withdrawn, itype)' );
2228
2229     $idr_rules="withdrawn: [1]\nitype: [HIDE,INVISIBLE]\nlocation: [PROC]";
2230     C4::Context->set_preference('ItemsDeniedRenewal', $idr_rules);
2231     is( $deny_book->is_denied_renewal, 1, 'Renewal blocked when 3 rules matched (withdrawn, itype, location)' );
2232     is( $allow_book->is_denied_renewal, 0, 'Renewal allowed when 3 rules not matched (withdrawn, itype, location)' );
2233
2234     $idr_rules="itemcallnumber: [null]";
2235     C4::Context->set_preference('ItemsDeniedRenewal', $idr_rules);
2236     is( $deny_book->is_denied_renewal, 1, 'Renewal blocked for undef when null in pref' );
2237
2238     $idr_rules="itemcallnumber: ['']";
2239     C4::Context->set_preference('ItemsDeniedRenewal', $idr_rules);
2240     is( $deny_book->is_denied_renewal, 0, 'Renewal not blocked for undef when "" in pref' );
2241
2242     $idr_rules="itemnotes: [null]";
2243     C4::Context->set_preference('ItemsDeniedRenewal', $idr_rules);
2244     is( $deny_book->is_denied_renewal, 0, 'Renewal not blocked for "" when null in pref' );
2245
2246     $idr_rules="itemnotes: ['']";
2247     C4::Context->set_preference('ItemsDeniedRenewal', $idr_rules);
2248     is( $deny_book->is_denied_renewal, 1, 'Renewal blocked for empty string when "" in pref' );
2249
2250     $schema->storage->txn_rollback;
2251 };
2252
2253 subtest 'current_branchtransfers relationship' => sub {
2254     plan tests => 3;
2255
2256     $schema->storage->txn_begin;
2257
2258     my $biblio = $builder->build_sample_biblio();
2259     my $item   = $builder->build_sample_item(
2260         {
2261             biblionumber => $biblio->biblionumber,
2262         }
2263     );
2264     my $transfers = $item->_result->current_branchtransfers;
2265     is( ref($transfers), 'DBIx::Class::ResultSet',
2266         'current_branchtransfers returns a DBIx::Class::ResultSet' );
2267     is( $transfers->count, 0,
2268         "Empty Koha::Item::Transfers set returned if no return_claims" );
2269     my $transfer1 = $builder->build(
2270         {
2271             source => 'Branchtransfer',
2272             value  => {
2273                 itemnumber  => $item->itemnumber,
2274                 datearrived => dt_from_string,
2275             }
2276         }
2277     );
2278     my $transfer2 = $builder->build(
2279         {
2280             source => 'Branchtransfer',
2281             value  => {
2282                 itemnumber    => $item->itemnumber,
2283                 datearrived   => undef,
2284                 datecancelled => dt_from_string,
2285             }
2286         }
2287     );
2288     my $transfer3 = $builder->build(
2289         {
2290             source => 'Branchtransfer',
2291             value  => {
2292                 itemnumber    => $item->itemnumber,
2293                 datearrived   => undef,
2294                 datecancelled => undef,
2295             }
2296         }
2297     );
2298
2299     is( $item->_result->current_branchtransfers()->count,
2300         1, "One transfer found for item" );
2301
2302     $schema->storage->txn_rollback;
2303 };
2304
2305 subtest 'location_update_trigger() tests' => sub {
2306
2307     plan tests => 10;
2308
2309     $schema->storage->txn_begin;
2310
2311     my $location           = 'YA';
2312     my $permanent_location = 'HEY';
2313
2314     foreach my $action (qw{ checkin checkout }) {
2315
2316         my $pref = $action eq 'checkin' ? 'UpdateItemLocationOnCheckin' : 'UpdateItemLocationOnCheckout';
2317
2318         t::lib::Mocks::mock_preference( 'UpdateItemLocationOnCheckin',  q{} );
2319         t::lib::Mocks::mock_preference( 'UpdateItemLocationOnCheckout', q{} );
2320
2321         my $item = $builder->build_sample_item( { location => $location, permanent_location => $permanent_location } );
2322
2323         $item->location_update_trigger($action);
2324
2325         is( $item->location, $location, "$pref does not modify value when not enabled" );
2326
2327         t::lib::Mocks::mock_preference( $pref, qq{$location: GEN} );
2328
2329         $item->location_update_trigger($action);
2330
2331         is( $item->location, 'GEN', qq{'location' value set from '$location' to 'GEN' with setting `$location: GEN`} );
2332
2333         t::lib::Mocks::mock_preference( $pref, q{_ALL_: BOO} );
2334
2335         $item->location_update_trigger($action);
2336
2337         is( $item->location, 'BOO', q{`_ALL_` does the job} );
2338
2339         t::lib::Mocks::mock_preference( $pref, qq{$location: _BLANK_} );
2340         $item->location($location)->store();
2341
2342         $item->location_update_trigger($action);
2343         is( $item->location, q{}, q{`_BLANK_` does the job} );
2344
2345         t::lib::Mocks::mock_preference( $pref, qq{GEN: _BLANK_\n_BLANK_: PROC\n$location: _PERM_} );
2346
2347         $item->set(
2348             {
2349                 location           => $location,
2350                 permanent_location =>
2351                     $permanent_location,    # setting `permanent_location` explicitly because ->store messes with it.
2352             }
2353         )->store;
2354
2355         $item->location_update_trigger($action);
2356
2357         is(
2358             $item->location, $permanent_location,
2359             q{_PERM_ does the job"}
2360         );
2361     }
2362
2363     $schema->storage->txn_rollback;
2364 };
2365
2366 subtest 'bookings' => sub {
2367     plan tests => 4;
2368
2369     $schema->storage->txn_begin;
2370
2371     my $biblio = $builder->build_sample_biblio();
2372     my $item   = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } );
2373     is( ref( $item->bookings() ), 'Koha::Bookings', "Koha::Item->bookings() returns a Koha::Bookings object" );
2374     is( $item->bookings->count,   0,                "Nothing returned if there are no bookings" );
2375
2376     my $start_1 = dt_from_string()->subtract( days => 7 );
2377     my $end_1   = dt_from_string()->subtract( days => 1 );
2378     my $start_2 = dt_from_string();
2379     my $end_2   = dt_from_string()->add( days => 7 );
2380     my $start_3 = dt_from_string()->add( days => 8 );
2381     my $end_3   = dt_from_string()->add( days => 16 );
2382
2383     my $booking1 = $builder->build_object(
2384         {
2385             class => 'Koha::Bookings',
2386             value => { item_id => $item->itemnumber, start_date => $start_1, end_date => $end_1 }
2387         }
2388     );
2389     my $booking2 = $builder->build_object(
2390         {
2391             class => 'Koha::Bookings',
2392             value => { item_id => $item->itemnumber, start_date => $start_2, end_date => $end_2 }
2393         }
2394     );
2395     my $booking3 = $builder->build_object(
2396         {
2397             class => 'Koha::Bookings',
2398             value => { item_id => $item->itemnumber, start_date => $start_3, end_date => $end_3 }
2399         }
2400     );
2401
2402     is( $item->bookings()->count, 3, "Three bookings found" );
2403     my $dtf = Koha::Database->new->schema->storage->datetime_parser;
2404     is(
2405         $item->bookings( { start_date => { '<=' => $dtf->format_datetime( dt_from_string() ) } } )->count, 2,
2406         "Two bookings starts on or before today"
2407     );
2408
2409     $schema->storage->txn_rollback;
2410 };
2411
2412 subtest 'find_booking' => sub {
2413     plan tests => 6;
2414
2415     $schema->storage->txn_begin;
2416
2417     my $biblio        = $builder->build_sample_biblio();
2418     my $item          = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } );
2419     my $found_booking = $item->find_booking(
2420         {
2421             checkout_date => dt_from_string(),
2422             due_date      => dt_from_string()->add( days => 7 ),
2423         }
2424     );
2425
2426     is(
2427         $found_booking, undef,
2428         "Nothing returned from Koha::Item->find_booking if there are no bookings"
2429     );
2430
2431     my $start_1 = dt_from_string()->subtract( days => 7 );
2432     my $end_1   = dt_from_string()->subtract( days => 1 );
2433     my $start_2 = dt_from_string();
2434     my $end_2   = dt_from_string()->add( days => 7 );
2435     my $start_3 = dt_from_string()->add( days => 8 );
2436     my $end_3   = dt_from_string()->add( days => 16 );
2437
2438     # Past booking
2439     my $booking1 = $builder->build_object(
2440         {
2441             class => 'Koha::Bookings',
2442             value => {
2443                 biblio_id  => $biblio->biblionumber,
2444                 item_id    => $item->itemnumber,
2445                 start_date => $start_1,
2446                 end_date   => $end_1
2447             }
2448         }
2449     );
2450
2451     $found_booking = $item->find_booking(
2452         {
2453             checkout_date => dt_from_string(),
2454             due_date      => dt_from_string()->add( days => 7 ),
2455         }
2456     );
2457
2458     is(
2459         $found_booking,
2460         undef,
2461         "Koha::Item->find_booking returns undefined if the passed dates do not conflict with any item bookings"
2462     );
2463
2464     # Current booking
2465     my $booking2 = $builder->build_object(
2466         {
2467             class => 'Koha::Bookings',
2468             value => {
2469                 biblio_id  => $biblio->biblionumber,
2470                 item_id    => $item->itemnumber,
2471                 start_date => $start_2,
2472                 end_date   => $end_2
2473             }
2474         }
2475     );
2476
2477     $found_booking = $item->find_booking(
2478         {
2479             checkout_date => dt_from_string(),
2480             due_date      => dt_from_string()->add( days => 7 ),
2481         }
2482     );
2483     is(
2484         ref($found_booking),
2485         'Koha::Booking',
2486         "Koha::Item->find_booking returns a Koha::Booking if one exists that would clash with the passed dates"
2487     );
2488     is( $found_booking->booking_id, $booking2->booking_id, "Koha::Item->find_booking returns the current booking" );
2489
2490     # Future booking
2491     my $booking3 = $builder->build_object(
2492         {
2493             class => 'Koha::Bookings',
2494             value => {
2495                 biblio_id  => $biblio->biblionumber,
2496                 item_id    => $item->itemnumber,
2497                 start_date => $start_3,
2498                 end_date   => $end_3
2499             }
2500         }
2501     );
2502
2503     $found_booking = $item->find_booking(
2504         {
2505             checkout_date => dt_from_string(),
2506             due_date      => dt_from_string()->add( days => 7 ),
2507         }
2508     );
2509
2510     is(
2511         ref($found_booking),
2512         'Koha::Booking',
2513         "Koha::Item->find_booking returns a Koha::Booking if one exists that would clash with the passed dates"
2514     );
2515     is(
2516         $found_booking->booking_id, $booking2->booking_id,
2517         "Koha::Item->find_booking returns the current booking not a future one"
2518     );
2519
2520     $schema->storage->txn_rollback;
2521 };
2522
2523 subtest 'check_booking tests' => sub {
2524     plan tests => 5;
2525
2526     $schema->storage->txn_begin;
2527
2528     my $biblio   = $builder->build_sample_biblio();
2529     my $item     = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } );
2530     my $can_book = $item->check_booking(
2531         {
2532             start_date => dt_from_string(),
2533             end_date   => dt_from_string()->add( days => 7 )
2534         }
2535     );
2536
2537     is(
2538         $can_book, 1,
2539         "True returned from Koha::Item->check_booking if there are no bookings that would clash"
2540     );
2541
2542     my $start_1 = dt_from_string()->subtract( days => 7 );
2543     my $end_1   = dt_from_string()->subtract( days => 1 );
2544     my $start_2 = dt_from_string();
2545     my $end_2   = dt_from_string()->add( days => 7 );
2546     my $start_3 = dt_from_string()->add( days => 8 );
2547     my $end_3   = dt_from_string()->add( days => 16 );
2548
2549     # Past booking
2550     my $booking1 = $builder->build_object(
2551         {
2552             class => 'Koha::Bookings',
2553             value => {
2554                 biblio_id  => $biblio->biblionumber,
2555                 item_id    => $item->itemnumber,
2556                 start_date => $start_1,
2557                 end_date   => $end_1
2558             }
2559         }
2560     );
2561
2562     $can_book = $item->check_booking(
2563         {
2564             start_date => dt_from_string(),
2565             end_date   => dt_from_string()->add( days => 7 ),
2566         }
2567     );
2568
2569     is(
2570         $can_book,
2571         1,
2572         "Koha::Item->check_booking returns true when we don't conflict with a past booking"
2573     );
2574
2575     # Current booking
2576     my $booking2 = $builder->build_object(
2577         {
2578             class => 'Koha::Bookings',
2579             value => {
2580                 biblio_id  => $biblio->biblionumber,
2581                 item_id    => $item->itemnumber,
2582                 start_date => $start_2,
2583                 end_date   => $end_2
2584             }
2585         }
2586     );
2587
2588     $can_book = $item->check_booking(
2589         {
2590             start_date => dt_from_string(),
2591             end_date   => dt_from_string()->add( days => 7 ),
2592         }
2593     );
2594     is(
2595         $can_book,
2596         0,
2597         "Koha::Item->check_booking returns false if the booking would conflict"
2598     );
2599
2600     $can_book = $item->check_booking(
2601         {
2602             start_date => dt_from_string(),
2603             end_date   => dt_from_string()->add( days => 7 ),
2604             booking_id => $booking2->booking_id
2605         }
2606     );
2607     is(
2608         $can_book,
2609         1,
2610         "Koha::Item->check_booking returns true if we pass the booking_id that would conflict"
2611     );
2612
2613     $booking2->delete();
2614
2615     # Future booking
2616     my $booking3 = $builder->build_object(
2617         {
2618             class => 'Koha::Bookings',
2619             value => {
2620                 biblio_id  => $biblio->biblionumber,
2621                 item_id    => $item->itemnumber,
2622                 start_date => $start_3,
2623                 end_date   => $end_3
2624             }
2625         }
2626     );
2627
2628     $can_book = $item->check_booking(
2629         {
2630             start_date => dt_from_string(),
2631             end_date   => dt_from_string()->add( days => 7 ),
2632         }
2633     );
2634
2635     is(
2636         $can_book,
2637         1,
2638         "Koha::Item->check_booking returns true when we don't conflict with a future booking"
2639     );
2640
2641     $schema->storage->txn_rollback;
2642 };