#!/usr/bin/perl # This file is part of Koha. # # Koha is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # Koha is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Koha; if not, see . use Modern::Perl; use Test::NoWarnings; use Test::More tests => 44; use Test::Exception; use Test::Warn; use C4::Biblio qw( AddBiblio DelBiblio ModBiblio ModBiblioMarc ); use C4::Circulation qw( AddIssue AddReturn ); use C4::Reserves qw( AddReserve ); use Koha::Database; use Koha::DateUtils qw( dt_from_string ); use Koha::Cache::Memory::Lite; use Koha::Caches; use Koha::Acquisition::Orders; use Koha::AuthorisedValueCategories; use Koha::AuthorisedValues; use Koha::MarcSubfieldStructures; use Koha::ActionLogs; use Koha::Exception; use Koha::BiblioFrameworks; use JSON qw( decode_json from_json ); use MARC::Field; use MARC::Record; use t::lib::Dates; use t::lib::TestBuilder; use t::lib::Mocks; use Test::MockModule; BEGIN { use_ok('Koha::Biblio'); use_ok('Koha::Biblios'); } my $schema = Koha::Database->new->schema; my $builder = t::lib::TestBuilder->new; subtest 'metadata() tests' => sub { plan tests => 4; $schema->storage->txn_begin; my $title = 'Oranges and Peaches'; my $record = MARC::Record->new(); my $field = MARC::Field->new( '245', '', '', 'a' => $title ); $record->append_fields($field); my ($biblionumber) = C4::Biblio::AddBiblio( $record, '' ); my $biblio = Koha::Biblios->find($biblionumber); is( ref $biblio, 'Koha::Biblio', 'Found a Koha::Biblio object' ); my $metadata = $biblio->metadata; is( ref $metadata, 'Koha::Biblio::Metadata', 'Method metadata() returned a Koha::Biblio::Metadata object' ); my $record2 = $metadata->record; is( ref $record2, 'MARC::Record', 'Method record() returned a MARC::Record object' ); is( $record2->field('245')->subfield("a"), $title, 'Title in 245$a matches title from original record object' ); $schema->storage->txn_rollback; }; subtest 'hidden_in_opac() tests' => sub { plan tests => 6; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); my $rules = { withdrawn => [2] }; t::lib::Mocks::mock_preference( 'OpacHiddenItemsHidesRecord', 0 ); ok( !$biblio->hidden_in_opac( { rules => $rules } ), 'Biblio not hidden if there is no item attached (!OpacHiddenItemsHidesRecord)' ); t::lib::Mocks::mock_preference( 'OpacHiddenItemsHidesRecord', 1 ); ok( !$biblio->hidden_in_opac( { rules => $rules } ), 'Biblio not hidden if there is no item attached (OpacHiddenItemsHidesRecord)' ); my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); $item_1->withdrawn(1)->store->discard_changes; $item_2->withdrawn(1)->store->discard_changes; ok( !$biblio->hidden_in_opac( { rules => $rules } ), 'Biblio not hidden' ); $item_2->withdrawn(2)->store->discard_changes; $biblio->discard_changes; # refresh ok( !$biblio->hidden_in_opac( { rules => $rules } ), 'Biblio not hidden' ); $item_1->withdrawn(2)->store->discard_changes; $biblio->discard_changes; # refresh ok( $biblio->hidden_in_opac( { rules => $rules } ), 'Biblio hidden' ); t::lib::Mocks::mock_preference( 'OpacHiddenItemsHidesRecord', 0 ); ok( !$biblio->hidden_in_opac( { rules => $rules } ), 'Biblio hidden (!OpacHiddenItemsHidesRecord)' ); $schema->storage->txn_rollback; }; subtest 'items() tests' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); is( $biblio->items->count, 0, 'No items, count is 0' ); my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); my $items = $biblio->items; is( ref($items), 'Koha::Items', 'Returns a Koha::Items resultset' ); is( $items->count, 2, 'Two items in resultset' ); $schema->storage->txn_rollback; }; subtest 'bookable_items() tests' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); # bookable items my $bookable_item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); # not bookable items my $non_bookable_item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 0 } ); my $non_bookable_item2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 0 } ); is( ref( $biblio->bookable_items ), 'Koha::Items', "bookable_items returns a Koha::Items resultset" ); is( $biblio->bookable_items->count, 1, "bookable_items returns the correct number of items" ); is( $biblio->bookable_items->next->itemnumber, $bookable_item1->itemnumber, "bookable_items returned the correct item" ); $schema->storage->txn_rollback; }; subtest 'get_coins and get_openurl' => sub { plan tests => 4; $schema->storage->txn_begin; my $builder = t::lib::TestBuilder->new; my $biblio = $builder->build_sample_biblio( { title => 'Title 1', author => 'Author 1' } ); is( $biblio->get_coins, 'ctx_ver=Z39.88-2004&rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&rft.genre=book&rft.btitle=Title%201&rft.au=Author%201', 'GetCOinsBiblio returned right metadata' ); my $record = MARC::Record->new(); $record->append_fields( MARC::Field->new( '100', '', '', 'a' => 'Author 2' ), MARC::Field->new( '880', '', '', 'a' => 'Something' ) ); my ($biblionumber) = C4::Biblio::AddBiblio( $record, '' ); my $biblio_no_title = Koha::Biblios->find($biblionumber); is( $biblio_no_title->get_coins, 'ctx_ver=Z39.88-2004&rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&rft.genre=book&rft.au=Author%202', 'GetCOinsBiblio returned right metadata if biblio does not have a title' ); t::lib::Mocks::mock_preference( "OpenURLResolverURL", "https://koha.example.com/" ); is( $biblio->get_openurl, 'https://koha.example.com/?ctx_ver=Z39.88-2004&rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&rft.genre=book&rft.btitle=Title%201&rft.au=Author%201', 'Koha::Biblio->get_openurl returned right URL' ); t::lib::Mocks::mock_preference( "OpenURLResolverURL", "https://koha.example.com/?client_id=ci1" ); is( $biblio->get_openurl, 'https://koha.example.com/?client_id=ci1&ctx_ver=Z39.88-2004&rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3Abook&rft.genre=book&rft.btitle=Title%201&rft.au=Author%201', 'Koha::Biblio->get_openurl returned right URL' ); $schema->storage->txn_rollback; }; subtest 'is_serial() tests' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); $biblio->serial(1)->store->discard_changes; ok( $biblio->is_serial, 'Bibliographic record is serial' ); $biblio->serial(0)->store->discard_changes; ok( !$biblio->is_serial, 'Bibliographic record is not serial' ); my $record = $biblio->metadata->record; $record->leader('00142nas a22 7a 4500'); ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); ok( $biblio->is_serial, 'Bibliographic record is serial' ); $schema->storage->txn_rollback; }; subtest 'pickup_locations() tests' => sub { plan tests => 19; $schema->storage->txn_begin; Koha::CirculationRules->search->delete; Koha::CirculationRules->set_rules( { categorycode => undef, itemtype => undef, branchcode => undef, rules => { reservesallowed => 25, } } ); my $root1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { ft_local_hold_group => 1 } } ); my $root2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { ft_local_hold_group => 1 } } ); my $root3 = $builder->build_object( { class => 'Koha::Library::Groups', value => { ft_local_hold_group => 1 } } ); my $library1 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, branchname => 'zzz' } } ); my $library2 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, branchname => 'AAA' } } ); my $library3 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 0, branchname => 'FFF' } } ); my $library4 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, branchname => 'CCC' } } ); my $library5 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, branchname => 'eee' } } ); my $library6 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, branchname => 'BBB' } } ); my $library7 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 1, branchname => 'DDD' } } ); my $library8 = $builder->build_object( { class => 'Koha::Libraries', value => { pickup_location => 0, branchname => 'GGG' } } ); our @branchcodes = map { $_->branchcode } ( $library1, $library2, $library3, $library4, $library5, $library6, $library7, $library8 ); Koha::CirculationRules->set_rules( { branchcode => $library1->branchcode, itemtype => undef, rules => { holdallowed => 'from_home_library', hold_fulfillment_policy => 'any', returnbranch => 'any' } } ); Koha::CirculationRules->set_rules( { branchcode => $library2->branchcode, itemtype => undef, rules => { holdallowed => 'from_local_hold_group', hold_fulfillment_policy => 'holdgroup', returnbranch => 'any' } } ); Koha::CirculationRules->set_rules( { branchcode => $library3->branchcode, itemtype => undef, rules => { holdallowed => 'from_local_hold_group', hold_fulfillment_policy => 'patrongroup', returnbranch => 'any' } } ); Koha::CirculationRules->set_rules( { branchcode => $library4->branchcode, itemtype => undef, rules => { holdallowed => 'from_any_library', hold_fulfillment_policy => 'holdingbranch', returnbranch => 'any' } } ); Koha::CirculationRules->set_rules( { branchcode => $library5->branchcode, itemtype => undef, rules => { holdallowed => 'from_any_library', hold_fulfillment_policy => 'homebranch', returnbranch => 'any' } } ); Koha::CirculationRules->set_rules( { branchcode => $library6->branchcode, itemtype => undef, rules => { holdallowed => 'from_home_library', hold_fulfillment_policy => 'holdgroup', returnbranch => 'any' } } ); Koha::CirculationRules->set_rules( { branchcode => $library7->branchcode, itemtype => undef, rules => { holdallowed => 'from_local_hold_group', hold_fulfillment_policy => 'holdingbranch', returnbranch => 'any' } } ); Koha::CirculationRules->set_rules( { branchcode => $library8->branchcode, itemtype => undef, rules => { holdallowed => 'from_any_library', hold_fulfillment_policy => 'patrongroup', returnbranch => 'any' } } ); my $group1_1 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root1->id, branchcode => $library1->branchcode } } ); my $group1_2 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root1->id, branchcode => $library2->branchcode } } ); my $group2_3 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root2->id, branchcode => $library3->branchcode } } ); my $group2_4 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root2->id, branchcode => $library4->branchcode } } ); my $group3_5 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root3->id, branchcode => $library5->branchcode } } ); my $group3_6 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root3->id, branchcode => $library6->branchcode } } ); my $group3_7 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root3->id, branchcode => $library7->branchcode } } ); my $group3_8 = $builder->build_object( { class => 'Koha::Library::Groups', value => { parent_id => $root3->id, branchcode => $library8->branchcode } } ); my $biblio1 = $builder->build_sample_biblio( { title => '1' } ); my $biblio2 = $builder->build_sample_biblio( { title => '2' } ); throws_ok { $biblio1->pickup_locations } 'Koha::Exceptions::MissingParameter', 'Exception thrown on missing parameter'; is( $@->parameter, 'patron', 'Exception param correctly set' ); my $item1_1 = $builder->build_sample_item( { biblionumber => $biblio1->biblionumber, homebranch => $library1->branchcode, holdingbranch => $library2->branchcode, } )->store; my $item1_3 = $builder->build_sample_item( { biblionumber => $biblio1->biblionumber, homebranch => $library3->branchcode, holdingbranch => $library4->branchcode, } )->store; my $item1_7 = $builder->build_sample_item( { biblionumber => $biblio1->biblionumber, homebranch => $library7->branchcode, holdingbranch => $library4->branchcode, } )->store; my $item2_2 = $builder->build_sample_item( { biblionumber => $biblio2->biblionumber, homebranch => $library2->branchcode, holdingbranch => $library1->branchcode, } )->store; my $item2_4 = $builder->build_sample_item( { biblionumber => $biblio2->biblionumber, homebranch => $library4->branchcode, holdingbranch => $library3->branchcode, } )->store; my $item2_6 = $builder->build_sample_item( { biblionumber => $biblio2->biblionumber, homebranch => $library6->branchcode, holdingbranch => $library4->branchcode, } )->store; my $patron1 = $builder->build_object( { class => 'Koha::Patrons', value => { firstname => '1', branchcode => $library1->branchcode } } ); my $patron8 = $builder->build_object( { class => 'Koha::Patrons', value => { firstname => '8', branchcode => $library8->branchcode } } ); my $results = { "ItemHomeLibrary-1-1" => 6, "ItemHomeLibrary-1-8" => 1, "ItemHomeLibrary-2-1" => 2, "ItemHomeLibrary-2-8" => 0, "PatronLibrary-1-1" => 6, "PatronLibrary-1-8" => 3, "PatronLibrary-2-1" => 0, "PatronLibrary-2-8" => 3, }; my $items_results = { "ItemHomeLibrary-1-1" => { $library1->branchcode => [ $item1_1->itemnumber ], $library2->branchcode => [ $item1_1->itemnumber ], $library4->branchcode => [ $item1_1->itemnumber ], $library5->branchcode => [ $item1_1->itemnumber ], $library6->branchcode => [ $item1_1->itemnumber ], $library7->branchcode => [ $item1_1->itemnumber ] }, "ItemHomeLibrary-1-8" => { $library4->branchcode => [ $item1_7->itemnumber ], }, "ItemHomeLibrary-2-1" => { $library1->branchcode => [ $item2_2->itemnumber ], $library2->branchcode => [ $item2_2->itemnumber ], }, "ItemHomeLibrary-2-8" => {}, "PatronLibrary-1-1" => { $library1->branchcode => [ $item1_1->itemnumber ], $library2->branchcode => [ $item1_1->itemnumber ], $library4->branchcode => [ $item1_1->itemnumber ], $library5->branchcode => [ $item1_1->itemnumber ], $library6->branchcode => [ $item1_1->itemnumber ], $library7->branchcode => [ $item1_1->itemnumber ] }, "PatronLibrary-1-8" => { $library5->branchcode => [ $item1_1->itemnumber, $item1_3->itemnumber, $item1_7->itemnumber ], $library6->branchcode => [ $item1_1->itemnumber, $item1_3->itemnumber, $item1_7->itemnumber ], $library7->branchcode => [ $item1_1->itemnumber, $item1_3->itemnumber, $item1_7->itemnumber ], }, "PatronLibrary-2-1" => {}, "PatronLibrary-2-8" => { $library5->branchcode => [ $item2_2->itemnumber, $item2_4->itemnumber, $item2_6->itemnumber ], $library6->branchcode => [ $item2_2->itemnumber, $item2_4->itemnumber, $item2_6->itemnumber ], $library7->branchcode => [ $item2_2->itemnumber, $item2_4->itemnumber, $item2_6->itemnumber ], } }; sub _doTest { my ( $cbranch, $biblio, $patron, $results, $items_results ) = @_; t::lib::Mocks::mock_preference( 'ReservesControlBranch', $cbranch ); my $pl = $biblio->pickup_locations( { patron => $patron } ); # Filter to just test branches my @pl = map { my $pickup_location = $_; grep { $pickup_location->branchcode eq $_ } @branchcodes } $pl->as_list; ok( scalar(@pl) == $results->{ $cbranch . '-' . $biblio->title . '-' . $patron->firstname }, 'ReservesControlBranch: ' . $cbranch . ', biblio' . $biblio->title . ', patron' . $patron->firstname . ' should return ' . $results->{ $cbranch . '-' . $biblio->title . '-' . $patron->firstname } . ' and returns ' . scalar(@pl) ); my %filtered_location_items = map { $_ => $pl->{_pickup_location_items}->{$_} } grep { exists $pl->{_pickup_location_items}->{$_} } @branchcodes; is_deeply( \%filtered_location_items, \%{ $items_results->{ $cbranch . '-' . $biblio->title . '-' . $patron->firstname } }, 'Items per location correctly cached in resultset' ); } foreach my $cbranch ( 'ItemHomeLibrary', 'PatronLibrary' ) { my $cache = Koha::Cache::Memory::Lite->get_instance(); $cache->flush(); # needed since we change ReservesControlBranch foreach my $biblio ( $biblio1, $biblio2 ) { foreach my $patron ( $patron1, $patron8 ) { _doTest( $cbranch, $biblio, $patron, $results, $items_results ); } } } my @pl_names = map { $_->branchname } $biblio1->pickup_locations( { patron => $patron1 } )->as_list; my $pl_ori_str = join( '|', @pl_names ); my $pl_sorted_str = join( '|', sort { lc($a) cmp lc($b) } @pl_names ); ok( $pl_ori_str eq $pl_sorted_str, 'Libraries must be sorted by name' ); $schema->storage->txn_rollback; }; subtest 'to_api() tests' => sub { $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); my $item = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); my $biblioitem_api = $biblio->biblioitem->to_api; my $biblio_api = $biblio->to_api; plan tests => ( scalar keys %{$biblioitem_api} ) + 4; foreach my $key ( keys %{$biblioitem_api} ) { if ( $key eq 'timestamp' ) { t::lib::Dates::compare( $biblio_api->{$key}, $biblioitem_api->{$key}, "$key is added to the biblio object" ); } is( $biblio_api->{$key}, $biblioitem_api->{$key}, "$key is added to the biblio object" ); } $biblio_api = $biblio->to_api( { embed => { items => {} } } ); is_deeply( $biblio_api->{items}, [ $item->to_api ], 'Item correctly embedded' ); $biblio->biblioitem->delete(); throws_ok { $biblio->to_api } 'Koha::Exceptions::RelatedObjectNotFound', 'Exception thrown if the biblioitem accessor returns undef'; is( $@->class, 'Koha::Biblioitem' ); is( $@->accessor, 'biblioitem' ); $schema->storage->txn_rollback; }; subtest 'bookings() tests' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); is( ref( $biblio->bookings ), 'Koha::Bookings', 'Return type is correct' ); is_deeply( $biblio->bookings->unblessed, [], '->bookings returns an empty Koha::Bookings resultset' ); my $booking = $builder->build_object( { class => 'Koha::Bookings', value => { biblio_id => $biblio->biblionumber } } ); my $bookings = $biblio->bookings->unblessed; is_deeply( $biblio->bookings->unblessed, [ $booking->unblessed ], '->bookings returns the related Koha::Booking objects' ); $schema->storage->txn_rollback; }; subtest 'merge of records' => sub { plan tests => 9; subtest 'move items' => sub { plan tests => 9; $schema->storage->txn_begin; # 3 items from 3 different biblio records my $item1 = $builder->build_sample_item; my $item2 = $builder->build_sample_item; my $item3 = $builder->build_sample_item; my $biblio1 = $item1->biblio; my $biblio2 = $item2->biblio; my $biblio3 = $item3->biblio; my $pre_merged_rs = Koha::Biblios->search( { biblionumber => [ $biblio1->biblionumber, $biblio2->biblionumber, $biblio3->biblionumber ] } ); is( $pre_merged_rs->count, 3, '3 biblios exist' ); warning_like { $biblio1->merge_with( [ $biblio2->biblionumber, $biblio3->biblionumber ] ) } q{}; is( $biblio1->items->count, 3, "After merge we have 3 items on first record" ); is( ref( $biblio1->get_from_storage ), 'Koha::Biblio', 'biblio record 1 still exists' ); is( $biblio2->get_from_storage, undef, 'biblio record 2 no longer exists' ); is( $biblio3->get_from_storage, undef, 'biblio record 3 no longer exists' ); is( $item1->get_from_storage->biblionumber, $biblio1->biblionumber ); is( $item2->get_from_storage->biblionumber, $biblio1->biblionumber ); is( $item3->get_from_storage->biblionumber, $biblio1->biblionumber ); $schema->storage->txn_rollback; }; subtest 'move holds' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio1 = $builder->build_sample_biblio; my $biblio2 = $builder->build_sample_biblio; my $hold1 = $builder->build_object( { class => 'Koha::Holds', value => { biblionumber => $biblio1->biblionumber } } ); my $hold2 = $builder->build_object( { class => 'Koha::Holds', value => { biblionumber => $biblio2->biblionumber } } ); warning_like { $biblio1->merge_with( [ $biblio2->biblionumber ] ) } q{}; is( $hold1->get_from_storage->biblionumber, $biblio1->biblionumber ); is( $hold2->get_from_storage->biblionumber, $biblio1->biblionumber ); $schema->storage->txn_rollback; }; subtest 'move item groups' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio1 = $builder->build_sample_biblio; my $biblio2 = $builder->build_sample_biblio; my $ig1 = $builder->build_object( { class => 'Koha::Biblio::ItemGroups', value => { biblio_id => $biblio1->biblionumber } } ); my $ig2 = $builder->build_object( { class => 'Koha::Biblio::ItemGroups', value => { biblio_id => $biblio2->biblionumber } } ); warning_like { $biblio1->merge_with( [ $biblio2->biblionumber ] ) } q{}; is( $ig1->get_from_storage->biblio_id, $biblio1->biblionumber ); is( $ig2->get_from_storage->biblio_id, $biblio1->biblionumber ); $schema->storage->txn_rollback; }; subtest 'move article requests' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio1 = $builder->build_sample_biblio; my $biblio2 = $builder->build_sample_biblio; my $ar1 = $builder->build_object( { class => 'Koha::ArticleRequests', value => { biblionumber => $biblio1->biblionumber } } ); my $ar2 = $builder->build_object( { class => 'Koha::ArticleRequests', value => { biblionumber => $biblio2->biblionumber } } ); warning_like { $biblio1->merge_with( [ $biblio2->biblionumber ] ) } q{}; is( $ar1->get_from_storage->biblionumber, $biblio1->biblionumber ); is( $ar2->get_from_storage->biblionumber, $biblio1->biblionumber ); $schema->storage->txn_rollback; }; subtest 'move subscriptions' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio1 = $builder->build_sample_biblio; my $biblio2 = $builder->build_sample_biblio; my $sub1 = $builder->build_object( { class => 'Koha::Subscriptions', value => { biblionumber => $biblio1->biblionumber } } ); my $sub2 = $builder->build_object( { class => 'Koha::Subscriptions', value => { biblionumber => $biblio2->biblionumber } } ); warning_like { $biblio1->merge_with( [ $biblio2->biblionumber ] ) } q{}; is( $sub1->get_from_storage->biblionumber, $biblio1->biblionumber ); is( $sub2->get_from_storage->biblionumber, $biblio1->biblionumber ); $schema->storage->txn_rollback; }; subtest 'move serials' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio1 = $builder->build_sample_biblio; my $biblio2 = $builder->build_sample_biblio; my $serial1 = $builder->build_object( { class => 'Koha::Serials', value => { biblionumber => $biblio1->biblionumber } } ); my $serial2 = $builder->build_object( { class => 'Koha::Serials', value => { biblionumber => $biblio2->biblionumber } } ); warning_like { $biblio1->merge_with( [ $biblio2->biblionumber ] ) } q{}; is( $serial1->get_from_storage->biblionumber, $biblio1->biblionumber ); is( $serial2->get_from_storage->biblionumber, $biblio1->biblionumber ); $schema->storage->txn_rollback; }; subtest 'move subscription history' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio1 = $builder->build_sample_biblio; my $biblio2 = $builder->build_sample_biblio; my $sh1 = $builder->build_object( { class => 'Koha::Subscription::Histories', value => { biblionumber => $biblio1->biblionumber } } ); my $sh2 = $builder->build_object( { class => 'Koha::Subscription::Histories', value => { biblionumber => $biblio2->biblionumber } } ); warning_like { $biblio1->merge_with( [ $biblio2->biblionumber ] ) } q{}; is( $sh1->get_from_storage->biblionumber, $biblio1->biblionumber ); is( $sh2->get_from_storage->biblionumber, $biblio1->biblionumber ); $schema->storage->txn_rollback; }; subtest 'move suggestions' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio1 = $builder->build_sample_biblio; my $biblio2 = $builder->build_sample_biblio; my $suggestion1 = $builder->build_object( { class => 'Koha::Suggestions', value => { biblionumber => $biblio1->biblionumber } } ); my $suggestion2 = $builder->build_object( { class => 'Koha::Suggestions', value => { biblionumber => $biblio2->biblionumber } } ); warning_like { $biblio1->merge_with( [ $biblio2->biblionumber ] ) } q{}; is( $suggestion1->get_from_storage->biblionumber, $biblio1->biblionumber ); is( $suggestion2->get_from_storage->biblionumber, $biblio1->biblionumber ); $schema->storage->txn_rollback; }; subtest 'move orders' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio1 = $builder->build_sample_biblio; my $biblio2 = $builder->build_sample_biblio; my $order1 = $builder->build_object( { class => 'Koha::Acquisition::Orders', value => { biblionumber => $biblio1->biblionumber } } ); my $order2 = $builder->build_object( { class => 'Koha::Acquisition::Orders', value => { biblionumber => $biblio2->biblionumber } } ); warning_like { $biblio1->merge_with( [ $biblio2->biblionumber ] ) } q{}; is( $order1->get_from_storage->biblionumber, $biblio1->biblionumber ); is( $order2->get_from_storage->biblionumber, $biblio1->biblionumber ); $schema->storage->txn_rollback; }; }; subtest 'suggestions() tests' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); is( ref( $biblio->suggestions ), 'Koha::Suggestions', 'Return type is correct' ); is_deeply( $biblio->suggestions->unblessed, [], '->suggestions returns an empty Koha::Suggestions resultset' ); my $suggestion = $builder->build_object( { class => 'Koha::Suggestions', value => { biblionumber => $biblio->biblionumber } } ); my $suggestions = $biblio->suggestions->unblessed; is_deeply( $biblio->suggestions->unblessed, [ $suggestion->unblessed ], '->suggestions returns the related Koha::Suggestion objects' ); $schema->storage->txn_rollback; }; subtest 'get_marc_components() tests' => sub { plan tests => 5; $schema->storage->txn_begin; my ($host_bibnum) = C4::Biblio::AddBiblio( host_record(), '' ); my $host_biblio = Koha::Biblios->find($host_bibnum); t::lib::Mocks::mock_preference( 'SearchEngine', 'Zebra' ); my $search_mod = Test::MockModule->new('Koha::SearchEngine::Zebra::Search'); $search_mod->mock( 'search_compat', \&search_component_record2 ); my $components = $host_biblio->get_marc_components; is( ref($components), 'ARRAY', 'Return type is correct' ); is_deeply( $components, [], '->get_marc_components returns an empty ARRAY' ); $search_mod->unmock('search_compat'); $search_mod->mock( 'search_compat', \&search_component_record1 ); my $component_record = component_record1()->as_xml(); is_deeply( $host_biblio->get_marc_components, [$component_record], '->get_marc_components returns the related component part record' ); $search_mod->unmock('search_compat'); $search_mod->mock( 'search_compat', sub { Koha::Exception->throw("error searching analytics") } ); warning_like { $components = $host_biblio->get_marc_components } qr{Warning from search_compat: .* 'error searching analytics'}; is_deeply( $host_biblio->object_messages, [ { type => 'error', message => 'component_search', payload => "Exception 'Koha::Exception' thrown 'error searching analytics'\n" } ] ); $search_mod->unmock('search_compat'); $schema->storage->txn_rollback; }; subtest 'get_components_query' => sub { plan tests => 12; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); my $biblionumber = $biblio->biblionumber; my $record = $biblio->metadata->record; foreach my $engine ( 'Zebra', 'Elasticsearch' ) { t::lib::Mocks::mock_preference( 'SearchEngine', $engine ); t::lib::Mocks::mock_preference( 'UseControlNumber', '0' ); t::lib::Mocks::mock_preference( 'ComponentSortField', 'author' ); t::lib::Mocks::mock_preference( 'ComponentSortOrder', 'za' ); my ( $comp_query, $comp_query_str, $comp_sort ) = $biblio->get_components_query; is( $comp_query_str, 'Host-item:("Some boring read")', "$engine: UseControlNumber disabled" ); is( $comp_sort, "author_za", "$engine: UseControlNumber disabled sort is correct" ); t::lib::Mocks::mock_preference( 'UseControlNumber', '1' ); t::lib::Mocks::mock_preference( 'ComponentSortOrder', 'az' ); my $marc_001_field = MARC::Field->new( '001', $biblionumber ); $record->append_fields($marc_001_field); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); ( $comp_query, $comp_query_str, $comp_sort ) = $biblio->get_components_query; is( $comp_query_str, "(rcn:\"$biblionumber\" AND (bib-level:a OR bib-level:b))", "$engine: UseControlNumber enabled without MarcOrgCode" ); is( $comp_sort, "author_az", "$engine: UseControlNumber enabled without MarcOrgCode sort is correct" ); my $marc_003_field = MARC::Field->new( '003', 'OSt' ); $record->append_fields($marc_003_field); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); t::lib::Mocks::mock_preference( 'ComponentSortField', 'title' ); t::lib::Mocks::mock_preference( 'ComponentSortOrder', 'asc' ); ( $comp_query, $comp_query_str, $comp_sort ) = $biblio->get_components_query; is( $comp_query_str, "(((rcn:\"$biblionumber\" AND cni:\"OSt\") OR rcn:\"OSt $biblionumber\") AND (bib-level:a OR bib-level:b))", "$engine: UseControlNumber enabled with MarcOrgCode" ); is( $comp_sort, "title_asc", "$engine: UseControlNumber enabled with MarcOrgCode sort if correct" ); $record->delete_field($marc_003_field); } $schema->storage->txn_rollback; }; subtest 'get_volumes_query' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); my $biblionumber = $biblio->biblionumber; my $record = $biblio->metadata->record; # Ensure our mocked record is captured as a set or monographic series my $ldr = $record->leader(); substr( $ldr, 19, 1 ) = 'a'; $record->leader($ldr); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); t::lib::Mocks::mock_preference( 'UseControlNumber', '0' ); is( $biblio->get_volumes_query, "(title-series,phr:(\"Some boring read\") OR Host-item,phr:(\"Some boring read\") NOT (bib-level:a OR bib-level:b))", "UseControlNumber disabled" ); t::lib::Mocks::mock_preference( 'UseControlNumber', '1' ); my $marc_001_field = MARC::Field->new( '001', $biblionumber ); $record->append_fields($marc_001_field); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); is( $biblio->get_volumes_query, "(rcn:$biblionumber NOT (bib-level:a OR bib-level:b))", "UseControlNumber enabled without MarcOrgCode" ); my $marc_003_field = MARC::Field->new( '003', 'OSt' ); $record->append_fields($marc_003_field); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); is( $biblio->get_volumes_query, "(((rcn:$biblionumber AND cni:OSt) OR rcn:\"OSt $biblionumber\") NOT (bib-level:a OR bib-level:b))", "UseControlNumber enabled with MarcOrgCode" ); $schema->storage->txn_rollback; }; subtest 'generate_marc_host_field' => sub { plan tests => 36; $schema->storage->txn_begin; # Set up MARC21 tests t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' ); # 1. Complete MARC21 record test my $record = MARC::Record->new(); $record->leader('00000nam a22000007a 4500'); $record->append_fields( MARC::Field->new( '001', '12345' ), MARC::Field->new( '003', 'NB' ), MARC::Field->new( '020', '', '', 'a' => '978-3-16-148410-0' ), MARC::Field->new( '022', '', '', 'a' => '1234-5678' ), MARC::Field->new( '100', '1', '', 'a' => 'Smith, John', 'e' => 'author', '9' => 'xyz', '4' => 'aut' ), MARC::Field->new( '245', '1', '0', 'a' => 'The Title', 'b' => 'Subtitle', 'c' => 'John Smith' ), MARC::Field->new( '250', '', '', 'a' => '2nd edition', 'b' => 'revised' ), MARC::Field->new( '260', '', '', 'a' => 'New York', 'b' => 'Publisher', 'c' => '2023' ), MARC::Field->new( '830', '', '', 'a' => 'Series Title', 'v' => 'vol. 2', 'x' => '2345-6789' ) ); my ($biblio_id) = AddBiblio( $record, qw{} ); my $biblio = Koha::Biblios->find($biblio_id); # Test MARC21 with UseControlNumber off t::lib::Mocks::mock_preference( 'UseControlNumber', 0 ); my $link = $biblio->generate_marc_host_field(); # Test standard MARC21 field is( ref($link), 'MARC::Field', 'Returns a MARC::Field object' ); is( $link->tag(), '773', 'Field tag is 773 for MARC21' ); is( $link->indicator(1), '0', 'First indicator is 0' ); is( $link->indicator(2), ' ', 'Second indicator is blank' ); # Check all subfields is( $link->subfield('7'), 'p1am', 'Subfield 7 correctly formed' ); is( $link->subfield('a'), 'Smith, John', 'Subfield a contains author from 100a' ); is( $link->subfield('t'), 'The Title Subtitle', 'Subfield t contains title without trailing punctuation from 245ab' ); is( $link->subfield('b'), '2nd edition revised', 'Subfield b contains edition info from 250ab' ); is( $link->subfield('d'), 'New York Publisher 2023', 'Subfield d contains publication info from 260abc' ); is( $link->subfield('k'), 'Series Title, ISSN 2345-6789 ; vol. 2', 'Subfield k contains series info from 830' ); is( $link->subfield('x'), '1234-5678', 'Subfield x contains ISSN from 022a' ); is( $link->subfield('z'), '978-3-16-148410-0', 'Subfield z contains ISBN from 020a' ); is( $link->subfield('w'), undef, 'Subfield w is undefined when UseControlNumber is disabled' ); # Test with UseControlNumber enabled t::lib::Mocks::mock_preference( 'UseControlNumber', '1' ); $link = $biblio->generate_marc_host_field(); is( $link->subfield('w'), '(NB)12345', 'Subfield w contains control number with source when UseControlNumber is enabled' ); # 245 punctuation handling tests # Trailing slash $record->field('245')->update( a => 'A title /', b => '', c => '', 'ind2' => '0' ); ($biblio_id) = AddBiblio( $record, qw{} ); $biblio = Koha::Biblios->find($biblio_id); $link = $biblio->generate_marc_host_field(); is( $link->subfield('t'), 'A title', "Trailing slash is removed from 245a" ); # Trailing period $record->field('245')->update( a => 'Another title.', 'ind2' => '0' ); ($biblio_id) = AddBiblio( $record, qw{} ); $biblio = Koha::Biblios->find($biblio_id); $link = $biblio->generate_marc_host_field(); is( $link->subfield('t'), 'Another title', "Trailing period is removed from 245a" ); # Offset from indicator 2 = 4 $record->field('245')->update( a => 'The offset title', 'ind2' => '4' ); ($biblio_id) = AddBiblio( $record, qw{} ); $biblio = Koha::Biblios->find($biblio_id); $link = $biblio->generate_marc_host_field(); is( $link->subfield('t'), 'Offset title', "Title offset applied from indicator 2" ); # Capitalization after offset $record->field('245')->update( a => 'the capital test', 'ind2' => '0' ); ($biblio_id) = AddBiblio( $record, qw{} ); $biblio = Koha::Biblios->find($biblio_id); $link = $biblio->generate_marc_host_field(); is( $link->subfield('t'), 'The capital test', "Title is capitalized after indicator offset" ); # 240 uniform title tests $record->append_fields( MARC::Field->new( '240', '1', '0', 'a' => 'Bible. English', 'l' => 'English' ) ); ($biblio_id) = AddBiblio( $record, qw{} ); $biblio = Koha::Biblios->find($biblio_id); $link = $biblio->generate_marc_host_field(); is( $link->subfield('s'), 'Bible. English', "Subfield s contains uniform title from 240a" ); # 260/264 handling tests $record->append_fields( MARC::Field->new( '264', '', '', a => 'Publication 264' ), ); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); $link = $biblio->generate_marc_host_field(); is( $link->subfield('d'), 'Publication 264', 'MARC::Field->subfield(d) returns content from 264 in preference to 260' ); $record->append_fields( MARC::Field->new( '264', '3', '', a => 'Publication 264', b => 'Preferred' ), ); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); $link = $biblio->generate_marc_host_field(); is( $link->subfield('d'), 'Publication 264 Preferred', 'MARC::Field->subfield(d) returns content from 264 with indicator 1 = 3 in preference to 264 without' ); # 2. Test MARC21 with corporate author (110) my $record_corporate = MARC::Record->new(); $record_corporate->leader('00000nam a22000007a 4500'); $record_corporate->append_fields( MARC::Field->new( '110', '2', '', 'a' => 'Corporate Author', 'e' => 'sponsor', '9' => 'xyz', '4' => 'spn' ), MARC::Field->new( '245', '1', '0', 'a' => 'The Title' ) ); ($biblio_id) = AddBiblio( $record_corporate, qw{} ); $biblio = Koha::Biblios->find($biblio_id); $link = $biblio->generate_marc_host_field(); is( $link->subfield('7'), 'c2am', 'Subfield 7 correctly formed for corporate author' ); is( $link->subfield('a'), 'Corporate Author', 'Subfield a contains corporate author' ); # 3. Test MARC21 with meeting name (111) my $record_meeting = MARC::Record->new(); $record_meeting->leader('00000nam a22000007a 4500'); $record_meeting->append_fields( MARC::Field->new( '111', '2', '', 'a' => 'Conference Name', 'j' => 'relator', '9' => 'xyz', '4' => 'spn' ), MARC::Field->new( '245', '1', '0', 'a' => 'The Title' ) ); ($biblio_id) = AddBiblio( $record_meeting, qw{} ); $biblio = Koha::Biblios->find($biblio_id); $link = $biblio->generate_marc_host_field(); is( $link->subfield('7'), 'm2am', 'Subfield 7 correctly formed for meeting name' ); # 4. Test MARC21 with minimal record my $record_minimal = MARC::Record->new(); $record_minimal->leader('00000nam a22000007a 4500'); $record_minimal->append_fields( MARC::Field->new( '245', '0', '0', 'a' => 'Title Only' ) ); ($biblio_id) = AddBiblio( $record_minimal, qw{} ); $biblio = Koha::Biblios->find($biblio_id); $link = $biblio->generate_marc_host_field(); is( $link->subfield('7'), 'nnam', 'Subfield 7 correctly formed with no main entry' ); # 5. Test UNIMARC t::lib::Mocks::mock_preference( 'marcflavour', 'UNIMARC' ); $biblio = $builder->build_sample_biblio(); my $record_unimarc = MARC::Record->new(); $record_unimarc->append_fields( MARC::Field->new( '001', '54321' ), MARC::Field->new( '010', '', '', 'a' => '978-0-12-345678-9' ), MARC::Field->new( '011', '', '', 'a' => '2345-6789' ), MARC::Field->new( '200', '', '', 'a' => 'UNIMARC Title' ), MARC::Field->new( '205', '', '', 'a' => 'Third edition' ), MARC::Field->new( '210', '', '', 'a' => 'Paris', 'd' => '2023' ), MARC::Field->new( '700', '', '', 'a' => 'Doe', 'b' => 'Jane' ), MARC::Field->new( '856', '', '', 'u' => 'http://example.com' ) ); ($biblio_id) = AddBiblio( $record_unimarc, qw{} ); $biblio = Koha::Biblios->find($biblio_id); $link = $biblio->generate_marc_host_field(); is( ref($link), 'MARC::Field', 'Returns a MARC::Field object for UNIMARC' ); is( $link->tag(), '461', 'Field tag is 461 for UNIMARC' ); is( $link->indicator(1), '0', 'First indicator is 0 for UNIMARC' ); is( $link->indicator(2), ' ', 'Second indicator is blank for UNIMARC' ); # Check UNIMARC subfields is( $link->subfield('a'), 'Doe Jane', 'Subfield a contains author for UNIMARC' ); is( $link->subfield('t'), 'UNIMARC Title', 'Subfield t contains title for UNIMARC' ); is( $link->subfield('c'), 'Paris', 'Subfield c contains place of publication for UNIMARC' ); is( $link->subfield('d'), '2023', 'Subfield d contains date of publication for UNIMARC' ); is( $link->subfield('0'), '54321', 'Subfield 0 contains control number for UNIMARC' ); # 6. Test UNIMARC with different author types my $record_unimarc_corporate = MARC::Record->new(); $record_unimarc_corporate->append_fields( MARC::Field->new( '710', '', '', 'a' => 'Corporate', 'b' => 'Department' ), MARC::Field->new( '200', '', '', 'a' => 'Title' ) ); C4::Biblio::ModBiblio( $record_unimarc_corporate, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); $link = $biblio->generate_marc_host_field(); is( $link->subfield('a'), 'Corporate Department', 'Subfield a contains corporate author for UNIMARC' ); my $record_unimarc_family = MARC::Record->new(); $record_unimarc_family->append_fields( MARC::Field->new( '720', '', '', 'a' => 'Family', 'b' => 'Name' ), MARC::Field->new( '200', '', '', 'a' => 'Title' ) ); C4::Biblio::ModBiblio( $record_unimarc_family, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); $link = $biblio->generate_marc_host_field(); is( $link->subfield('a'), 'Family Name', 'Subfield a contains family name for UNIMARC' ); $schema->storage->txn_rollback; t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' ); }; subtest 'link_marc_host' => sub { plan tests => 6; $schema->storage->txn_begin; my $host = $builder->build_sample_biblio(); my $child = $builder->build_sample_biblio(); my $child_record = $child->metadata->record; is( $child_record->field('773'), undef, "773 field is undefined before link_marc_host" ); $child->link_marc_host( { host => $host->biblionumber } ); $child->discard_changes; $child_record = $child->metadata->record; is( ref( $child_record->field('773') ), 'MARC::Field', '773 field is set after calling link_marc_host({ host => $biblionumber })' ); $child = $builder->build_sample_biblio(); $child_record = $child->metadata->record; is( $child_record->field('773'), undef, "773 field is undefined before link_marc_host" ); $child->link_marc_host( { host => $host } ); $child->discard_changes; $child_record = $child->metadata->record; is( ref( $child_record->field('773') ), 'MARC::Field', '773 field is set after calling link_marc_host({ host => $biblio })' ); $child = $builder->build_sample_biblio(); $child_record = $child->metadata->record; is( $child_record->field('773'), undef, "773 field is undefined before link_marc_host" ); my $link_field = $host->generate_marc_host_field; $child->link_marc_host( { field => $link_field } ); $child->discard_changes; $child_record = $child->metadata->record; is( ref( $child_record->field('773') ), 'MARC::Field', '773 field is set after calling link_marc_host({ field => $link_field })' ); $schema->storage->txn_rollback; }; subtest '->orders, ->uncancelled_orders and ->acq_status tests' => sub { plan tests => 9; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); my $orders = $biblio->orders; my $uncancelled_orders = $biblio->uncancelled_orders; is( ref($orders), 'Koha::Acquisition::Orders', 'Result type is correct' ); is( $biblio->orders->count, $biblio->uncancelled_orders->count, '->orders->count returns the count for the resultset' ); # Add a couple orders foreach ( 1 .. 2 ) { $builder->build_object( { class => 'Koha::Acquisition::Orders', value => { biblionumber => $biblio->biblionumber, datecancellationprinted => '2019-12-31', orderstatus => 'cancelled', } } ); } $builder->build_object( { class => 'Koha::Acquisition::Orders', value => { biblionumber => $biblio->biblionumber, datecancellationprinted => undef, orderstatus => 'ordered', quantity => 1, quantityreceived => 0, } } ); $orders = $biblio->orders; $uncancelled_orders = $biblio->uncancelled_orders; is( ref($orders), 'Koha::Acquisition::Orders', 'Result type is correct' ); is( ref($uncancelled_orders), 'Koha::Acquisition::Orders', 'Result type is correct' ); is( $orders->count, $uncancelled_orders->count + 2, '->uncancelled_orders->count returns the right count' ); # Check acq status is( $biblio->acq_status, 'processing', 'Processing for presence of ordered lines' ); $orders->filter_by_active->update( { orderstatus => 'new' } ); is( $biblio->acq_status, 'processing', 'Still processing for presence of new lines' ); $orders->filter_out_cancelled->update( { orderstatus => 'complete' } ); is( $biblio->acq_status, 'acquired', 'Acquired: some complete, rest cancelled' ); $orders->update( { orderstatus => 'cancelled', datecancellationprinted => dt_from_string() } ); is( $biblio->acq_status, 'cancelled', 'Cancelled for only cancelled lines' ); $schema->storage->txn_rollback; }; subtest 'tickets() tests' => sub { plan tests => 4; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); my $tickets = $biblio->tickets; is( ref($tickets), 'Koha::Tickets', 'Koha::Biblio->tickets should return a Koha::Tickets object' ); is( $tickets->count, 0, 'Koha::Biblio->tickets should return a count of 0 when there are no related tickets' ); # Add two tickets foreach ( 1 .. 2 ) { $builder->build_object( { class => 'Koha::Tickets', value => { biblio_id => $biblio->biblionumber } } ); } $tickets = $biblio->tickets; is( ref($tickets), 'Koha::Tickets', 'Koha::Biblio->tickets should return a Koha::Tickets object' ); is( $tickets->count, 2, 'Koha::Biblio->tickets should return the correct number of tickets' ); $schema->storage->txn_rollback; }; subtest 'serials() tests' => sub { plan tests => 4; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio; my $serials = $biblio->serials; is( ref($serials), 'Koha::Serials', 'Koha::Biblio->serials should return a Koha::Serials object' ); is( $serials->count, 0, 'Koha::Biblio->serials should return the correct number of serials' ); # Add two serials foreach ( 1 .. 2 ) { $builder->build_object( { class => 'Koha::Serials', value => { biblionumber => $biblio->biblionumber } } ); } $serials = $biblio->serials; is( ref($serials), 'Koha::Serials', 'Koha::Biblio->serials should return a Koha::Serials object' ); is( $serials->count, 2, 'Koha::Biblio->serials should return the correct number of serials' ); $schema->storage->txn_rollback; }; subtest 'subscriptions() tests' => sub { plan tests => 4; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio; my $subscriptions = $biblio->subscriptions; is( ref($subscriptions), 'Koha::Subscriptions', 'Koha::Biblio->subscriptions should return a Koha::Subscriptions object' ); is( $subscriptions->count, 0, 'Koha::Biblio->subscriptions should return the correct number of subscriptions' ); # Add two subscriptions foreach ( 1 .. 2 ) { $builder->build_object( { class => 'Koha::Subscriptions', value => { biblionumber => $biblio->biblionumber } } ); } $subscriptions = $biblio->subscriptions; is( ref($subscriptions), 'Koha::Subscriptions', 'Koha::Biblio->subscriptions should return a Koha::Subscriptions object' ); is( $subscriptions->count, 2, 'Koha::Biblio->subscriptions should return the correct number of subscriptions' ); $schema->storage->txn_rollback; }; subtest 'subscription_histories() tests' => sub { plan tests => 4; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio; my $sub_histories = $biblio->subscription_histories; is( ref($sub_histories), 'Koha::Subscription::Histories', 'Koha::Biblio->subscription_histories should return a Koha::Subscription::Histories object' ); is( $sub_histories->count, 0, 'Koha::Biblio->subscription_histories should return the correct number of subscription histories' ); # Add two subscription histories foreach ( 1 .. 2 ) { $builder->build_object( { class => 'Koha::Subscription::Histories', value => { biblionumber => $biblio->biblionumber } } ); } $sub_histories = $biblio->subscription_histories; is( ref($sub_histories), 'Koha::Subscription::Histories', 'Koha::Biblio->subscription_histories should return a Koha::Subscription::Histories object' ); is( $sub_histories->count, 2, 'Koha::Biblio->subscription_histories should return the correct number of subscription histories' ); $schema->storage->txn_rollback; }; subtest 'get_marc_notes() MARC21 tests' => sub { plan tests => 14; $schema->storage->txn_begin; t::lib::Mocks::mock_preference( 'NotesToHide', '520' ); my $av = $builder->build_object( { class => 'Koha::AuthorisedValues' } ); my $biblio = $builder->build_sample_biblio; my $record = $biblio->metadata->record; $record->append_fields( MARC::Field->new( '500', '', '', a => 'Note1' ), MARC::Field->new( '505', '', '', a => 'Note2', u => 'http://someserver.com' ), MARC::Field->new( '520', '', '', a => 'Note3 skipped' ), MARC::Field->new( '541', '0', '', a => 'Note4 skipped on opac' ), MARC::Field->new( '544', '', '', a => 'Note5' ), MARC::Field->new( '590', '', '', a => $av->authorised_value ), MARC::Field->new( '545', '', '', a => 'Invisible on OPAC' ), ); my $mss = Koha::MarcSubfieldStructures->find( { tagfield => "590", tagsubfield => "a", frameworkcode => $biblio->frameworkcode } ); $mss->update( { authorised_value => $av->category } ); $mss = Koha::MarcSubfieldStructures->find( { tagfield => "545", tagsubfield => "a", frameworkcode => $biblio->frameworkcode } ); $mss->update( { hidden => 1 } ); my $cache = Koha::Caches->get_instance; $cache->clear_from_cache("MarcStructure-0-"); $cache->clear_from_cache("MarcStructure-1-"); $cache->clear_from_cache("MarcSubfieldStructure-"); $cache->clear_from_cache("MarcCodedFields-"); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); my $notes = $biblio->get_marc_notes; is( $notes->[0]->{marcnote}, 'Note1', 'First note' ); is( $notes->[1]->{marcnote}, 'Note2', 'Second note' ); is( $notes->[2]->{marcnote}, 'http://someserver.com', 'URL separated' ); is( $notes->[3]->{marcnote}, 'Note4 skipped on opac', "Note shows if not opac (Hidden by Indicator)" ); is( $notes->[4]->{marcnote}, 'Note5', 'Fifth note' ); is( $notes->[5]->{marcnote}, $av->lib, 'Authorised value is correctly parsed to show description rather than code' ); is( $notes->[6]->{marcnote}, 'Invisible on OPAC', 'Note shows if not opac (Hidden by framework)' ); is( @$notes, 7, 'No more notes' ); $notes = $biblio->get_marc_notes( { opac => 1 } ); is( $notes->[0]->{marcnote}, 'Note1', 'First note' ); is( $notes->[1]->{marcnote}, 'Note2', 'Second note' ); is( $notes->[2]->{marcnote}, 'http://someserver.com', 'URL separated' ); is( $notes->[3]->{marcnote}, 'Note5', 'Fifth note shows after fourth skipped' ); is( $notes->[4]->{marcnote}, $av->lib_opac, 'Authorised value is correctly parsed for OPAC to show description rather than code' ); is( @$notes, 5, 'No more notes' ); $cache->clear_from_cache("MarcStructure-0-"); $cache->clear_from_cache("MarcStructure-1-"); $cache->clear_from_cache("MarcSubfieldStructure-"); $cache->clear_from_cache("MarcCodedFields-"); $schema->storage->txn_rollback; }; subtest 'get_marc_notes() UNIMARC tests' => sub { plan tests => 3; $schema->storage->txn_begin; t::lib::Mocks::mock_preference( 'NotesToHide', '310' ); t::lib::Mocks::mock_preference( 'marcflavour', 'UNIMARC' ); my $biblio = $builder->build_sample_biblio; my $record = $biblio->metadata->record; $record->append_fields( MARC::Field->new( '300', '', '', a => 'Note1' ), MARC::Field->new( '300', '', '', a => 'Note2' ), MARC::Field->new( '310', '', '', a => 'Note3 skipped' ), ); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); my $notes = $biblio->get_marc_notes( { marcflavour => 'UNIMARC' } ); is( $notes->[0]->{marcnote}, 'Note1', 'First note' ); is( $notes->[1]->{marcnote}, 'Note2', 'Second note' ); is( @$notes, 2, 'No more notes' ); t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' ); $schema->storage->txn_rollback; }; subtest 'host_items() tests' => sub { plan tests => 8; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio( { frameworkcode => '' } ); t::lib::Mocks::mock_preference( 'EasyAnalyticalRecords', 1 ); my $host_items = $biblio->host_items; is( ref($host_items), 'Koha::Items' ); is( $host_items->count, 0 ); my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } ); my $host_item_1 = $builder->build_sample_item; my $host_item_2 = $builder->build_sample_item; my $record = $biblio->metadata->record; $record->append_fields( MARC::Field->new( '773', '', '', 9 => $host_item_1->itemnumber, 9 => $host_item_2->itemnumber ), ); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = $biblio->get_from_storage; $host_items = $biblio->host_items; is( $host_items->count, 2 ); is_deeply( [ $host_items->get_column('itemnumber') ], [ $host_item_1->itemnumber, $host_item_2->itemnumber ] ); my $transfer = $builder->build_object( { class => 'Koha::Item::Transfers', value => { itemnumber => $host_item_1->itemnumber, frombranch => $host_item_1->holdingbranch, } } ); ok( $host_items->search( {}, { join => 'branchtransfers', order_by => 'branchtransfers.daterequested' } )->as_list, "host_items can be used with a join query on itemnumber" ); $transfer->delete; t::lib::Mocks::mock_preference( 'EasyAnalyticalRecords', 0 ); $host_items = $biblio->host_items; is( ref($host_items), 'Koha::Items' ); is( $host_items->count, 0 ); subtest 'test host_items param in items()' => sub { plan tests => 5; t::lib::Mocks::mock_preference( 'EasyAnalyticalRecords', 1 ); my $items = $biblio->items; is( $items->count, 1, "Without host_items param we only get the items on the biblio" ); $items = $biblio->items( { host_items => 1 } ); is( $items->count, 3, "With param host_items we get the biblio items plus analytics" ); is( ref($items), 'Koha::Items', "We correctly get an Items object" ); is_deeply( [ $items->get_column('itemnumber') ], [ $item_1->itemnumber, $host_item_1->itemnumber, $host_item_2->itemnumber ] ); t::lib::Mocks::mock_preference( 'EasyAnalyticalRecords', 0 ); $items = $biblio->items( { host_items => 1 } ); is( $items->count, 1, "With host_items param but EasyAnalyticalRecords disabled we only get the items on the biblio" ); }; $schema->storage->txn_rollback; }; subtest 'article_requests() tests' => sub { plan tests => 3; $schema->storage->txn_begin; my $item = $builder->build_sample_item; my $biblio = $item->biblio; my $article_requests = $biblio->article_requests; is( ref($article_requests), 'Koha::ArticleRequests', 'In scalar context, type is correct' ); is( $article_requests->count, 0, 'No article requests' ); foreach my $i ( 0 .. 3 ) { my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); $builder->build_object( { class => 'Koha::ArticleRequests', value => { borrowernumber => $patron->id, biblionumber => $biblio->id, itemnumber => $item->id, title => $biblio->title, } } )->request; } $article_requests = $biblio->article_requests; is( $article_requests->count, 4, '4 article requests' ); $schema->storage->txn_rollback; }; subtest 'current_checkouts() and old_checkouts() tests' => sub { plan tests => 4; $schema->storage->txn_begin; my $library = $builder->build_object( { class => 'Koha::Libraries' } ); my $patron_1 = $builder->build_object( { class => 'Koha::Patrons' } ); my $patron_2 = $builder->build_object( { class => 'Koha::Patrons' } ); my $item_1 = $builder->build_sample_item; my $item_2 = $builder->build_sample_item( { biblionumber => $item_1->biblionumber } ); t::lib::Mocks::mock_userenv( { branchcode => $library->id } ); AddIssue( $patron_1, $item_1->barcode ); AddIssue( $patron_1, $item_2->barcode ); AddReturn( $item_1->barcode ); AddIssue( $patron_2, $item_1->barcode ); my $biblio = $item_1->biblio; my $current_checkouts = $biblio->current_checkouts; my $old_checkouts = $biblio->old_checkouts; is( ref($current_checkouts), 'Koha::Checkouts', 'Type is correct' ); is( ref($old_checkouts), 'Koha::Old::Checkouts', 'Type is correct' ); is( $current_checkouts->count, 2, 'Count is correct for current checkouts' ); is( $old_checkouts->count, 1, 'Count is correct for old checkouts' ); $schema->storage->txn_rollback; }; subtest 'get_marc_contributors() tests' => sub { plan tests => 4; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio( { author => 'Main author' } ); my $record = $biblio->metadata->record; # add author information my $field = MARC::Field->new( '700', '1', '', 'a' => 'Jefferson, Thomas' ); $record->append_fields($field); $field = MARC::Field->new( '701', '1', '', 'd' => 'Secondary author 2' ); $record->append_fields($field); # get record C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); is( @{ $biblio->get_marc_authors }, 3, 'get_marc_authors retrieves correct number of author subfields' ); is( @{ $biblio->get_marc_contributors }, 2, 'get_marc_contributors retrieves correct number of author subfields' ); # main heading 110 my $biblio2 = $builder->build_sample_biblio; my $record2 = $biblio2->metadata->record; $record2->delete_fields( $record2->field('1..') ); # add author information $field = MARC::Field->new( '110', '1', '', 'a' => 'City of Marseille' ); $record2->append_fields($field); $field = MARC::Field->new( '700', '1', '', 'a' => 'Jefferson, Thomas' ); $record2->append_fields($field); $field = MARC::Field->new( '701', '1', '', 'd' => 'Secondary author 2' ); $record2->append_fields($field); # get record2 C4::Biblio::ModBiblio( $record2, $biblio2->biblionumber ); $biblio2 = Koha::Biblios->find( $biblio2->biblionumber ); is( @{ $biblio2->get_marc_authors }, 3, 'get_marc_authors retrieves correct number of author fields for 110 being the main heading' ); is( @{ $biblio2->get_marc_contributors }, 2, 'get_marc_contributors retrieves correct number of author fields for 110 being the main heading' ); $schema->storage->txn_rollback; }; subtest 'Recalls tests' => sub { plan tests => 13; $schema->storage->txn_begin; my $item1 = $builder->build_sample_item; my $biblio = $item1->biblio; my $branchcode = $item1->holdingbranch; my $patron1 = $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $branchcode } } ); my $patron2 = $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $branchcode } } ); my $patron3 = $builder->build_object( { class => 'Koha::Patrons', value => { branchcode => $branchcode } } ); my $item2 = $builder->build_object( { class => 'Koha::Items', value => { holdingbranch => $branchcode, homebranch => $branchcode, biblionumber => $biblio->biblionumber, itype => $item1->effective_itemtype } } ); t::lib::Mocks::mock_userenv( { patron => $patron1 } ); my $recall1 = Koha::Recall->new( { patron_id => $patron1->borrowernumber, created_date => \'NOW()', biblio_id => $biblio->biblionumber, pickup_library_id => $branchcode, item_id => $item1->itemnumber, expiration_date => undef, item_level => 1 } )->store; my $recall2 = Koha::Recall->new( { patron_id => $patron2->borrowernumber, created_date => \'NOW()', biblio_id => $biblio->biblionumber, pickup_library_id => $branchcode, item_id => undef, expiration_date => undef, item_level => 0 } )->store; my $recall3 = Koha::Recall->new( { patron_id => $patron3->borrowernumber, created_date => \'NOW()', biblio_id => $biblio->biblionumber, pickup_library_id => $branchcode, item_id => $item1->itemnumber, expiration_date => undef, item_level => 1 } )->store; my $recalls = $biblio->recalls; is( $recalls->count, 3, 'Correctly get number of recalls for biblio' ); $recall1->set_cancelled; $recall2->set_expired( { interface => 'COMMANDLINE' } ); is( $recalls->count, 3, 'Correctly get number of recalls for biblio' ); is( $recalls->filter_by_current->count, 1, 'Correctly get number of active recalls for biblio' ); t::lib::Mocks::mock_preference( 'UseRecalls', 0 ); is( $biblio->can_be_recalled( { patron => $patron1 } ), 0, "Can't recall with UseRecalls disabled" ); t::lib::Mocks::mock_preference( "UseRecalls", 1 ); $item1->update( { notforloan => 1 } ); is( $biblio->can_be_recalled( { patron => $patron1 } ), 0, "Can't recall with no available items" ); $item1->update( { notforloan => 0 } ); Koha::CirculationRules->set_rules( { branchcode => $branchcode, categorycode => $patron1->categorycode, itemtype => $item1->effective_itemtype, rules => { recalls_allowed => 0, recalls_per_record => 1, on_shelf_recalls => 'all', }, } ); is( $biblio->can_be_recalled( { patron => $patron1 } ), 0, "Can't recall if recalls_allowed = 0" ); Koha::CirculationRules->set_rules( { branchcode => $branchcode, categorycode => $patron1->categorycode, itemtype => $item1->effective_itemtype, rules => { recalls_allowed => 1, recalls_per_record => 1, on_shelf_recalls => 'all', }, } ); is( $biblio->can_be_recalled( { patron => $patron1 } ), 0, "Can't recall if patron has more existing recall(s) than recalls_allowed" ); is( $biblio->can_be_recalled( { patron => $patron1 } ), 0, "Can't recall if patron has more existing recall(s) than recalls_per_record" ); $recall1->set_cancelled; C4::Circulation::AddIssue( $patron1, $item2->barcode ); is( $biblio->can_be_recalled( { patron => $patron1 } ), 0, "Can't recall if patron has already checked out an item attached to this biblio" ); is( $biblio->can_be_recalled( { patron => $patron1 } ), 0, "Can't recall if on_shelf_recalls = all and items are still available" ); Koha::CirculationRules->set_rules( { branchcode => $branchcode, categorycode => $patron1->categorycode, itemtype => $item1->effective_itemtype, rules => { recalls_allowed => 1, recalls_per_record => 1, on_shelf_recalls => 'any', }, } ); C4::Circulation::AddReturn( $item2->barcode, $branchcode ); is( $biblio->can_be_recalled( { patron => $patron1 } ), 0, "Can't recall if no items are checked out" ); $recall2->set_cancelled; C4::Circulation::AddIssue( $patron2, $item2->barcode ); C4::Circulation::AddIssue( $patron2, $item1->barcode ); is( $biblio->can_be_recalled( { patron => $patron1 } ), 2, "Can recall two items" ); $item1->update( { withdrawn => 1 } ); is( $biblio->can_be_recalled( { patron => $patron1 } ), 1, "Can recall one item" ); $schema->storage->txn_rollback; }; subtest 'ill_requests() tests' => sub { plan tests => 3; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio; my $rs = $biblio->ill_requests; is( ref($rs), 'Koha::ILL::Requests' ); is( $rs->count, 0, 'No linked requests' ); foreach ( 1 .. 10 ) { $builder->build_object( { class => 'Koha::ILL::Requests', value => { biblio_id => $biblio->id } } ); } is( $biblio->ill_requests->count, 10, 'Linked requests are present' ); $schema->storage->txn_rollback; }; subtest 'item_groups() tests' => sub { plan tests => 6; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); my @item_groups = $biblio->item_groups->as_list; is( scalar(@item_groups), 0, 'Got zero item groups' ); my $item_group_1 = Koha::Biblio::ItemGroup->new( { biblio_id => $biblio->id } )->store(); @item_groups = $biblio->item_groups->as_list; is( scalar(@item_groups), 1, 'Got one item group' ); is( $item_groups[0]->id, $item_group_1->id, 'Got correct item group' ); my $item_group_2 = Koha::Biblio::ItemGroup->new( { biblio_id => $biblio->id } )->store(); @item_groups = $biblio->item_groups->as_list; is( scalar(@item_groups), 2, 'Got two item groups' ); is( $item_groups[0]->id, $item_group_1->id, 'Got correct item group 1' ); is( $item_groups[1]->id, $item_group_2->id, 'Got correct item group 2' ); $schema->storage->txn_rollback; }; subtest 'normalized_isbn() tests' => sub { plan tests => 1; $schema->storage->txn_begin; # We will move the tests from GetNormalizedISBN here when it will get replaced my $biblio = $builder->build_sample_biblio(); $biblio->biblioitem->set( { isbn => '9781250067128 | 125006712X' } )->store; is( $biblio->normalized_isbn, C4::Koha::GetNormalizedISBN( $biblio->biblioitem->isbn ), 'normalized_isbn is a wrapper around C4::Koha::GetNormalizedISBN' ); $schema->storage->txn_rollback; }; subtest 'normalized_upc() tests' => sub { plan tests => 1; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); is( $biblio->normalized_upc, $biblio->metadata_extractor->get_normalized_upc, 'normalized_upc delegates to the metadata extractor' ); $schema->storage->txn_rollback; }; subtest 'normalized_ean() tests' => sub { plan tests => 2; $schema->storage->txn_begin; t::lib::Mocks::mock_preference( 'marcflavour', 'MARC21' ); my $biblio = $builder->build_sample_biblio(); my $record = $biblio->metadata->record; $record->append_fields( MARC::Field->new( '024', '3', ' ', a => '978-3-16-148410-0' ) ); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = $biblio->get_from_storage; is( $biblio->normalized_ean, '9783161484100', 'normalized_ean returns EAN from 024 ind1=3' ); my $biblio_no_ean = $builder->build_sample_biblio(); is( $biblio_no_ean->normalized_ean, undef, 'normalized_ean returns undef when no EAN' ); $schema->storage->txn_rollback; }; subtest 'normalized_oclc() tests' => sub { plan tests => 2; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); is( $biblio->normalized_oclc, '', 'normalized_oclc returns empty string when no 035 with OCoLC' ); my $record = $biblio->metadata->record; $record->append_fields( MARC::Field->new( '035', ' ', ' ', a => '(OCoLC)902632762' ) ); C4::Biblio::ModBiblio( $record, $biblio->biblionumber ); $biblio = Koha::Biblios->find( $biblio->biblionumber ); is( $biblio->normalized_oclc, '902632762', 'normalized_oclc extracts OCLC number' ); $schema->storage->txn_rollback; }; subtest 'opac_suppressed() tests' => sub { plan tests => 4; $schema->storage->txn_begin; my $record = MARC::Record->new; $record->append_fields( MARC::Field->new( '245', '', '', a => 'Some title 1' ), MARC::Field->new( '942', '', '', n => '1' ), ); my ($biblio_id) = AddBiblio( $record, qw{} ); my $biblio = Koha::Biblios->find($biblio_id); ok( $biblio->opac_suppressed(), 'Record is suppressed' ); $record->field('942')->replace_with( MARC::Field->new( '942', '', '', n => '0' ) ); ($biblio_id) = AddBiblio( $record, qw{} ); $biblio = Koha::Biblios->find($biblio_id); ok( !$biblio->opac_suppressed(), 'Record is not suppressed' ); $record->field('942')->replace_with( MARC::Field->new( '942', '', '', n => '' ) ); ($biblio_id) = AddBiblio( $record, qw{} ); $biblio = Koha::Biblios->find($biblio_id); ok( !$biblio->opac_suppressed(), 'Record is not suppressed' ); $record->delete_field( $record->field('942') ); ($biblio_id) = AddBiblio( $record, qw{} ); $biblio = Koha::Biblios->find($biblio_id); ok( !$biblio->opac_suppressed(), 'Record is not suppressed' ); $schema->storage->txn_rollback; }; subtest 'ratings' => sub { plan tests => 1; $schema->storage->txn_begin; # See t/db_dependent/Koha/Ratings.t ok(1); $schema->storage->txn_rollback; }; subtest 'opac_summary_html' => sub { plan tests => 2; $schema->storage->txn_begin; my $author = 'my author'; my $title = 'my title'; my $isbn = '9781250067128 | 125006712X'; my $biblio = $builder->build_sample_biblio( { author => $author, title => $title } ); $biblio->biblioitem->set( { isbn => '9781250067128 | 125006712X' } )->store; t::lib::Mocks::mock_preference( 'OPACMySummaryHTML', '' ); is( $biblio->opac_summary_html, '', 'opac_summary_html returns empty string if pref is off' ); t::lib::Mocks::mock_preference( 'OPACMySummaryHTML', 'Replace {AUTHOR}, {TITLE}, {ISBN} AND {BIBLIONUMBER} please' ); is( $biblio->opac_summary_html, sprintf( 'Replace %s, %s, %s AND %s please', $author, $title, $biblio->normalized_isbn, $biblio->biblionumber ), 'opac_summary_html replaces the different patterns' ); $schema->storage->txn_rollback; }; subtest 'can_be_edited() tests' => sub { plan tests => 11; $schema->storage->txn_begin; my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { flags => 0 } } ); my $biblio = $builder->build_sample_biblio; my $source_allows_editing = 1; my $mock_metadata = Test::MockModule->new('Koha::Biblio::Metadata'); $mock_metadata->mock( 'source_allows_editing', sub { return $source_allows_editing; } ); ok( !$biblio->can_be_edited($patron), "Patron needs 'edit_catalog' subpermission" ); # Add editcatalogue => edit_catalog subpermission $builder->build( { source => 'UserPermission', value => { borrowernumber => $patron->id, module_bit => 9, # editcatalogue code => 'edit_catalogue', }, } ); ok( $biblio->can_be_edited($patron), "Patron with 'edit_catalogue' can edit" ); my $fa_biblio = $builder->build_sample_biblio( { frameworkcode => 'FA' } ); my $fa_patron = $builder->build_object( { class => 'Koha::Patrons', value => { flags => 0 } } ); my $is_fa_framework = $builder->build_object( { class => 'Koha::BiblioFrameworks', value => { is_fast_add => 1 } } ); my $is_fa_biblio = $builder->build_sample_biblio( { frameworkcode => $is_fa_framework->frameworkcode } ); # Add editcatalogue => edit_catalog subpermission $builder->build( { source => 'UserPermission', value => { borrowernumber => $fa_patron->id, module_bit => 9, # editcatalogue code => 'fast_cataloging', }, } ); ok( !$biblio->can_be_edited($fa_patron), "Fast add permissions are not enough" ); ok( $fa_biblio->can_be_edited($fa_patron), "Fast add user can edit FA records" ); ok( $fa_biblio->can_be_edited($patron), "edit_catalogue user can edit FA records" ); ok( $is_fa_biblio->can_be_edited($fa_patron), "Fast add user can edit is_fast_add records" ); ok( $is_fa_biblio->can_be_edited($patron), "edit_catalogue user can edit is_fast_add records" ); # Mock the record source doesn't allow direct editing $source_allows_editing = 0; ok( !$biblio->can_be_edited($patron), "Patron needs 'edit_locked_record' subpermission for locked records" ); # Add editcatalogue => edit_catalog subpermission $builder->build( { source => 'UserPermission', value => { borrowernumber => $patron->id, module_bit => 9, # editcatalogue code => 'edit_locked_records', }, } ); ok( $biblio->can_be_edited($patron), "Patron needs 'edit_locked_record' subpermission for locked records" ); throws_ok { $biblio->can_be_edited() } 'Koha::Exceptions::MissingParameter', 'Exception thrown on missing parameter'; my $potato = 'Potato'; throws_ok { $biblio->can_be_edited($potato) } 'Koha::Exceptions::MissingParameter', 'Exception thrown if parameter not a Koha::Patron reference'; $schema->storage->txn_rollback; }; sub component_record1 { my $marc = MARC::Record->new; $marc->append_fields( MARC::Field->new( '001', '3456' ), MARC::Field->new( '245', '', '', a => 'Some title 1' ), MARC::Field->new( '773', '', '', w => '(FIRST)1234' ), ); return $marc; } sub search_component_record1 { my @results = ( component_record1()->as_xml() ); return ( undef, { biblioserver => { RECORDS => \@results, hits => 1 } }, 1 ); } sub search_component_record2 { my @results; return ( undef, { biblioserver => { RECORDS => \@results, hits => 0 } }, 0 ); } sub host_record { my $marc = MARC::Record->new; $marc->append_fields( MARC::Field->new( '001', '1234' ), MARC::Field->new( '003', 'FIRST' ), MARC::Field->new( '245', '', '', a => 'Some title' ), ); return $marc; } subtest 'check_booking tests' => sub { plan tests => 7; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio(); my @items; for ( 0 .. 2 ) { my $item = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); push @items, $item; } my $can_book = $biblio->check_booking( { start_date => dt_from_string(), end_date => dt_from_string()->add( days => 7 ) } ); is( $can_book, 1, "True returned from Koha::Biblio->check_booking if there are no bookings" ); my $start_1 = dt_from_string()->subtract( days => 7 ); my $end_1 = dt_from_string()->subtract( days => 1 ); my $start_2 = dt_from_string(); my $end_2 = dt_from_string()->add( days => 7 ); # Past bookings my @bookings; for my $item (@items) { my $booking = $builder->build_object( { class => 'Koha::Bookings', value => { biblio_id => $biblio->biblionumber, item_id => $item->itemnumber, start_date => $start_1, end_date => $end_1 } } ); push @bookings, $booking; } $can_book = $biblio->check_booking( { start_date => dt_from_string(), end_date => dt_from_string()->add( days => 7 ), } ); is( $can_book, 1, "Koha::Biblio->check_booking returns true when we all existing bookings are in the past" ); # Current bookings my @current_bookings; for my $item (@items) { my $booking = $builder->build_object( { class => 'Koha::Bookings', value => { biblio_id => $biblio->biblionumber, item_id => $item->itemnumber, start_date => $start_2, end_date => $end_2 } } ); push @current_bookings, $booking; } $can_book = $biblio->check_booking( { start_date => dt_from_string(), end_date => dt_from_string()->add( days => 7 ), } ); is( $can_book, 0, "Koha::Biblio->check_booking returns false if the booking would conflict with existing bookings" ); $can_book = $biblio->check_booking( { start_date => dt_from_string(), end_date => dt_from_string()->add( days => 7 ), booking_id => $current_bookings[0]->booking_id } ); is( $can_book, 1, "Koha::Biblio->check_booking returns true if we pass the booking_id of one of the bookings that we would conflict with" ); # Cancelled booking $current_bookings[0]->update( { status => 'cancelled' } ); $can_book = $biblio->check_booking( { start_date => dt_from_string(), end_date => dt_from_string()->add( days => 7 ), } ); is( $can_book, 1, "Koha::Item->check_booking takes account of cancelled status in bookings check" ); my $patron_1 = $builder->build_object( { class => 'Koha::Patrons' } ); my $library = $builder->build_object( { class => 'Koha::Libraries' } ); t::lib::Mocks::mock_userenv( { branchcode => $library->id } ); # Create a a test biblio with 8 items my $new_biblio = $builder->build_object( { class => 'Koha::Biblios', value => { title => 'Test biblio with items' } } ); my @new_items; for ( 1 .. 8 ) { my $item = $builder->build_object( { class => 'Koha::Items', value => { homebranch => $library->branchcode, holdingbranch => $library->branchcode, biblionumber => $new_biblio->biblionumber, bookable => 1 } } ); push @new_items, $item; } my @item_numbers = map { $_->itemnumber } @new_items; my @item_barcodes = map { $_->barcode } @new_items; # Check-out all of those 6 items @item_barcodes = splice @item_barcodes, 0, 6; for my $item_barcode (@item_barcodes) { AddIssue( $patron_1, $item_barcode ); } @item_numbers = splice @item_numbers, 0, 6; my @new_bookings; for my $itemnumber (@item_numbers) { my $booking = $builder->build_object( { class => 'Koha::Bookings', value => { biblio_id => $new_biblio->biblionumber, item_id => $itemnumber, start_date => $start_2, end_date => $end_2, status => 'new' } } ); push @new_bookings, $booking; } # Place a booking on one of the 2 remaining items my $item = ( grep { $_->itemnumber ne $new_bookings[0]->item_id } @new_items )[0]; my $check_booking = $new_biblio->get_from_storage->check_booking( { start_date => $start_2, end_date => $end_2, item_id => $item->itemnumber } ); is( $check_booking, 1, "Koha::Biblio->check_booking returns true when we can book on an item" ); subtest 'checkouts on non-bookable items do not cause false clashes' => sub { plan tests => 2; my $nb_biblio = $builder->build_sample_biblio(); my $bookable_item = $builder->build_sample_item( { biblionumber => $nb_biblio->biblionumber, bookable => 1 } ); my $non_bookable_item = $builder->build_sample_item( { biblionumber => $nb_biblio->biblionumber, bookable => 0 } ); my $nb_patron = $builder->build_object( { class => 'Koha::Patrons' } ); # Check out the non-bookable item AddIssue( $nb_patron, $non_bookable_item->barcode ); my $nb_start = dt_from_string()->truncate( to => 'day' ); my $nb_end = $nb_start->clone->add( days => 7 ); my $can_book = $nb_biblio->check_booking( { start_date => $nb_start, end_date => $nb_end, } ); is( $can_book, 1, "Checkout on non-bookable item does not reduce availability" ); # Now also check out the bookable item to confirm it # correctly detects unavailability AddIssue( $nb_patron, $bookable_item->barcode ); $can_book = $nb_biblio->check_booking( { start_date => $nb_start, end_date => $nb_end, } ); is( $can_book, 0, "Checkout on bookable item correctly reduces availability" ); }; $schema->storage->txn_rollback; }; subtest '_unblessed_for_log() tests' => sub { plan tests => 8; $schema->storage->txn_begin; my $biblio = $builder->build_sample_biblio( { author => 'Test Author', title => 'Test Title' } ); my $data = $biblio->_unblessed_for_log; ok( ref $data eq 'HASH', '_unblessed_for_log returns a hashref' ); # Only non-MARC-derived Koha metadata fields should be present ok( exists $data->{frameworkcode}, 'frameworkcode (Koha metadata) is present' ); ok( !exists $data->{biblionumber}, 'biblionumber is excluded (internal ID)' ); ok( !exists $data->{title}, 'title is excluded (MARC-derived)' ); ok( !exists $data->{author}, 'author is excluded (MARC-derived)' ); ok( !exists $data->{timestamp}, 'timestamp is excluded' ); ok( !exists $data->{copyrightdate}, 'copyrightdate is excluded (MARC-derived)' ); ok( !exists $data->{abstract}, 'abstract is excluded (MARC-derived)' ); $schema->storage->txn_rollback; }; subtest 'CataloguingLog MARC-in-JSON diff tests' => sub { plan tests => 21; $schema->storage->txn_begin; t::lib::Mocks::mock_preference( 'CataloguingLog', 1 ); # --- ADD --- my $record = MARC::Record->new(); $record->leader('00000cam a2200000 a 4500'); $record->append_fields( MARC::Field->new( '245', '1', '0', a => 'Test Title', b => 'A Subtitle' ), MARC::Field->new( '100', '1', ' ', a => 'Test Author' ), ); my ($biblionumber) = AddBiblio( $record, '' ); my $add_log = Koha::ActionLogs->search( { object => $biblionumber, module => 'Cataloguing', action => 'ADD' } )->next; ok( defined $add_log->diff, 'ADD log populates the diff column' ); is( $add_log->info, 'biblio', 'ADD log info column is the bare "biblio" prefix (no pre-change JSON payload)' ); my $add_diff = from_json( $add_log->diff ); my $add_added = $add_diff->{D}{_marc}{A}; ok( defined $add_added, 'ADD diff contains _marc key' ); ok( exists $add_added->{leader}, '_marc has leader' ); ok( ref $add_added->{fields} eq 'ARRAY', '_marc.fields is an array' ); my ($f245) = grep { exists $_->{'245'} } @{ $add_added->{fields} }; ok( defined $f245, '245 field present in _marc.fields' ); is( $f245->{'245'}{ind1}, '1', '245 ind1 correct' ); is( $f245->{'245'}{subfields}[0]{a}, 'Test Title', '245 $a correct' ); my ($f100) = grep { exists $_->{'100'} } @{ $add_added->{fields} }; ok( defined $f100, '100 field present in _marc.fields' ); ok( !exists $add_diff->{D}{title}, 'MARC-derived title column absent from diff' ); ok( !exists $add_diff->{D}{author}, 'MARC-derived author column absent from diff' ); # --- MODIFY --- my $mod_record = MARC::Record->new(); $mod_record->leader('00000cam a2200000 a 4500'); $mod_record->append_fields( MARC::Field->new( '245', '1', '0', a => 'Changed Title' ) ); ModBiblio( $mod_record, $biblionumber, '' ); my $mod_log = Koha::ActionLogs->search( { object => $biblionumber, module => 'Cataloguing', action => 'MODIFY' } )->next; ok( defined $mod_log->diff, 'MODIFY log populates the diff column' ); like( $mod_log->info, qr/^biblio \{/, 'MODIFY log info column carries the pre-change JSON payload' ); my $mod_diff = from_json( $mod_log->diff ); ok( exists $mod_diff->{D}{_marc}, 'MODIFY diff contains _marc key' ); ok( !exists $mod_diff->{D}{title}, 'MARC-derived title column absent from MODIFY diff' ); ok( !exists $mod_diff->{D}{author}, 'MARC-derived author column absent from MODIFY diff' ); # --- DELETE --- DelBiblio( $biblionumber, { skip_record_index => 1 } ); my $del_log = Koha::ActionLogs->search( { object => $biblionumber, module => 'Cataloguing', action => 'DELETE' } )->next; ok( defined $del_log->diff, 'DELETE log populates the diff column' ); like( $del_log->info, qr/^biblio \{/, 'DELETE log info column carries the final-state JSON payload' ); my $del_diff = from_json( $del_log->diff ); my $del_removed = $del_diff->{D}{_marc}{R}; ok( defined $del_removed, 'DELETE diff contains _marc key' ); ok( exists $del_removed->{leader}, '_marc has leader in DELETE diff' ); ok( ref $del_removed->{fields} eq 'ARRAY', '_marc.fields is an array in DELETE diff' ); $schema->storage->txn_rollback; };