3 # Copyright 2016 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 => 16;
28 use C4::Circulation qw( AddIssue LostItem AddReturn );
30 use C4::Serials qw( NewIssue AddItem2Serial );
32 use Koha::Item::Transfer::Limits;
35 use Koha::DateUtils qw( dt_from_string );
38 use t::lib::TestBuilder;
42 my $schema = Koha::Database->new->schema;
43 $schema->storage->txn_begin;
45 my $dbh = C4::Context->dbh;
47 my $builder = t::lib::TestBuilder->new;
48 my $library = $builder->build( { source => 'Branch' } );
49 my $nb_of_items = Koha::Items->search->count;
50 my $biblio = $builder->build_sample_biblio();
51 my $new_item_1 = $builder->build_sample_item({
52 biblionumber => $biblio->biblionumber,
53 homebranch => $library->{branchcode},
54 holdingbranch => $library->{branchcode},
56 my $new_item_2 = $builder->build_sample_item({
57 biblionumber => $biblio->biblionumber,
58 homebranch => $library->{branchcode},
59 holdingbranch => $library->{branchcode},
63 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
65 like( $new_item_1->itemnumber, qr|^\d+$|, 'Adding a new item should have set the itemnumber' );
66 is( Koha::Items->search->count, $nb_of_items + 2, 'The 2 items should have been added' );
68 my $retrieved_item_1 = Koha::Items->find( $new_item_1->itemnumber );
69 is( $retrieved_item_1->barcode, $new_item_1->barcode, 'Find a item by id should return the correct item' );
71 subtest 'store' => sub {
74 my $biblio = $builder->build_sample_biblio;
75 my $today = dt_from_string->set( hour => 0, minute => 0, second => 0 );
76 my $item = Koha::Item->new(
78 homebranch => $library->{branchcode},
79 holdingbranch => $library->{branchcode},
80 biblionumber => $biblio->biblionumber,
83 )->store->get_from_storage;
85 is( t::lib::Dates::compare( $item->replacementpricedate, $today ),
86 0, 'replacementpricedate must have been set to today if not given' );
87 is( t::lib::Dates::compare( dt_from_string($item->datelastseen)->ymd, $today ),
88 0, 'datelastseen must have been set to today if not given' );
91 $biblio->biblioitem->itemtype,
92 'items.itype must have been set to biblioitem.itemtype is not given'
96 subtest 'permanent_location' => sub {
99 subtest 'location passed to ->store' => sub {
102 my $location = 'my_loc';
104 homebranch => $library->{branchcode},
105 holdingbranch => $library->{branchcode},
106 biblionumber => $biblio->biblionumber,
107 location => $location,
111 # NewItemsDefaultLocation not set
112 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', '' );
114 # Not passing permanent_location on creating the item
115 my $item = Koha::Item->new($attributes)->store->get_from_storage;
116 is( $item->location, $location,
117 'location must have been set to location if given' );
118 is( $item->permanent_location, $item->location,
119 'permanent_location must have been set to location if not given' );
122 # Passing permanent_location on creating the item
123 $item = Koha::Item->new(
124 { %$attributes, permanent_location => 'perm_loc' } )
125 ->store->get_from_storage;
126 is( $item->permanent_location, 'perm_loc',
127 'permanent_location must have been kept if given' );
132 # NewItemsDefaultLocation set
133 my $default_location = 'default_location';
134 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', $default_location );
136 # Not passing permanent_location on creating the item
137 my $item = Koha::Item->new($attributes)->store->get_from_storage;
138 is( $item->location, $location,
139 'location must have been kept if given' );
140 is( $item->permanent_location, $location,
141 'permanent_location must have been set to the location given' );
144 # Passing permanent_location on creating the item
145 $item = Koha::Item->new(
146 { %$attributes, permanent_location => 'perm_loc' } )
147 ->store->get_from_storage;
148 is( $item->location, $location,
149 'location must have been kept if given' );
150 is( $item->permanent_location, 'perm_loc',
151 'permanent_location must have been kept if given' );
156 subtest 'location NOT passed to ->store' => sub {
160 homebranch => $library->{branchcode},
161 holdingbranch => $library->{branchcode},
162 biblionumber => $biblio->biblionumber,
166 # NewItemsDefaultLocation not set
167 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', '' );
169 # Not passing permanent_location on creating the item
170 my $item = Koha::Item->new($attributes)->store->get_from_storage;
171 is( $item->location, undef,
172 'location not passed and no default, it is undef' );
173 is( $item->permanent_location, $item->location,
174 'permanent_location must have been set to location if not given' );
177 # Passing permanent_location on creating the item
178 $item = Koha::Item->new(
179 { %$attributes, permanent_location => 'perm_loc' } )
180 ->store->get_from_storage;
181 is( $item->permanent_location, 'perm_loc',
182 'permanent_location must have been kept if given' );
187 # NewItemsDefaultLocation set
188 my $default_location = 'default_location';
189 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', $default_location );
191 # Not passing permanent_location on creating the item
192 my $item = Koha::Item->new($attributes)->store->get_from_storage;
193 is( $item->location, $default_location,
194 'location must have been set to default location if not given' );
195 is( $item->permanent_location, $default_location,
196 'permanent_location must have been set to the default location as well' );
199 # Passing permanent_location on creating the item
200 $item = Koha::Item->new(
201 { %$attributes, permanent_location => 'perm_loc' } )
202 ->store->get_from_storage;
203 is( $item->location, $default_location,
204 'location must have been set to default location if not given' );
205 is( $item->permanent_location, 'perm_loc',
206 'permanent_location must have been kept if given' );
213 subtest '*_on updates' => sub {
216 # Once the '_on' value is set (triggered by the related field turning from false to true)
217 # it should not be re-set for any changes outside of the related field being 'unset'.
219 my @fields = qw( itemlost withdrawn damaged );
220 my $today = dt_from_string();
221 my $yesterday = $today->clone()->subtract( days => 1 );
223 for my $field ( @fields ) {
224 my $item = $builder->build_sample_item(
227 itemlost_on => undef,
229 withdrawn_on => undef,
234 my $field_on = $field . '_on';
236 # Set field for the first time
237 Time::Fake->offset( $yesterday->epoch );
238 $item->$field(1)->store;
239 $item->get_from_storage;
240 is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
241 0, $field_on . " was set upon first truthy setting" );
243 # Update the field to a new 'true' value
244 Time::Fake->offset( $today->epoch );
245 $item->$field(2)->store;
246 $item->get_from_storage;
247 is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
248 0, $field_on . " was not updated upon second truthy setting" );
250 # Update the field to a new 'false' value
251 $item->$field(0)->store;
252 $item->get_from_storage;
253 is($item->$field_on, undef, $field_on . " was unset upon untruthy setting");
259 subtest '_lost_found_trigger' => sub {
262 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
263 t::lib::Mocks::mock_preference( 'WhenLostForgiveFine', 0 );
265 my $processfee_amount = 20;
266 my $replacement_amount = 99.00;
267 my $item_type = $builder->build_object(
269 class => 'Koha::ItemTypes',
273 defaultreplacecost => undef,
274 processfee => $processfee_amount,
275 rentalcharge_daily => 0,
279 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
281 $biblio = $builder->build_sample_biblio( { author => 'Hall, Daria' } );
283 subtest 'Full write-off tests' => sub {
287 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
289 $builder->build_object( { class => "Koha::Patrons" } );
290 t::lib::Mocks::mock_userenv(
291 { patron => $manager, branchcode => $manager->branchcode } );
293 my $item = $builder->build_sample_item(
295 biblionumber => $biblio->biblionumber,
296 library => $library->branchcode,
297 replacementprice => $replacement_amount,
298 itype => $item_type->itemtype,
302 C4::Circulation::AddIssue( $patron, $item->barcode );
304 # Simulate item marked as lost
305 $item->itemlost(3)->store;
306 C4::Circulation::LostItem( $item->itemnumber, 1 );
308 my $processing_fee_lines = Koha::Account::Lines->search(
310 borrowernumber => $patron->id,
311 itemnumber => $item->itemnumber,
312 debit_type_code => 'PROCESSING'
315 is( $processing_fee_lines->count,
316 1, 'Only one processing fee produced' );
317 my $processing_fee_line = $processing_fee_lines->next;
318 is( $processing_fee_line->amount + 0,
320 'The right PROCESSING amount is generated' );
321 is( $processing_fee_line->amountoutstanding + 0,
323 'The right PROCESSING amountoutstanding is generated' );
325 my $lost_fee_lines = Koha::Account::Lines->search(
327 borrowernumber => $patron->id,
328 itemnumber => $item->itemnumber,
329 debit_type_code => 'LOST'
332 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
333 my $lost_fee_line = $lost_fee_lines->next;
334 is( $lost_fee_line->amount + 0,
335 $replacement_amount, 'The right LOST amount is generated' );
336 is( $lost_fee_line->amountoutstanding + 0,
338 'The right LOST amountoutstanding is generated' );
339 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
341 my $account = $patron->account;
342 my $debts = $account->outstanding_debits;
345 my $credit = $account->add_credit(
347 amount => $account->balance,
352 $credit->apply( { debits => [ $debts->as_list ] } );
354 # Simulate item marked as found
355 $item->itemlost(0)->store;
356 is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 0, 'No LOST_FOUND account line added' );
358 $lost_fee_line->discard_changes; # reload from DB
359 is( $lost_fee_line->amountoutstanding + 0,
360 0, 'Lost fee has no outstanding amount' );
361 is( $lost_fee_line->debit_type_code,
362 'LOST', 'Lost fee now still has account type of LOST' );
363 is( $lost_fee_line->status, 'FOUND',
364 "Lost fee now has account status of FOUND - No Refund" );
366 is( $patron->account->balance,
367 -0, 'The patron balance is 0, everything was written off' );
370 subtest 'Full payment tests' => sub {
374 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
376 my $item = $builder->build_sample_item(
378 biblionumber => $biblio->biblionumber,
379 library => $library->branchcode,
380 replacementprice => $replacement_amount,
381 itype => $item_type->itemtype
386 C4::Circulation::AddIssue( $patron, $item->barcode );
388 # Simulate item marked as lost
389 $item->itemlost(1)->store;
390 C4::Circulation::LostItem( $item->itemnumber, 1 );
392 my $processing_fee_lines = Koha::Account::Lines->search(
394 borrowernumber => $patron->id,
395 itemnumber => $item->itemnumber,
396 debit_type_code => 'PROCESSING'
399 is( $processing_fee_lines->count,
400 1, 'Only one processing fee produced' );
401 my $processing_fee_line = $processing_fee_lines->next;
402 is( $processing_fee_line->amount + 0,
404 'The right PROCESSING amount is generated' );
405 is( $processing_fee_line->amountoutstanding + 0,
407 'The right PROCESSING amountoutstanding is generated' );
409 my $lost_fee_lines = Koha::Account::Lines->search(
411 borrowernumber => $patron->id,
412 itemnumber => $item->itemnumber,
413 debit_type_code => 'LOST'
416 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
417 my $lost_fee_line = $lost_fee_lines->next;
418 is( $lost_fee_line->amount + 0,
419 $replacement_amount, 'The right LOST amount is generated' );
420 is( $lost_fee_line->amountoutstanding + 0,
422 'The right LOST amountountstanding is generated' );
424 my $account = $patron->account;
425 my $debts = $account->outstanding_debits;
428 my $credit = $account->add_credit(
430 amount => $account->balance,
435 $credit->apply( { debits => [ $debts->as_list ] } );
437 # Simulate item marked as found
438 $item->itemlost(0)->store;
439 is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
441 my $credit_return = Koha::Account::Lines->search(
443 itemnumber => $item->itemnumber,
444 credit_type_code => 'LOST_FOUND'
449 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
450 is( $credit_return->amount + 0,
452 'The account line of type LOST_FOUND has an amount of -99' );
454 $credit_return->amountoutstanding + 0,
456 'The account line of type LOST_FOUND has an amountoutstanding of -99'
459 my $processing_return = Koha::Account::Lines->search(
461 itemnumber => $item->itemnumber,
462 credit_type_code => 'PROCESSING_FOUND'
466 ok( $processing_return, 'An account line of type PROCESSING_FOUND is added' );
467 is( $processing_return->amount + 0,
469 'The account line of type PROCESSING_FOUND has an amount of -20' );
471 $lost_fee_line->discard_changes;
472 is( $lost_fee_line->amountoutstanding + 0,
473 0, 'Lost fee has no outstanding amount' );
474 is( $lost_fee_line->debit_type_code,
475 'LOST', 'Lost fee now still has account type of LOST' );
476 is( $lost_fee_line->status, 'FOUND',
477 "Lost fee now has account status of FOUND" );
479 is( $patron->account->balance, -119,
480 'The patron balance is -119, a credit that equals the lost fee payment and the processing fee'
484 subtest 'Test without payment or write off' => sub {
488 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
490 my $item = $builder->build_sample_item(
492 biblionumber => $biblio->biblionumber,
493 library => $library->branchcode,
494 replacementprice => 23.00,
495 replacementprice => $replacement_amount,
496 itype => $item_type->itemtype
501 C4::Circulation::AddIssue( $patron, $item->barcode );
503 # Simulate item marked as lost
504 $item->itemlost(3)->store;
505 C4::Circulation::LostItem( $item->itemnumber, 1 );
507 my $processing_fee_lines = Koha::Account::Lines->search(
509 borrowernumber => $patron->id,
510 itemnumber => $item->itemnumber,
511 debit_type_code => 'PROCESSING'
514 is( $processing_fee_lines->count,
515 1, 'Only one processing fee produced' );
516 my $processing_fee_line = $processing_fee_lines->next;
517 is( $processing_fee_line->amount + 0,
519 'The right PROCESSING amount is generated' );
520 is( $processing_fee_line->amountoutstanding + 0,
522 'The right PROCESSING amountoutstanding is generated' );
524 my $lost_fee_lines = Koha::Account::Lines->search(
526 borrowernumber => $patron->id,
527 itemnumber => $item->itemnumber,
528 debit_type_code => 'LOST'
531 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
532 my $lost_fee_line = $lost_fee_lines->next;
533 is( $lost_fee_line->amount + 0,
534 $replacement_amount, 'The right LOST amount is generated' );
535 is( $lost_fee_line->amountoutstanding + 0,
537 'The right LOST amountountstanding is generated' );
539 # Set processingreturn_policy to '0' so processing fee is retained
540 # these tests are just for lostreturn
541 my $processingreturn_rule = $builder->build(
543 source => 'CirculationRule',
546 categorycode => undef,
548 rule_name => 'processingreturn',
554 # Simulate item marked as found
555 $item->itemlost(0)->store;
556 is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
558 my $credit_return = Koha::Account::Lines->search(
560 itemnumber => $item->itemnumber,
561 credit_type_code => 'LOST_FOUND'
566 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
567 is( $credit_return->amount + 0,
569 'The account line of type LOST_FOUND has an amount of -99' );
571 $credit_return->amountoutstanding + 0,
573 'The account line of type LOST_FOUND has an amountoutstanding of 0'
576 $lost_fee_line->discard_changes;
577 is( $lost_fee_line->amountoutstanding + 0,
578 0, 'Lost fee has no outstanding amount' );
579 is( $lost_fee_line->debit_type_code,
580 'LOST', 'Lost fee now still has account type of LOST' );
581 is( $lost_fee_line->status, 'FOUND',
582 "Lost fee now has account status of FOUND" );
584 is( $patron->account->balance,
585 20, 'The patron balance is 20, still owes the processing fee' );
589 'Test with partial payment and write off, and remaining debt' =>
594 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 0 );
596 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
597 my $item = $builder->build_sample_item(
599 biblionumber => $biblio->biblionumber,
600 library => $library->branchcode,
601 replacementprice => $replacement_amount,
602 itype => $item_type->itemtype
607 C4::Circulation::AddIssue( $patron, $item->barcode );
609 # Simulate item marked as lost
610 $item->itemlost(1)->store;
611 C4::Circulation::LostItem( $item->itemnumber, 1 );
613 my $processing_fee_lines = Koha::Account::Lines->search(
615 borrowernumber => $patron->id,
616 itemnumber => $item->itemnumber,
617 debit_type_code => 'PROCESSING'
620 is( $processing_fee_lines->count,
621 1, 'Only one processing fee produced' );
622 my $processing_fee_line = $processing_fee_lines->next;
623 is( $processing_fee_line->amount + 0,
625 'The right PROCESSING amount is generated' );
626 is( $processing_fee_line->amountoutstanding + 0,
628 'The right PROCESSING amountoutstanding is generated' );
630 my $lost_fee_lines = Koha::Account::Lines->search(
632 borrowernumber => $patron->id,
633 itemnumber => $item->itemnumber,
634 debit_type_code => 'LOST'
637 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
638 my $lost_fee_line = $lost_fee_lines->next;
639 is( $lost_fee_line->amount + 0,
640 $replacement_amount, 'The right LOST amount is generated' );
641 is( $lost_fee_line->amountoutstanding + 0,
643 'The right LOST amountountstanding is generated' );
645 my $account = $patron->account;
648 $processfee_amount + $replacement_amount,
649 'Balance is PROCESSING + LOST'
652 # Partially pay fee (99 - 27 = 72)
653 my $payment_amount = 24;
654 my $payment = $account->add_credit(
656 amount => $payment_amount,
662 $payment->apply( { debits => [$lost_fee_line] } );
664 # Partially write off fee (72 - 20 = 52)
665 my $write_off_amount = 20;
666 my $write_off = $account->add_credit(
668 amount => $write_off_amount,
673 $write_off->apply( { debits => [$lost_fee_line] } );
676 my $payment_amount_2 = 3;
677 my $payment_2 = $account->add_credit(
679 amount => $payment_amount_2,
686 { debits => [$lost_fee_line] } );
688 # Partially write off fee (52 - 5 = 47)
689 my $write_off_amount_2 = 5;
690 my $write_off_2 = $account->add_credit(
692 amount => $write_off_amount_2,
699 { debits => [$lost_fee_line] } );
704 $replacement_amount -
709 'Balance is PROCESSING + LOST - PAYMENT 1 - WRITEOFF - PAYMENT 2 - WRITEOFF 2'
712 # VOID payment_2 and writeoff_2
713 $payment_2->void({ interface => 'test' });
714 $write_off_2->void({ interface => 'test' });
719 $replacement_amount -
722 'Balance is PROCESSING + LOST - PAYMENT 1 - WRITEOFF (PAYMENT 2 and WRITEOFF 2 VOIDED)'
725 # Store the amountoutstanding value
726 $lost_fee_line->discard_changes;
727 my $outstanding = $lost_fee_line->amountoutstanding;
730 $replacement_amount - $payment_amount - $write_off_amount,
731 "Lost Fee Outstanding is LOST - PAYMENT 1 - WRITEOFF"
734 # Simulate item marked as found
735 $item->itemlost(0)->store;
736 is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
738 my $credit_return = Koha::Account::Lines->search(
740 itemnumber => $item->itemnumber,
741 credit_type_code => 'LOST_FOUND'
746 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
750 $processfee_amount - $payment_amount,
751 'Balance is PROCESSING - PAYMENT (LOST_FOUND)'
754 $lost_fee_line->discard_changes;
755 is( $lost_fee_line->amountoutstanding + 0,
756 0, 'Lost fee has no outstanding amount' );
757 is( $lost_fee_line->debit_type_code,
758 'LOST', 'Lost fee now still has account type of LOST' );
759 is( $lost_fee_line->status, 'FOUND',
760 "Lost fee now has account status of FOUND" );
763 $credit_return->amount + 0,
764 ( $payment_amount + $outstanding ) * -1,
765 'The account line of type LOST_FOUND has an amount equal to the payment 1 + outstanding'
768 $credit_return->amountoutstanding + 0,
769 $payment_amount * -1,
770 'The account line of type LOST_FOUND has an amountoutstanding equal to the payment'
775 $processfee_amount - $payment_amount,
776 'The patron balance is the difference between the PROCESSING and the credit'
780 subtest 'Partial payment, existing debits and AccountAutoReconcile' =>
785 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
786 my $barcode = 'KD123456793';
787 my $replacement_amount = 100;
788 my $processfee_amount = 20;
790 my $item_type = $builder->build_object(
792 class => 'Koha::ItemTypes',
796 defaultreplacecost => undef,
798 rentalcharge_daily => 0,
802 my $item = Koha::Item->new(
804 biblionumber => $biblio->biblionumber,
805 homebranch => $library->branchcode,
806 holdingbranch => $library->branchcode,
808 replacementprice => $replacement_amount,
809 itype => $item_type->itemtype
814 C4::Circulation::AddIssue( $patron, $barcode );
816 # Simulate item marked as lost
817 $item->itemlost(1)->store;
818 C4::Circulation::LostItem( $item->itemnumber, 1 );
820 my $lost_fee_lines = Koha::Account::Lines->search(
822 borrowernumber => $patron->id,
823 itemnumber => $item->itemnumber,
824 debit_type_code => 'LOST'
827 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
828 my $lost_fee_line = $lost_fee_lines->next;
829 is( $lost_fee_line->amount + 0,
830 $replacement_amount, 'The right LOST amount is generated' );
831 is( $lost_fee_line->amountoutstanding + 0,
833 'The right LOST amountountstanding is generated' );
835 my $account = $patron->account;
836 is( $account->balance, $replacement_amount, 'Balance is L' );
839 my $payment_amount = 27;
840 my $payment = $account->add_credit(
842 amount => $payment_amount,
847 $payment->apply( { debits => [$lost_fee_line] } );
851 $replacement_amount - $payment_amount,
855 my $manual_debit_amount = 80;
858 amount => $manual_debit_amount,
866 $manual_debit_amount + $replacement_amount - $payment_amount,
867 'Manual debit applied'
870 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
872 # Simulate item marked as found
873 $item->itemlost(0)->store;
874 is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
876 my $credit_return = Koha::Account::Lines->search(
878 itemnumber => $item->itemnumber,
879 credit_type_code => 'LOST_FOUND'
884 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
888 $manual_debit_amount - $payment_amount,
889 'Balance is PROCESSING - payment (LOST_FOUND)'
892 my $manual_debit = Koha::Account::Lines->search(
894 borrowernumber => $patron->id,
895 debit_type_code => 'OVERDUE',
896 status => 'UNRETURNED'
900 $manual_debit->amountoutstanding + 0,
901 $manual_debit_amount - $payment_amount,
902 'reconcile_balance was called'
906 subtest 'Patron deleted' => sub {
909 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
910 my $barcode = 'KD123456794';
911 my $replacement_amount = 100;
912 my $processfee_amount = 20;
914 my $item_type = $builder->build_object(
916 class => 'Koha::ItemTypes',
920 defaultreplacecost => undef,
922 rentalcharge_daily => 0,
926 my $item = Koha::Item->new(
928 biblionumber => $biblio->biblionumber,
929 homebranch => $library->branchcode,
930 holdingbranch => $library->branchcode,
932 replacementprice => $replacement_amount,
933 itype => $item_type->itemtype
938 C4::Circulation::AddIssue( $patron, $barcode );
940 # Simulate item marked as lost
941 $item->itemlost(1)->store;
942 C4::Circulation::LostItem( $item->itemnumber, 1 );
947 # Simulate item marked as found
948 $item->itemlost(0)->store;
949 is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 0, 'No refund triggered' );
953 subtest 'restore fine | no overdue' => sub {
958 $builder->build_object( { class => "Koha::Patrons" } );
959 t::lib::Mocks::mock_userenv(
960 { patron => $manager, branchcode => $manager->branchcode } );
962 # Set lostreturn_policy to 'restore' for tests
963 my $specific_rule_restore = $builder->build(
965 source => 'CirculationRule',
967 branchcode => $manager->branchcode,
968 categorycode => undef,
970 rule_name => 'lostreturn',
971 rule_value => 'restore'
976 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
978 my $item = $builder->build_sample_item(
980 biblionumber => $biblio->biblionumber,
981 library => $library->branchcode,
982 replacementprice => $replacement_amount,
983 itype => $item_type->itemtype
988 C4::Circulation::AddIssue( $patron, $item->barcode );
990 # Simulate item marked as lost
991 $item->itemlost(1)->store;
992 C4::Circulation::LostItem( $item->itemnumber, 1 );
994 my $processing_fee_lines = Koha::Account::Lines->search(
996 borrowernumber => $patron->id,
997 itemnumber => $item->itemnumber,
998 debit_type_code => 'PROCESSING'
1001 is( $processing_fee_lines->count,
1002 1, 'Only one processing fee produced' );
1003 my $processing_fee_line = $processing_fee_lines->next;
1004 is( $processing_fee_line->amount + 0,
1006 'The right PROCESSING amount is generated' );
1007 is( $processing_fee_line->amountoutstanding + 0,
1009 'The right PROCESSING amountoutstanding is generated' );
1011 my $lost_fee_lines = Koha::Account::Lines->search(
1013 borrowernumber => $patron->id,
1014 itemnumber => $item->itemnumber,
1015 debit_type_code => 'LOST'
1018 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1019 my $lost_fee_line = $lost_fee_lines->next;
1020 is( $lost_fee_line->amount + 0,
1021 $replacement_amount, 'The right LOST amount is generated' );
1022 is( $lost_fee_line->amountoutstanding + 0,
1023 $replacement_amount,
1024 'The right LOST amountountstanding is generated' );
1026 my $account = $patron->account;
1027 my $debts = $account->outstanding_debits;
1030 my $credit = $account->add_credit(
1032 amount => $account->balance,
1034 interface => 'test',
1037 $credit->apply( { debits => [ $debts->as_list ] } );
1039 # Simulate item marked as found
1040 $item->itemlost(0)->store;
1041 is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1042 is( scalar ( grep { $_->message eq 'lost_restored' } @{$item->object_messages} ), 0, 'Restore not triggered when there is no overdue fine found' );
1045 subtest 'restore fine | unforgiven overdue' => sub {
1049 # Set lostreturn_policy to 'restore' for tests
1051 $builder->build_object( { class => "Koha::Patrons" } );
1052 t::lib::Mocks::mock_userenv(
1053 { patron => $manager, branchcode => $manager->branchcode } );
1054 my $specific_rule_restore = $builder->build(
1056 source => 'CirculationRule',
1058 branchcode => $manager->branchcode,
1059 categorycode => undef,
1061 rule_name => 'lostreturn',
1062 rule_value => 'restore'
1067 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1069 my $item = $builder->build_sample_item(
1071 biblionumber => $biblio->biblionumber,
1072 library => $library->branchcode,
1073 replacementprice => $replacement_amount,
1074 itype => $item_type->itemtype
1079 C4::Circulation::AddIssue( $patron, $item->barcode );
1081 # Simulate item marked as lost
1082 $item->itemlost(1)->store;
1083 C4::Circulation::LostItem( $item->itemnumber, 1 );
1085 my $processing_fee_lines = Koha::Account::Lines->search(
1087 borrowernumber => $patron->id,
1088 itemnumber => $item->itemnumber,
1089 debit_type_code => 'PROCESSING'
1092 is( $processing_fee_lines->count,
1093 1, 'Only one processing fee produced' );
1094 my $processing_fee_line = $processing_fee_lines->next;
1095 is( $processing_fee_line->amount + 0,
1097 'The right PROCESSING amount is generated' );
1098 is( $processing_fee_line->amountoutstanding + 0,
1100 'The right PROCESSING amountoutstanding is generated' );
1102 my $lost_fee_lines = Koha::Account::Lines->search(
1104 borrowernumber => $patron->id,
1105 itemnumber => $item->itemnumber,
1106 debit_type_code => 'LOST'
1109 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1110 my $lost_fee_line = $lost_fee_lines->next;
1111 is( $lost_fee_line->amount + 0,
1112 $replacement_amount, 'The right LOST amount is generated' );
1113 is( $lost_fee_line->amountoutstanding + 0,
1114 $replacement_amount,
1115 'The right LOST amountountstanding is generated' );
1117 my $account = $patron->account;
1118 my $debts = $account->outstanding_debits;
1121 my $credit = $account->add_credit(
1123 amount => $account->balance,
1125 interface => 'test',
1128 $credit->apply( { debits => [ $debts->as_list ] } );
1131 my $overdue = $account->add_debit(
1134 user_id => $manager->borrowernumber,
1135 library_id => $library->branchcode,
1136 interface => 'test',
1137 item_id => $item->itemnumber,
1141 $overdue->status('LOST')->store();
1142 $overdue->discard_changes;
1143 is( $overdue->status, 'LOST',
1144 'Overdue status set to LOST' );
1146 # Simulate item marked as found
1147 $item->itemlost(0)->store;
1148 is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1149 is( scalar ( grep { $_->message eq 'lost_restored' } @{$item->object_messages} ), 0, 'Restore not triggered when overdue was not forgiven' );
1150 $overdue->discard_changes;
1151 is( $overdue->status, 'FOUND',
1152 'Overdue status updated to FOUND' );
1155 subtest 'restore fine | forgiven overdue' => sub {
1159 # Set lostreturn_policy to 'restore' for tests
1161 $builder->build_object( { class => "Koha::Patrons" } );
1162 t::lib::Mocks::mock_userenv(
1163 { patron => $manager, branchcode => $manager->branchcode } );
1164 my $specific_rule_restore = $builder->build(
1166 source => 'CirculationRule',
1168 branchcode => $manager->branchcode,
1169 categorycode => undef,
1171 rule_name => 'lostreturn',
1172 rule_value => 'restore'
1177 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1179 my $item = $builder->build_sample_item(
1181 biblionumber => $biblio->biblionumber,
1182 library => $library->branchcode,
1183 replacementprice => $replacement_amount,
1184 itype => $item_type->itemtype
1189 C4::Circulation::AddIssue( $patron, $item->barcode );
1191 # Simulate item marked as lost
1192 $item->itemlost(1)->store;
1193 C4::Circulation::LostItem( $item->itemnumber, 1 );
1195 my $processing_fee_lines = Koha::Account::Lines->search(
1197 borrowernumber => $patron->id,
1198 itemnumber => $item->itemnumber,
1199 debit_type_code => 'PROCESSING'
1202 is( $processing_fee_lines->count,
1203 1, 'Only one processing fee produced' );
1204 my $processing_fee_line = $processing_fee_lines->next;
1205 is( $processing_fee_line->amount + 0,
1207 'The right PROCESSING amount is generated' );
1208 is( $processing_fee_line->amountoutstanding + 0,
1210 'The right PROCESSING amountoutstanding is generated' );
1212 my $lost_fee_lines = Koha::Account::Lines->search(
1214 borrowernumber => $patron->id,
1215 itemnumber => $item->itemnumber,
1216 debit_type_code => 'LOST'
1219 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1220 my $lost_fee_line = $lost_fee_lines->next;
1221 is( $lost_fee_line->amount + 0,
1222 $replacement_amount, 'The right LOST amount is generated' );
1223 is( $lost_fee_line->amountoutstanding + 0,
1224 $replacement_amount,
1225 'The right LOST amountountstanding is generated' );
1227 my $account = $patron->account;
1228 my $debts = $account->outstanding_debits;
1231 my $credit = $account->add_credit(
1233 amount => $account->balance,
1235 interface => 'test',
1238 $credit->apply( { debits => [ $debts->as_list ] } );
1241 my $overdue = $account->add_debit(
1244 user_id => $manager->borrowernumber,
1245 library_id => $library->branchcode,
1246 interface => 'test',
1247 item_id => $item->itemnumber,
1251 $overdue->status('LOST')->store();
1252 is( $overdue->status, 'LOST',
1253 'Overdue status set to LOST' );
1255 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 0 );
1258 $credit = $account->add_credit(
1261 user_id => $manager->borrowernumber,
1262 library_id => $library->branchcode,
1263 interface => 'test',
1265 item_id => $item->itemnumber
1268 $credit->apply( { debits => [$overdue] } );
1270 # Simulate item marked as found
1271 $item->itemlost(0)->store;
1272 is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1273 is( scalar ( grep { $_->message eq 'lost_restored' } @{$item->object_messages} ), 1, 'Restore triggered when overdue was forgiven' );
1274 $overdue->discard_changes;
1275 is( $overdue->status, 'FOUND', 'Overdue status updated to FOUND' );
1276 is( $overdue->amountoutstanding, $overdue->amount, 'Overdue outstanding has been restored' );
1277 $credit->discard_changes;
1278 is( $credit->status, 'VOID', 'Overdue Forgival has been marked as VOID');
1281 subtest 'Continue when userenv is not set' => sub {
1284 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1285 my $barcode = 'KD123456795';
1286 my $replacement_amount = 100;
1287 my $processfee_amount = 20;
1289 my $item_type = $builder->build_object(
1291 class => 'Koha::ItemTypes',
1293 notforloan => undef,
1295 defaultreplacecost => undef,
1297 rentalcharge_daily => 0,
1301 my $item = $builder->build_sample_item(
1303 biblionumber => $biblio->biblionumber,
1304 homebranch => $library->branchcode,
1305 holdingbranch => $library->branchcode,
1306 barcode => $barcode,
1307 replacementprice => $replacement_amount,
1308 itype => $item_type->itemtype
1313 C4::Circulation::AddIssue( $patron, $barcode );
1315 # Simulate item marked as lost
1316 $item->itemlost(1)->store;
1317 C4::Circulation::LostItem( $item->itemnumber, 1 );
1320 C4::Context->_new_userenv(undef);
1322 # Simluate item marked as found
1323 $item->itemlost(0)->store;
1324 is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1329 subtest 'log_action' => sub {
1331 t::lib::Mocks::mock_preference( 'CataloguingLog', 1 );
1333 my $item = Koha::Item->new(
1335 homebranch => $library->{branchcode},
1336 holdingbranch => $library->{branchcode},
1337 biblionumber => $biblio->biblionumber,
1338 location => 'my_loc',
1342 Koha::ActionLogs->search(
1344 module => 'CATALOGUING',
1346 object => $item->itemnumber,
1351 "Item creation logged"
1354 $item->location('another_loc')->store;
1356 Koha::ActionLogs->search(
1358 module => 'CATALOGUING',
1360 object => $item->itemnumber
1364 "Item modification logged"
1368 subtest 'itemlost / statistics' => sub { # TODO BZ 34308 (gt zero checks)
1371 my $item = $builder->build_sample_item;
1372 $item->itemlost(-1)->store; # weird value; >0 test not triggered ?
1373 is( Koha::Statistics->search( { itemnumber => $item->id } )->count, 0, 'No statistics added' );
1374 $item->itemlost(1)->store;
1375 is( Koha::Statistics->search( { itemnumber => $item->id } )->count, 1, 'statistics added' );
1376 $item->itemlost(2)->store;
1377 is( Koha::Statistics->search( { itemnumber => $item->id } )->count, 1, 'No statistics added, already lost' );
1378 $item->itemlost(-1)->store; # weird value; <=0 test triggered ?
1379 is( Koha::Statistics->search( { itemnumber => $item->id } )->count, 2, 'statistics added' );
1380 $item->itemlost(-2)->store; # weird value, but no status change
1381 is( Koha::Statistics->search( { itemnumber => $item->id } )->count, 2, 'No statistics added, already *found*' );
1385 subtest 'get_transfer' => sub {
1388 my $transfer = $new_item_1->get_transfer();
1389 is( $transfer, undef, 'Koha::Item->get_transfer should return undef if the item is not in transit' );
1391 my $library_to = $builder->build( { source => 'Branch' } );
1393 my $transfer_1 = $builder->build_object(
1395 class => 'Koha::Item::Transfers',
1397 itemnumber => $new_item_1->itemnumber,
1398 frombranch => $new_item_1->holdingbranch,
1399 tobranch => $library_to->{branchcode},
1402 datearrived => undef,
1403 datecancelled => undef,
1404 daterequested => \'NOW()'
1409 $transfer = $new_item_1->get_transfer();
1410 is( ref($transfer), 'Koha::Item::Transfer', 'Koha::Item->get_transfer should return a Koha::Item::Transfer object' );
1412 my $transfer_2 = $builder->build_object(
1414 class => 'Koha::Item::Transfers',
1416 itemnumber => $new_item_1->itemnumber,
1417 frombranch => $new_item_1->holdingbranch,
1418 tobranch => $library_to->{branchcode},
1421 datearrived => undef,
1422 datecancelled => undef,
1423 daterequested => \'NOW()'
1428 $transfer = $new_item_1->get_transfer();
1429 is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the oldest transfer request');
1431 $transfer_2->datesent(\'NOW()')->store;
1432 $transfer = $new_item_1->get_transfer();
1433 is( $transfer->branchtransfer_id, $transfer_2->branchtransfer_id, 'Koha::Item->get_transfer returns the in_transit transfer');
1435 my $transfer_3 = $builder->build_object(
1437 class => 'Koha::Item::Transfers',
1439 itemnumber => $new_item_1->itemnumber,
1440 frombranch => $new_item_1->holdingbranch,
1441 tobranch => $library_to->{branchcode},
1444 datearrived => undef,
1445 datecancelled => undef,
1446 daterequested => \'NOW()'
1451 $transfer_2->datearrived(\'NOW()')->store;
1452 $transfer = $new_item_1->get_transfer();
1453 is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the next queued transfer');
1454 is( $transfer->itemnumber, $new_item_1->itemnumber, 'Koha::Item->get_transfer returns the right items transfer' );
1456 $transfer_1->datecancelled(\'NOW()')->store;
1457 $transfer = $new_item_1->get_transfer();
1458 is( $transfer->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfer ignores cancelled transfers');
1461 subtest 'holds' => sub {
1464 my $biblio = $builder->build_sample_biblio();
1465 my $item = $builder->build_sample_item({
1466 biblionumber => $biblio->biblionumber,
1468 is($item->holds->count, 0, "Nothing returned if no holds");
1469 my $hold1 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'T' }});
1470 my $hold2 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1471 my $hold3 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1473 is($item->holds()->count,3,"Three holds found");
1474 is($item->holds({found => 'W'})->count,2,"Two waiting holds found");
1475 is_deeply($item->holds({found => 'T'})->next->unblessed,$hold1,"Found transit holds matches the hold");
1476 is($item->holds({found => undef})->count, 0,"Nothing returned if no matching holds");
1479 subtest 'biblio' => sub {
1482 my $biblio = $retrieved_item_1->biblio;
1483 is( ref( $biblio ), 'Koha::Biblio', 'Koha::Item->biblio should return a Koha::Biblio' );
1484 is( $biblio->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblio should return the correct biblio' );
1487 subtest 'biblioitem' => sub {
1490 my $biblioitem = $retrieved_item_1->biblioitem;
1491 is( ref( $biblioitem ), 'Koha::Biblioitem', 'Koha::Item->biblioitem should return a Koha::Biblioitem' );
1492 is( $biblioitem->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblioitem should return the correct biblioitem' );
1496 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
1497 subtest 'checkout' => sub {
1499 my $item = Koha::Items->find( $new_item_1->itemnumber );
1501 my $checkout = $item->checkout;
1502 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no current checkout on this item' );
1505 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1506 C4::Circulation::AddIssue( $patron, $item->barcode );
1507 $checkout = $retrieved_item_1->checkout;
1508 is( ref( $checkout ), 'Koha::Checkout', 'Koha::Item->checkout should return a Koha::Checkout' );
1509 is( $checkout->itemnumber, $item->itemnumber, 'Koha::Item->checkout should return the correct checkout' );
1510 is( $checkout->borrowernumber, $patron->borrowernumber, 'Koha::Item->checkout should return the correct checkout' );
1513 C4::Circulation::AddReturn( $item->barcode );
1515 # There is no more checkout on this item, making sure it will not return old checkouts
1516 $checkout = $item->checkout;
1517 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no *current* checkout on this item' );
1520 subtest 'can_be_transferred' => sub {
1523 t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
1524 t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
1526 my $biblio = $builder->build_sample_biblio();
1527 my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
1528 my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
1529 my $item = $builder->build_sample_item({
1530 biblionumber => $biblio->biblionumber,
1531 homebranch => $library1->branchcode,
1532 holdingbranch => $library1->branchcode,
1535 is(Koha::Item::Transfer::Limits->search({
1536 fromBranch => $library1->branchcode,
1537 toBranch => $library2->branchcode,
1538 })->count, 0, 'There are no transfer limits between libraries.');
1539 ok($item->can_be_transferred({ to => $library2 }),
1540 'Item can be transferred between libraries.');
1542 my $limit = Koha::Item::Transfer::Limit->new({
1543 fromBranch => $library1->branchcode,
1544 toBranch => $library2->branchcode,
1545 itemtype => $item->effective_itemtype,
1547 is(Koha::Item::Transfer::Limits->search({
1548 fromBranch => $library1->branchcode,
1549 toBranch => $library2->branchcode,
1550 })->count, 1, 'Given we have added a transfer limit,');
1551 is($item->can_be_transferred({ to => $library2 }), 0,
1552 'Item can no longer be transferred between libraries.');
1553 is($item->can_be_transferred({ to => $library2, from => $library1 }), 0,
1554 'We get the same result also if we pass the from-library parameter.');
1557 # Reset nb_of_items prior to testing delete
1558 $nb_of_items = Koha::Items->search->count;
1561 $retrieved_item_1->delete;
1562 is( Koha::Items->search->count, $nb_of_items - 1, 'Delete should have deleted the item' );
1564 $schema->storage->txn_rollback;
1566 subtest 'filter_by_visible_in_opac() tests' => sub {
1570 $schema->storage->txn_begin;
1572 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1573 my $mocked_category = Test::MockModule->new('Koha::Patron::Category');
1575 $mocked_category->mock( 'override_hidden_items', sub {
1579 # have a fresh biblio
1580 my $biblio = $builder->build_sample_biblio;
1581 # have two itemtypes
1582 my $itype_1 = $builder->build_object({ class => 'Koha::ItemTypes' });
1583 my $itype_2 = $builder->build_object({ class => 'Koha::ItemTypes' });
1584 # have 5 items on that biblio
1585 my $item_1 = $builder->build_sample_item(
1587 biblionumber => $biblio->biblionumber,
1589 itype => $itype_1->itemtype,
1594 my $item_2 = $builder->build_sample_item(
1596 biblionumber => $biblio->biblionumber,
1598 itype => $itype_2->itemtype,
1603 my $item_3 = $builder->build_sample_item(
1605 biblionumber => $biblio->biblionumber,
1607 itype => $itype_1->itemtype,
1612 my $item_4 = $builder->build_sample_item(
1614 biblionumber => $biblio->biblionumber,
1616 itype => $itype_2->itemtype,
1621 my $item_5 = $builder->build_sample_item(
1623 biblionumber => $biblio->biblionumber,
1625 itype => $itype_1->itemtype,
1630 my $item_6 = $builder->build_sample_item(
1632 biblionumber => $biblio->biblionumber,
1634 itype => $itype_1->itemtype,
1642 my $mocked_context = Test::MockModule->new('C4::Context');
1643 $mocked_context->mock( 'yaml_preference', sub {
1647 t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
1648 is( $biblio->items->filter_by_visible_in_opac->count,
1649 6, 'No rules passed, hidelostitems unset' );
1651 is( $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1652 6, 'No rules passed, hidelostitems unset, patron exception changes nothing' );
1654 $rules = { copynumber => [ 2 ] };
1656 t::lib::Mocks::mock_preference( 'hidelostitems', 1 );
1658 $biblio->items->filter_by_visible_in_opac->count,
1660 'No rules passed, hidelostitems set'
1664 $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1666 'No rules passed, hidelostitems set, patron exception changes nothing'
1669 $rules = { biblionumber => [ $biblio->biblionumber ] };
1671 $biblio->items->filter_by_visible_in_opac->count,
1673 'Biblionumber rule successfully hides all items'
1676 my $biblio2 = $builder->build_sample_biblio;
1677 $rules = { biblionumber => [ $biblio2->biblionumber ] };
1678 my $prefetched = $biblio->items->search({},{ prefetch => ['branchtransfers','reserves'] })->filter_by_visible_in_opac;
1679 ok( $prefetched->next, "Can retrieve object when prefetching and hiding on a duplicated column");
1681 $rules = { withdrawn => [ 1, 2 ], copynumber => [ 2 ] };
1683 $biblio->items->filter_by_visible_in_opac->count,
1685 'Rules on withdrawn, hidelostitems set'
1689 $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1691 'hidelostitems set, rules on withdrawn but patron override passed'
1694 $rules = { itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1696 $biblio->items->filter_by_visible_in_opac->count,
1698 'Rules on itype, hidelostitems set'
1701 $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1703 $biblio->items->filter_by_visible_in_opac->count,
1705 'Rules on itype and withdrawn, hidelostitems set'
1708 $biblio->items->filter_by_visible_in_opac
1710 $item_4->itemnumber,
1711 'The right item is returned'
1714 $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_2->itemtype ], copynumber => [ 2 ] };
1716 $biblio->items->filter_by_visible_in_opac->count,
1718 'Rules on itype and withdrawn, hidelostitems set'
1721 $biblio->items->filter_by_visible_in_opac
1723 $item_5->itemnumber,
1724 'The right item is returned'
1727 # Make sure the warning on the about page will work
1728 $rules = { itemlost => ['AB'] };
1729 my $c = Koha::Items->filter_by_visible_in_opac->count;
1730 my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
1731 like( $warnings[2], qr/Truncated incorrect (DOUBLE|DECIMAL) value: 'AB'/);
1733 $schema->storage->txn_rollback;
1736 subtest 'filter_out_lost() tests' => sub {
1740 $schema->storage->txn_begin;
1742 # have a fresh biblio
1743 my $biblio = $builder->build_sample_biblio;
1744 # have 3 items on that biblio
1745 my $item_1 = $builder->build_sample_item(
1747 biblionumber => $biblio->biblionumber,
1751 my $item_2 = $builder->build_sample_item(
1753 biblionumber => $biblio->biblionumber,
1757 my $item_3 = $builder->build_sample_item(
1759 biblionumber => $biblio->biblionumber,
1764 is( $biblio->items->filter_out_lost->next->itemnumber, $item_2->itemnumber, 'Right item returned' );
1765 is( $biblio->items->filter_out_lost->count, 1, 'Only one item is not lost' );
1767 $schema->storage->txn_rollback;
1770 subtest 'move_to_biblio() tests' => sub {
1774 $schema->storage->txn_begin;
1776 my $biblio1 = $builder->build_sample_biblio;
1777 my $biblio2 = $builder->build_sample_biblio;
1778 my $item1 = $builder->build_sample_item({ biblionumber => $biblio1->biblionumber });
1779 my $item2 = $builder->build_sample_item({ biblionumber => $biblio1->biblionumber });
1781 $biblio1->items->move_to_biblio($biblio2);
1783 $item1->discard_changes;
1784 $item2->discard_changes;
1786 is($item1->biblionumber, $biblio2->biblionumber, "Item 1 moved");
1787 is($item2->biblionumber, $biblio2->biblionumber, "Item 2 moved");
1789 $schema->storage->txn_rollback;
1793 subtest 'search_ordered' => sub {
1797 $schema->storage->txn_begin;
1799 my $library_a = $builder->build_object(
1800 { class => 'Koha::Libraries', value => { branchname => 'TEST_A' } } );
1801 my $library_z = $builder->build_object(
1802 { class => 'Koha::Libraries', value => { branchname => 'TEST_Z' } } );
1803 my $biblio = $builder->build_sample_biblio( { serial => 0 } );
1804 my $item1 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1805 my $item2 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1806 my $item3 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1810 # order_by homebranch.branchname
1811 $item1->discard_changes->update( { homebranch => $library_z->branchcode } );
1812 $item2->discard_changes->update( { homebranch => $library_a->branchcode } );
1813 $item3->discard_changes->update( { homebranch => $library_z->branchcode } );
1814 is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1815 [ $item2->itemnumber, $item1->itemnumber, $item3->itemnumber ],
1816 "not a serial - order by homebranch" );
1818 # order_by me.enumchron
1819 $biblio->items->update( { homebranch => $library_a->branchcode } );
1820 $item1->discard_changes->update( { enumchron => 'cc' } );
1821 $item2->discard_changes->update( { enumchron => 'bb' } );
1822 $item3->discard_changes->update( { enumchron => 'aa' } );
1823 is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1824 [ $item3->itemnumber, $item2->itemnumber, $item1->itemnumber ],
1825 "not a serial - order by enumchron" );
1827 # order_by LPAD( me.copynumber, 8, '0' )
1828 $biblio->items->update( { enumchron => undef } );
1829 $item1->discard_changes->update( { copynumber => '12345678' } );
1830 $item2->discard_changes->update( { copynumber => '34567890' } );
1831 $item3->discard_changes->update( { copynumber => '23456789' } );
1832 is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1833 [ $item1->itemnumber, $item3->itemnumber, $item2->itemnumber ],
1834 "not a serial - order by LPAD( me.copynumber, 8, '0' )" );
1836 # order_by -desc => 'me.dateaccessioned'
1837 $biblio->items->update( { copynumber => undef } );
1838 $item1->discard_changes->update( { dateaccessioned => '2022-08-19' } );
1839 $item2->discard_changes->update( { dateaccessioned => '2022-07-19' } );
1840 $item3->discard_changes->update( { dateaccessioned => '2022-09-19' } );
1841 is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1842 [ $item3->itemnumber, $item1->itemnumber, $item2->itemnumber ],
1843 "not a serial - order by date accessioned desc" );
1848 my $sub_freq = $builder->build( { source => 'SubscriptionFrequency' } );
1850 $builder->build( { source => 'SubscriptionNumberpattern' } );
1851 my $subscription = $builder->build_object(
1853 class => 'Koha::Subscriptions',
1855 biblionumber => $biblio->biblionumber,
1856 periodicity => $sub_freq->{id},
1857 numberpattern => $sub_np->{id},
1858 published_on_template => "[% publisheddatetext %] [% biblionumber %]",
1862 $builder->build_object(
1864 class => 'Koha::Subscription::Histories',
1866 subscriptionid => $subscription->subscriptionid,
1867 biblionumber => $biblio->biblionumber
1872 $biblio->update( { serial => 1 } );
1874 C4::Serials::NewIssue( "serialseq", $subscription->subscriptionid,
1875 $biblio->biblionumber, 1, undef, undef, "publisheddatetext",
1876 "notes", "routingnotes" );
1877 C4::Serials::AddItem2Serial( $serialid1, $item1->itemnumber );
1879 C4::Serials::NewIssue( "serialseq", $subscription->subscriptionid,
1880 $biblio->biblionumber, 1, undef, undef, "publisheddatetext",
1881 "notes", "routingnotes" );
1882 C4::Serials::AddItem2Serial( $serialid2, $item2->itemnumber );
1884 C4::Serials::NewIssue( "serialseq", $subscription->subscriptionid,
1885 $biblio->biblionumber, 1, undef, undef, "publisheddatetext",
1886 "notes", "routingnotes" );
1887 C4::Serials::AddItem2Serial( $serialid3, $item3->itemnumber );
1888 my $serial1 = Koha::Serials->find($serialid1);
1889 my $serial2 = Koha::Serials->find($serialid2);
1890 my $serial3 = Koha::Serials->find($serialid3);
1892 # order_by serial.publisheddate
1893 $serial1->discard_changes->update( { publisheddate => '2022-09-19' } );
1894 $serial2->discard_changes->update( { publisheddate => '2022-07-19' } );
1895 $serial3->discard_changes->update( { publisheddate => '2022-08-19' } );
1897 [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1898 [ $item2->itemnumber, $item3->itemnumber, $item1->itemnumber ],
1899 "serial - order by publisheddate"
1902 # order_by me.enumchron
1903 $serial1->discard_changes->update({ publisheddate => '2022-08-19' });
1904 $serial2->discard_changes->update({ publisheddate => '2022-08-19' });
1905 $serial3->discard_changes->update({ publisheddate => '2022-08-19' });
1906 $item1->discard_changes->update( { enumchron => 'cc' } );
1907 $item2->discard_changes->update( { enumchron => 'bb' } );
1908 $item3->discard_changes->update( { enumchron => 'aa' } );
1909 is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1910 [ $item3->itemnumber, $item2->itemnumber, $item1->itemnumber ],
1911 "serial - order by enumchron" );
1913 is( $serial1->publisheddatetext, "publisheddatetext " . $biblio->biblionumber, "Column publisheddatetext rendered correctly from template for serial1" );
1914 is( $serial2->publisheddatetext, "publisheddatetext " . $biblio->biblionumber, "Column publisheddatetext rendered correctly from template for serial2" );
1915 is( $serial3->publisheddatetext, "publisheddatetext " . $biblio->biblionumber, "Column publisheddatetext rendered correctly from template for serial3" );
1919 $schema->storage->txn_rollback;
1923 subtest 'filter_by_for_hold' => sub {
1927 $schema->storage->txn_begin;
1929 my $biblio = $builder->build_sample_biblio;
1930 my $library = $builder->build_object({ class => 'Koha::Libraries' });
1932 t::lib::Mocks::mock_preference('IndependentBranches', 0); # more robust tests
1934 is( $biblio->items->filter_by_for_hold->count, 0, 'no item yet' );
1935 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 1 } );
1936 is( $biblio->items->filter_by_for_hold->count, 0, 'no item for hold' );
1937 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 0 } );
1938 is( $biblio->items->filter_by_for_hold->count, 1, '1 item for hold' );
1939 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => -1 } );
1940 is( $biblio->items->filter_by_for_hold->count, 2, '2 items for hold' );
1942 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 0, library => $library->id } );
1943 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 1, library => $library->id } );
1944 is( $biblio->items->filter_by_for_hold->count, 3, '3 items for hold - itemlost' );
1946 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 0, library => $library->id } );
1947 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 1, library => $library->id } );
1948 is( $biblio->items->filter_by_for_hold->count, 4, '4 items for hold - withdrawn' );
1950 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, damaged => 0 } );
1951 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, damaged => 1 } );
1952 t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0);
1953 is( $biblio->items->filter_by_for_hold->count, 5, '5 items for hold - not damaged if not AllowHoldsOnDamagedItems' );
1954 t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1);
1955 is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - damaged if AllowHoldsOnDamagedItems' );
1957 my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' });
1958 my $not_holdable_itemtype = $itemtype->itemtype;
1959 $builder->build_sample_item(
1961 biblionumber => $biblio->biblionumber,
1962 itype => $not_holdable_itemtype,
1965 Koha::CirculationRules->set_rule(
1967 branchcode => undef,
1968 itemtype => $not_holdable_itemtype,
1969 rule_name => 'holdallowed',
1970 rule_value => 'not_allowed',
1973 is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - holdallowed=not_allowed' );
1975 # Remove rule, test notforloan on itemtype
1976 Koha::CirculationRules->set_rule(
1978 branchcode => undef,
1979 itemtype => $not_holdable_itemtype,
1980 rule_name => 'holdallowed',
1981 rule_value => undef,
1984 is( $biblio->items->filter_by_for_hold->count, 7, '7 items for hold - rule deleted' );
1985 $itemtype->notforloan(1)->store;
1986 is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - notforloan' );
1989 my $mock_context = Test::MockModule->new('C4::Context');
1990 $mock_context->mock( 'only_my_library', 1 );
1991 $mock_context->mock( 'mybranch', $library->id );
1992 is( $biblio->items->filter_by_for_hold->count, 2, '2 items for hold, filtered by IndependentBranches' );
1995 t::lib::Mocks::mock_preference('item-level_itypes', 0);
1996 $biblio->biblioitem->itemtype($not_holdable_itemtype)->store;
1997 is( $biblio->items->filter_by_for_hold->count, 0, '0 item-level_itypes=0' );
1999 t::lib::Mocks::mock_preference('item-level_itypes', 1);
2001 $schema->storage->txn_rollback;