3 # Copyright 2019 Koha Development team
5 # This file is part of Koha
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.
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.
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>.
22 use Test::More tests => 8;
33 use List::MoreUtils qw(all);
35 use t::lib::TestBuilder;
38 my $schema = Koha::Database->new->schema;
39 my $builder = t::lib::TestBuilder->new;
41 subtest 'hidden_in_opac() tests' => sub {
45 $schema->storage->txn_begin;
47 my $item = $builder->build_sample_item({ itemlost => 2 });
50 # disable hidelostitems as it interteres with OpachiddenItems for the calculation
51 t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
53 ok( !$item->hidden_in_opac, 'No rules passed, shouldn\'t hide' );
54 ok( !$item->hidden_in_opac({ rules => $rules }), 'Empty rules passed, shouldn\'t hide' );
56 # enable hidelostitems to verify correct behaviour
57 t::lib::Mocks::mock_preference( 'hidelostitems', 1 );
58 ok( $item->hidden_in_opac, 'Even with no rules, item should hide because of hidelostitems syspref' );
60 # disable hidelostitems
61 t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
62 my $withdrawn = $item->withdrawn + 1; # make sure this attribute doesn't match
64 $rules = { withdrawn => [$withdrawn], itype => [ $item->itype ] };
66 ok( $item->hidden_in_opac({ rules => $rules }), 'Rule matching itype passed, should hide' );
70 $schema->storage->txn_rollback;
73 subtest 'has_pending_hold() tests' => sub {
77 $schema->storage->txn_begin;
79 my $dbh = C4::Context->dbh;
80 my $item = $builder->build_sample_item({ itemlost => 0 });
81 my $itemnumber = $item->itemnumber;
83 $dbh->do("INSERT INTO tmp_holdsqueue (surname,borrowernumber,itemnumber) VALUES ('Clamp',42,$itemnumber)");
84 ok( $item->has_pending_hold, "Yes, we have a pending hold");
85 $dbh->do("DELETE FROM tmp_holdsqueue WHERE itemnumber=$itemnumber");
86 ok( !$item->has_pending_hold, "We don't have a pending hold if nothing in the tmp_holdsqueue");
88 $schema->storage->txn_rollback;
91 subtest "as_marc_field() tests" => sub {
93 my $mss = C4::Biblio::GetMarcSubfieldStructure( '' );
95 my @schema_columns = $schema->resultset('Item')->result_source->columns;
96 my @mapped_columns = grep { exists $mss->{'items.'.$_} } @schema_columns;
98 plan tests => 2 * (scalar @mapped_columns + 1) + 2;
100 $schema->storage->txn_begin;
102 my $item = $builder->build_sample_item;
103 # Make sure it has at least one undefined attribute
104 $item->set({ replacementprice => undef })->store->discard_changes;
106 # Tests with the mss parameter
107 my $marc_field = $item->as_marc_field({ mss => $mss });
111 $mss->{'items.itemnumber'}[0]->{tagfield},
112 'Generated field set the right tag number'
115 foreach my $column ( @mapped_columns ) {
116 my $tagsubfield = $mss->{ 'items.' . $column }[0]->{tagsubfield};
117 is( $marc_field->subfield($tagsubfield),
118 $item->$column, "Value is mapped correctly for column $column" );
121 # Tests without the mss parameter
122 $marc_field = $item->as_marc_field();
126 $mss->{'items.itemnumber'}[0]->{tagfield},
127 'Generated field set the right tag number'
130 foreach my $column (@mapped_columns) {
131 my $tagsubfield = $mss->{ 'items.' . $column }[0]->{tagsubfield};
132 is( $marc_field->subfield($tagsubfield),
133 $item->$column, "Value is mapped correctly for column $column" );
136 my $unmapped_subfield = Koha::MarcSubfieldStructure->new(
139 tagfield => $mss->{'items.itemnumber'}[0]->{tagfield},
144 $mss = C4::Biblio::GetMarcSubfieldStructure( '' );
145 my @unlinked_subfields;
146 push @unlinked_subfields, X => 'Something weird';
147 $item->more_subfields_xml( C4::Items::_get_unlinked_subfields_xml( \@unlinked_subfields ) )->store;
149 $marc_field = $item->as_marc_field;
151 my @subfields = $marc_field->subfields;
152 my $result = all { defined $_->[1] } @subfields;
153 ok( $result, 'There are no undef subfields' );
155 is( scalar $marc_field->subfield('X'), 'Something weird', 'more_subfield_xml is considered' );
157 $schema->storage->txn_rollback;
160 subtest 'pickup_locations' => sub {
163 $schema->storage->txn_begin;
165 my $dbh = C4::Context->dbh;
167 my $root1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { ft_local_hold_group => 1, branchcode => undef } } );
168 my $root2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { ft_local_hold_group => 1, branchcode => undef } } );
169 my $library1 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, } } );
170 my $library2 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, } } );
171 my $library3 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 0, } } );
172 my $library4 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, } } );
173 my $group1_1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root1->id, branchcode => $library1->branchcode } } );
174 my $group1_2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root1->id, branchcode => $library2->branchcode } } );
176 my $group2_1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root2->id, branchcode => $library3->branchcode } } );
177 my $group2_2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root2->id, branchcode => $library4->branchcode } } );
180 $library1->branchcode, $library2->branchcode,
181 $library3->branchcode, $library4->branchcode
184 my $item1 = $builder->build_sample_item(
186 homebranch => $library1->branchcode,
187 holdingbranch => $library2->branchcode,
193 my $item3 = $builder->build_sample_item(
195 homebranch => $library3->branchcode,
196 holdingbranch => $library4->branchcode,
198 itype => $item1->itype,
202 Koha::CirculationRules->set_rules(
204 categorycode => undef,
205 itemtype => $item1->itype,
208 reservesallowed => 25,
214 my $patron1 = $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $library1->branchcode, firstname => '1' } } );
215 my $patron4 = $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $library4->branchcode, firstname => '4' } } );
219 "1-1-1-holdgroup" => 2,
220 "1-1-1-patrongroup" => 2,
221 "1-1-1-homebranch" => 1,
222 "1-1-1-holdingbranch" => 1,
224 "1-1-2-holdgroup" => 2,
225 "1-1-2-patrongroup" => 2,
226 "1-1-2-homebranch" => 1,
227 "1-1-2-holdingbranch" => 1,
229 "1-1-3-holdgroup" => 2,
230 "1-1-3-patrongroup" => 2,
231 "1-1-3-homebranch" => 1,
232 "1-1-3-holdingbranch" => 1,
234 "1-4-1-holdgroup" => 0,
235 "1-4-1-patrongroup" => 0,
236 "1-4-1-homebranch" => 0,
237 "1-4-1-holdingbranch" => 0,
239 "1-4-2-holdgroup" => 2,
240 "1-4-2-patrongroup" => 1,
241 "1-4-2-homebranch" => 1,
242 "1-4-2-holdingbranch" => 1,
244 "1-4-3-holdgroup" => 0,
245 "1-4-3-patrongroup" => 0,
246 "1-4-3-homebranch" => 0,
247 "1-4-3-holdingbranch" => 0,
249 "3-1-1-holdgroup" => 0,
250 "3-1-1-patrongroup" => 0,
251 "3-1-1-homebranch" => 0,
252 "3-1-1-holdingbranch" => 0,
254 "3-1-2-holdgroup" => 1,
255 "3-1-2-patrongroup" => 2,
256 "3-1-2-homebranch" => 0,
257 "3-1-2-holdingbranch" => 1,
259 "3-1-3-holdgroup" => 0,
260 "3-1-3-patrongroup" => 0,
261 "3-1-3-homebranch" => 0,
262 "3-1-3-holdingbranch" => 0,
264 "3-4-1-holdgroup" => 0,
265 "3-4-1-patrongroup" => 0,
266 "3-4-1-homebranch" => 0,
267 "3-4-1-holdingbranch" => 0,
269 "3-4-2-holdgroup" => 1,
270 "3-4-2-patrongroup" => 1,
271 "3-4-2-homebranch" => 0,
272 "3-4-2-holdingbranch" => 1,
274 "3-4-3-holdgroup" => 1,
275 "3-4-3-patrongroup" => 1,
276 "3-4-3-homebranch" => 0,
277 "3-4-3-holdingbranch" => 1
281 my ( $item, $patron, $ha, $hfp, $results ) = @_;
283 Koha::CirculationRules->set_rules(
289 hold_fulfillment_policy => $hfp,
290 returnbranch => 'any'
294 my $ha_value=$ha==3?'holdgroup':($ha==2?'any':'homebranch');
297 my $pickup_location = $_;
298 grep { $pickup_location->branchcode eq $_ } @branchcodes
299 } $item->pickup_locations( { patron => $patron } )->as_list;
302 scalar(@pl) == $results->{
303 $item->copynumber . '-'
304 . $patron->firstname . '-'
314 . ', hold_fulfillment_policy: '
318 $item->copynumber . '-'
319 . $patron->firstname . '-'
330 foreach my $item ($item1, $item3) {
331 foreach my $patron ($patron1, $patron4) {
332 #holdallowed 1: homebranch, 2: any, 3: holdgroup
333 foreach my $ha (1, 2, 3) {
334 foreach my $hfp ('any', 'holdgroup', 'patrongroup', 'homebranch', 'holdingbranch') {
335 _doTest($item, $patron, $ha, $hfp, $results);
341 # Now test that branchtransferlimits will further filter the pickup locations
343 my $item_no_ccode = $builder->build_sample_item(
345 homebranch => $library1->branchcode,
346 holdingbranch => $library2->branchcode,
347 itype => $item1->itype,
351 t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
352 t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
353 Koha::CirculationRules->set_rules(
356 itemtype => $item1->itype,
359 hold_fulfillment_policy => 1,
360 returnbranch => 'any'
364 $builder->build_object(
366 class => 'Koha::Item::Transfer::Limits',
368 toBranch => $library1->branchcode,
369 fromBranch => $library2->branchcode,
370 itemtype => $item1->itype,
376 my @pickup_locations = map {
377 my $pickup_location = $_;
378 grep { $pickup_location->branchcode eq $_ } @branchcodes
379 } $item1->pickup_locations( { patron => $patron1 } )->as_list;
381 is( scalar @pickup_locations, 3 - 1, "With a transfer limits we get back the libraries that are pickup locations minus 1 limited library");
383 $builder->build_object(
385 class => 'Koha::Item::Transfer::Limits',
387 toBranch => $library4->branchcode,
388 fromBranch => $library2->branchcode,
389 itemtype => $item1->itype,
395 @pickup_locations = map {
396 my $pickup_location = $_;
397 grep { $pickup_location->branchcode eq $_ } @branchcodes
398 } $item1->pickup_locations( { patron => $patron1 } )->as_list;
400 is( scalar @pickup_locations, 3 - 2, "With 2 transfer limits we get back the libraries that are pickup locations minus 2 limited libraries");
402 t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'ccode');
403 @pickup_locations = map {
404 my $pickup_location = $_;
405 grep { $pickup_location->branchcode eq $_ } @branchcodes
406 } $item1->pickup_locations( { patron => $patron1 } )->as_list;
407 is( scalar @pickup_locations, 3, "With no transfer limits of type ccode we get back the libraries that are pickup locations");
409 @pickup_locations = map {
410 my $pickup_location = $_;
411 grep { $pickup_location->branchcode eq $_ } @branchcodes
412 } $item_no_ccode->pickup_locations( { patron => $patron1 } )->as_list;
413 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");
415 $builder->build_object(
417 class => 'Koha::Item::Transfer::Limits',
419 toBranch => $library2->branchcode,
420 fromBranch => $library2->branchcode,
422 ccode => $item1->ccode,
427 @pickup_locations = map {
428 my $pickup_location = $_;
429 grep { $pickup_location->branchcode eq $_ } @branchcodes
430 } $item1->pickup_locations( { patron => $patron1 } )->as_list;
431 is( scalar @pickup_locations, 3 - 1, "With a transfer limits we get back the libraries that are pickup locations minus 1 limited library");
433 $builder->build_object(
435 class => 'Koha::Item::Transfer::Limits',
437 toBranch => $library4->branchcode,
438 fromBranch => $library2->branchcode,
440 ccode => $item1->ccode,
445 @pickup_locations = map {
446 my $pickup_location = $_;
447 grep { $pickup_location->branchcode eq $_ } @branchcodes
448 } $item1->pickup_locations( { patron => $patron1 } )->as_list;
449 is( scalar @pickup_locations, 3 - 2, "With 2 transfer limits we get back the libraries that are pickup locations minus 2 limited libraries");
451 t::lib::Mocks::mock_preference('UseBranchTransferLimits', 0);
453 $schema->storage->txn_rollback;
456 subtest 'request_transfer' => sub {
458 $schema->storage->txn_begin;
460 my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
461 my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
462 my $item = $builder->build_sample_item(
464 homebranch => $library1->branchcode,
465 holdingbranch => $library2->branchcode,
469 # Mandatory fields tests
470 throws_ok { $item->request_transfer( { to => $library1 } ) }
471 'Koha::Exceptions::MissingParameter',
472 'Exception thrown if `reason` parameter is missing';
474 throws_ok { $item->request_transfer( { reason => 'Manual' } ) }
475 'Koha::Exceptions::MissingParameter',
476 'Exception thrown if `to` parameter is missing';
479 my $transfer = $item->request_transfer({ to => $library1, reason => 'Manual' });
480 is( ref($transfer), 'Koha::Item::Transfer',
481 'Koha::Item->request_transfer should return a Koha::Item::Transfer object'
484 # Transfer already in progress
485 throws_ok { $item->request_transfer( { to => $library2, reason => 'Manual' } ) }
486 'Koha::Exceptions::Item::Transfer::Found',
487 'Exception thrown if transfer is already in progress';
490 is( ref( $exception->transfer ),
491 'Koha::Item::Transfer',
492 'The exception contains the found Koha::Item::Transfer' );
494 $transfer->datearrived(dt_from_string)->store();
496 # BranchTransferLimits
497 t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
498 t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
499 my $limit = Koha::Item::Transfer::Limit->new({
500 fromBranch => $library2->branchcode,
501 toBranch => $library1->branchcode,
502 itemtype => $item->effective_itemtype,
505 throws_ok { $item->request_transfer( { to => $library1, reason => 'Manual' } ) }
506 'Koha::Exceptions::Item::Transfer::Limit',
507 'Exception thrown if transfer is prevented by limits';
509 my $forced_transfer = $item->request_transfer( { to => $library1, reason => 'Manual', ignore_limits => 1 } );
510 is( ref($forced_transfer), 'Koha::Item::Transfer',
511 'Koha::Item->request_transfer allowed when forced'
514 $schema->storage->txn_rollback;
517 subtest 'deletion' => sub {
520 $schema->storage->txn_begin;
522 my $biblio = $builder->build_sample_biblio();
524 my $item = $builder->build_sample_item(
526 biblionumber => $biblio->biblionumber,
530 is( ref( $item->move_to_deleted ), 'Koha::Schema::Result::Deleteditem', 'Koha::Item->move_to_deleted should return the Deleted item' )
531 ; # FIXME This should be Koha::Deleted::Item
532 is( Koha::Old::Items->search({itemnumber => $item->itemnumber})->count, 1, '->move_to_deleted must have moved the item to deleteditem' );
533 $item = $builder->build_sample_item(
535 biblionumber => $biblio->biblionumber,
539 is( Koha::Old::Items->search({itemnumber => $item->itemnumber})->count, 0, '->move_to_deleted must not have moved the item to deleteditem' );
542 my $library = $builder->build_object({ class => 'Koha::Libraries' });
543 my $library_2 = $builder->build_object({ class => 'Koha::Libraries' });
544 t::lib::Mocks::mock_userenv({ branchcode => $library->branchcode });
546 my $patron = $builder->build_object({class => 'Koha::Patrons'});
547 $item = $builder->build_sample_item({ library => $library->branchcode });
550 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
553 $item->safe_to_delete,
555 'Koha::Item->safe_to_delete reports item on loan',
561 'item that is on loan cannot be deleted',
564 AddReturn( $item->barcode, $library->branchcode );
566 # book_reserved is tested in t/db_dependent/Reserves.t
569 t::lib::Mocks::mock_preference('IndependentBranches', 1);
570 my $item_2 = $builder->build_sample_item({ library => $library_2->branchcode });
573 $item_2->safe_to_delete,
575 'Koha::Item->safe_to_delete reports IndependentBranches restriction',
579 $item_2->safe_delete,
581 'IndependentBranches prevents deletion at another branch',
586 { # codeblock to limit scope of $module->mock
588 my $module = Test::MockModule->new('C4::Items');
589 $module->mock( GetAnalyticsCount => sub { return 1 } );
591 $item->discard_changes;
593 $item->safe_to_delete,
595 'Koha::Item->safe_to_delete reports linked analytics',
601 'Linked analytics prevents deletion of item',
606 { # last_item_for_hold
607 C4::Reserves::AddReserve({ branchcode => $patron->branchcode, borrowernumber => $patron->borrowernumber, biblionumber => $item->biblionumber });
608 is( $item->safe_to_delete, 'last_item_for_hold', 'Item cannot be deleted if a biblio-level is placed on the biblio and there is only 1 item attached to the biblio' );
610 # With another item attached to the biblio, the item can be deleted
611 $builder->build_sample_item({ biblionumber => $item->biblionumber });
615 $item->safe_to_delete,
617 'Koha::Item->safe_to_delete shows item safe to delete'
622 my $test_item = Koha::Items->find( $item->itemnumber );
624 is( $test_item, undef,
625 "Koha::Item->safe_delete should delete item if safe_to_delete returns true"
628 $schema->storage->txn_rollback;
631 subtest 'renewal_branchcode' => sub {
634 $schema->storage->txn_begin;
636 my $item = $builder->build_sample_item();
637 my $branch = $builder->build_object({ class => 'Koha::Libraries' });
638 my $checkout = $builder->build_object({
639 class => 'Koha::Checkouts',
641 itemnumber => $item->itemnumber,
646 C4::Context->interface( 'intranet' );
647 t::lib::Mocks::mock_userenv({ branchcode => $branch->branchcode });
649 is( $item->renewal_branchcode, $branch->branchcode, "If interface not opac, we get the branch from context");
650 is( $item->renewal_branchcode({ branch => "PANDA"}), $branch->branchcode, "If interface not opac, we get the branch from context even if we pass one in");
651 C4::Context->set_userenv(51, 'userid4tests', undef, 'firstname', 'surname', undef, undef, 0, undef, undef, undef ); #mock userenv doesn't let us set null branch
652 is( $item->renewal_branchcode({ branch => "PANDA"}), "PANDA", "If interface not opac, we get the branch we pass one in if context not set");
654 C4::Context->interface( 'opac' );
656 t::lib::Mocks::mock_preference('OpacRenewalBranch', undef);
657 is( $item->renewal_branchcode, 'OPACRenew', "If interface opac and OpacRenewalBranch undef, we get OPACRenew");
658 is( $item->renewal_branchcode({branch=>'COW'}), 'OPACRenew', "If interface opac and OpacRenewalBranch undef, we get OPACRenew even if branch passed");
660 t::lib::Mocks::mock_preference('OpacRenewalBranch', 'none');
661 is( $item->renewal_branchcode, '', "If interface opac and OpacRenewalBranch is none, we get blank string");
662 is( $item->renewal_branchcode({branch=>'COW'}), '', "If interface opac and OpacRenewalBranch is none, we get blank string even if branch passed");
664 t::lib::Mocks::mock_preference('OpacRenewalBranch', 'checkoutbranch');
665 is( $item->renewal_branchcode, $checkout->branchcode, "If interface opac and OpacRenewalBranch set to checkoutbranch, we get branch of checkout");
666 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");
668 t::lib::Mocks::mock_preference('OpacRenewalBranch','patronhomebranch');
669 is( $item->renewal_branchcode, $checkout->patron->branchcode, "If interface opac and OpacRenewalBranch set to patronbranch, we get branch of patron");
670 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");
672 t::lib::Mocks::mock_preference('OpacRenewalBranch','itemhomebranch');
673 is( $item->renewal_branchcode, $item->homebranch, "If interface opac and OpacRenewalBranch set to itemhomebranch, we get homebranch of item");
674 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");
676 $schema->storage->txn_rollback;
679 subtest 'Tests for itemtype' => sub {
681 $schema->storage->txn_begin;
683 my $biblio = $builder->build_sample_biblio;
684 my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' });
685 my $item = $builder->build_sample_item({ biblionumber => $biblio->biblionumber, itype => $itemtype->itemtype });
687 t::lib::Mocks::mock_preference('item-level_itypes', 1);
688 is( $item->itemtype->itemtype, $item->itype, 'Pref enabled' );
689 t::lib::Mocks::mock_preference('item-level_itypes', 0);
690 is( $item->itemtype->itemtype, $biblio->biblioitem->itemtype, 'Pref disabled' );
692 $schema->storage->txn_rollback;