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 => 20;
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',
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 'serial_item' => sub {
1389 $schema->storage->txn_begin;
1391 my $item = $builder->build_sample_item;
1393 $builder->build_object( { class => 'Koha::Serial::Items', value => { itemnumber => $item->itemnumber } } );
1394 is( ref( $item->serial_item ), 'Koha::Serial::Item' );
1395 is( $item->serial_item->itemnumber, $item->itemnumber );
1397 is( ref( $item->serial_item->serial ), 'Koha::Serial', 'Koha::Serial::Item->serial returns a Koha::Serial object' );
1399 $schema->storage->txn_rollback;
1403 subtest 'item_group_item' => sub {
1407 $schema->storage->txn_begin;
1409 my $biblio = $builder->build_sample_biblio();
1410 my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
1411 my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
1413 my $item_group = Koha::Biblio::ItemGroup->new( { biblio_id => $biblio->id } )->store();
1414 $item_group->add_item( { item_id => $item_1->itemnumber } );
1417 ref( $item_1->item_group_item ), 'Koha::Biblio::ItemGroup::Item',
1418 '->item_group_item should return a Koha::Biblio::ItemGroup::Item object'
1420 is( $item_1->item_group_item->item_id, $item_1->itemnumber, '->item_group_item should return the correct item' );
1422 $schema->storage->txn_rollback;
1426 subtest 'course_item' => sub {
1430 $schema->storage->txn_begin;
1432 my $item = $builder->build_sample_item;
1434 $builder->build_object( { class => 'Koha::Course::Items', value => { itemnumber => $item->itemnumber } } );
1435 is( ref( $item->course_item ), 'Koha::Course::Item', '->course_item should return a Koha::Course::Item object' );
1436 is( $item->course_item->ci_id, $course_item->ci_id, '->course_item should return the correct object' );
1438 $schema->storage->txn_rollback;
1442 subtest 'get_transfer|transfer' => sub {
1445 my $transfer = $new_item_1->get_transfer();
1446 is( $transfer, undef, 'Koha::Item->get_transfer should return undef if the item is not in transit' );
1447 is( $new_item_1->transfer, undef );
1449 my $library_to = $builder->build( { source => 'Branch' } );
1451 my $transfer_1 = $builder->build_object(
1453 class => 'Koha::Item::Transfers',
1455 itemnumber => $new_item_1->itemnumber,
1456 frombranch => $new_item_1->holdingbranch,
1457 tobranch => $library_to->{branchcode},
1460 datearrived => undef,
1461 datecancelled => undef,
1462 daterequested => \'NOW()'
1467 $transfer = $new_item_1->get_transfer();
1468 is( ref($transfer), 'Koha::Item::Transfer', 'Koha::Item->get_transfer should return a Koha::Item::Transfer object' );
1469 is( ref($new_item_1->transfer), 'Koha::Item::Transfer' );
1471 my $transfer_2 = $builder->build_object(
1473 class => 'Koha::Item::Transfers',
1475 itemnumber => $new_item_1->itemnumber,
1476 frombranch => $new_item_1->holdingbranch,
1477 tobranch => $library_to->{branchcode},
1480 datearrived => undef,
1481 datecancelled => undef,
1482 daterequested => \'NOW()'
1487 $transfer = $new_item_1->get_transfer();
1488 is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the oldest transfer request');
1490 $transfer_2->datesent(\'NOW()')->store;
1491 $transfer = $new_item_1->get_transfer();
1492 is( $transfer->branchtransfer_id, $transfer_2->branchtransfer_id, 'Koha::Item->get_transfer returns the in_transit transfer');
1494 my $transfer_3 = $builder->build_object(
1496 class => 'Koha::Item::Transfers',
1498 itemnumber => $new_item_1->itemnumber,
1499 frombranch => $new_item_1->holdingbranch,
1500 tobranch => $library_to->{branchcode},
1503 datearrived => undef,
1504 datecancelled => undef,
1505 daterequested => \'NOW()'
1510 $transfer_2->datearrived(\'NOW()')->store;
1511 $transfer = $new_item_1->get_transfer();
1512 is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the next queued transfer');
1513 is( $transfer->itemnumber, $new_item_1->itemnumber, 'Koha::Item->get_transfer returns the right items transfer' );
1515 $transfer_1->datecancelled(\'NOW()')->store;
1516 $transfer = $new_item_1->get_transfer();
1517 is( $transfer->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfer ignores cancelled transfers');
1520 subtest 'holds' => sub {
1523 my $biblio = $builder->build_sample_biblio();
1524 my $item = $builder->build_sample_item(
1526 biblionumber => $biblio->biblionumber,
1529 is( $item->holds->count, 0, "Nothing returned if no holds" );
1530 is( $item->first_hold, undef, 'No hold yet' );
1531 my $yesterday = dt_from_string->subtract( days => 1 )->ymd;
1532 my $hold1 = $builder->build(
1534 source => 'Reserve',
1536 itemnumber => $item->itemnumber, found => 'T', reservedate => $yesterday, suspend => 0, priority => 2
1540 my $hold2 = $builder->build(
1542 source => 'Reserve',
1544 itemnumber => $item->itemnumber, found => 'W', reservedate => $yesterday, suspend => 0, priority => 1
1548 my $hold3 = $builder->build(
1550 source => 'Reserve',
1552 itemnumber => $item->itemnumber, found => 'W', reservedate => $yesterday, suspend => 0, priority => 3
1557 is( $item->holds()->count, 3, "Three holds found" );
1558 is( $item->holds( { found => 'W' } )->count, 2, "Two waiting holds found" );
1559 is_deeply( $item->holds( { found => 'T' } )->next->unblessed, $hold1, "Found transit holds matches the hold" );
1560 is( $item->holds( { found => undef } )->count, 0, "Nothing returned if no matching holds" );
1561 is( $item->first_hold->reserve_id, $hold2->{reserve_id}, '->first_hold returns the correct hold' );
1564 subtest 'biblio' => sub {
1567 my $biblio = $retrieved_item_1->biblio;
1568 is( ref( $biblio ), 'Koha::Biblio', 'Koha::Item->biblio should return a Koha::Biblio' );
1569 is( $biblio->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblio should return the correct biblio' );
1572 subtest 'biblioitem' => sub {
1575 my $biblioitem = $retrieved_item_1->biblioitem;
1576 is( ref( $biblioitem ), 'Koha::Biblioitem', 'Koha::Item->biblioitem should return a Koha::Biblioitem' );
1577 is( $biblioitem->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblioitem should return the correct biblioitem' );
1581 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
1582 subtest 'checkout' => sub {
1584 my $item = Koha::Items->find( $new_item_1->itemnumber );
1586 my $checkout = $item->checkout;
1587 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no current checkout on this item' );
1590 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1591 C4::Circulation::AddIssue( $patron, $item->barcode );
1592 $checkout = $retrieved_item_1->checkout;
1593 is( ref( $checkout ), 'Koha::Checkout', 'Koha::Item->checkout should return a Koha::Checkout' );
1594 is( $checkout->itemnumber, $item->itemnumber, 'Koha::Item->checkout should return the correct checkout' );
1595 is( $checkout->borrowernumber, $patron->borrowernumber, 'Koha::Item->checkout should return the correct checkout' );
1598 C4::Circulation::AddReturn( $item->barcode );
1600 # There is no more checkout on this item, making sure it will not return old checkouts
1601 $checkout = $item->checkout;
1602 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no *current* checkout on this item' );
1605 subtest 'can_be_transferred' => sub {
1608 t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
1609 t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
1611 my $biblio = $builder->build_sample_biblio();
1612 my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
1613 my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
1614 my $item = $builder->build_sample_item({
1615 biblionumber => $biblio->biblionumber,
1616 homebranch => $library1->branchcode,
1617 holdingbranch => $library1->branchcode,
1620 is(Koha::Item::Transfer::Limits->search({
1621 fromBranch => $library1->branchcode,
1622 toBranch => $library2->branchcode,
1623 })->count, 0, 'There are no transfer limits between libraries.');
1624 ok($item->can_be_transferred({ to => $library2 }),
1625 'Item can be transferred between libraries.');
1627 my $limit = Koha::Item::Transfer::Limit->new({
1628 fromBranch => $library1->branchcode,
1629 toBranch => $library2->branchcode,
1630 itemtype => $item->effective_itemtype,
1632 is(Koha::Item::Transfer::Limits->search({
1633 fromBranch => $library1->branchcode,
1634 toBranch => $library2->branchcode,
1635 })->count, 1, 'Given we have added a transfer limit,');
1636 is($item->can_be_transferred({ to => $library2 }), 0,
1637 'Item can no longer be transferred between libraries.');
1638 is($item->can_be_transferred({ to => $library2, from => $library1 }), 0,
1639 'We get the same result also if we pass the from-library parameter.');
1642 # Reset nb_of_items prior to testing delete
1643 $nb_of_items = Koha::Items->search->count;
1646 $retrieved_item_1->delete;
1647 is( Koha::Items->search->count, $nb_of_items - 1, 'Delete should have deleted the item' );
1649 $schema->storage->txn_rollback;
1651 subtest 'filter_by_visible_in_opac() tests' => sub {
1655 $schema->storage->txn_begin;
1657 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1658 my $mocked_category = Test::MockModule->new('Koha::Patron::Category');
1660 $mocked_category->mock( 'override_hidden_items', sub {
1664 # have a fresh biblio
1665 my $biblio = $builder->build_sample_biblio;
1666 # have two itemtypes
1667 my $itype_1 = $builder->build_object({ class => 'Koha::ItemTypes' });
1668 my $itype_2 = $builder->build_object({ class => 'Koha::ItemTypes' });
1669 # have 5 items on that biblio
1670 my $item_1 = $builder->build_sample_item(
1672 biblionumber => $biblio->biblionumber,
1674 itype => $itype_1->itemtype,
1679 my $item_2 = $builder->build_sample_item(
1681 biblionumber => $biblio->biblionumber,
1683 itype => $itype_2->itemtype,
1688 my $item_3 = $builder->build_sample_item(
1690 biblionumber => $biblio->biblionumber,
1692 itype => $itype_1->itemtype,
1697 my $item_4 = $builder->build_sample_item(
1699 biblionumber => $biblio->biblionumber,
1701 itype => $itype_2->itemtype,
1706 my $item_5 = $builder->build_sample_item(
1708 biblionumber => $biblio->biblionumber,
1710 itype => $itype_1->itemtype,
1715 my $item_6 = $builder->build_sample_item(
1717 biblionumber => $biblio->biblionumber,
1719 itype => $itype_1->itemtype,
1727 my $mocked_context = Test::MockModule->new('C4::Context');
1728 $mocked_context->mock( 'yaml_preference', sub {
1732 t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
1733 is( $biblio->items->filter_by_visible_in_opac->count,
1734 6, 'No rules passed, hidelostitems unset' );
1736 is( $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1737 6, 'No rules passed, hidelostitems unset, patron exception changes nothing' );
1739 $rules = { copynumber => [ 2 ] };
1741 t::lib::Mocks::mock_preference( 'hidelostitems', 1 );
1743 $biblio->items->filter_by_visible_in_opac->count,
1745 'No rules passed, hidelostitems set'
1749 $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1751 'No rules passed, hidelostitems set, patron exception changes nothing'
1754 $rules = { biblionumber => [ $biblio->biblionumber ] };
1756 $biblio->items->filter_by_visible_in_opac->count,
1758 'Biblionumber rule successfully hides all items'
1761 my $biblio2 = $builder->build_sample_biblio;
1762 $rules = { biblionumber => [ $biblio2->biblionumber ] };
1763 my $prefetched = $biblio->items->search({},{ prefetch => ['branchtransfers','reserves'] })->filter_by_visible_in_opac;
1764 ok( $prefetched->next, "Can retrieve object when prefetching and hiding on a duplicated column");
1766 $rules = { withdrawn => [ 1, 2 ], copynumber => [ 2 ] };
1768 $biblio->items->filter_by_visible_in_opac->count,
1770 'Rules on withdrawn, hidelostitems set'
1774 $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1776 'hidelostitems set, rules on withdrawn but patron override passed'
1779 $rules = { itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1781 $biblio->items->filter_by_visible_in_opac->count,
1783 'Rules on itype, hidelostitems set'
1786 $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1788 $biblio->items->filter_by_visible_in_opac->count,
1790 'Rules on itype and withdrawn, hidelostitems set'
1793 $biblio->items->filter_by_visible_in_opac
1795 $item_4->itemnumber,
1796 'The right item is returned'
1799 $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_2->itemtype ], copynumber => [ 2 ] };
1801 $biblio->items->filter_by_visible_in_opac->count,
1803 'Rules on itype and withdrawn, hidelostitems set'
1806 $biblio->items->filter_by_visible_in_opac
1808 $item_5->itemnumber,
1809 'The right item is returned'
1812 # Make sure the warning on the about page will work
1813 $rules = { itemlost => ['AB'] };
1814 my $c = Koha::Items->filter_by_visible_in_opac->count;
1815 my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
1816 like( $warnings[2], qr/Truncated incorrect (DOUBLE|DECIMAL) value: 'AB'/);
1818 $schema->storage->txn_rollback;
1821 subtest 'filter_out_lost() tests' => sub {
1825 $schema->storage->txn_begin;
1827 # have a fresh biblio
1828 my $biblio = $builder->build_sample_biblio;
1829 # have 3 items on that biblio
1830 my $item_1 = $builder->build_sample_item(
1832 biblionumber => $biblio->biblionumber,
1836 my $item_2 = $builder->build_sample_item(
1838 biblionumber => $biblio->biblionumber,
1842 my $item_3 = $builder->build_sample_item(
1844 biblionumber => $biblio->biblionumber,
1849 is( $biblio->items->filter_out_lost->next->itemnumber, $item_2->itemnumber, 'Right item returned' );
1850 is( $biblio->items->filter_out_lost->count, 1, 'Only one item is not lost' );
1852 $schema->storage->txn_rollback;
1855 subtest 'move_to_biblio() tests' => sub {
1859 $schema->storage->txn_begin;
1861 my $biblio1 = $builder->build_sample_biblio;
1862 my $biblio2 = $builder->build_sample_biblio;
1863 my $item1 = $builder->build_sample_item({ biblionumber => $biblio1->biblionumber });
1864 my $item2 = $builder->build_sample_item({ biblionumber => $biblio1->biblionumber });
1866 $biblio1->items->move_to_biblio($biblio2);
1868 $item1->discard_changes;
1869 $item2->discard_changes;
1871 is($item1->biblionumber, $biblio2->biblionumber, "Item 1 moved");
1872 is($item2->biblionumber, $biblio2->biblionumber, "Item 2 moved");
1874 $schema->storage->txn_rollback;
1878 subtest 'search_ordered' => sub {
1882 $schema->storage->txn_begin;
1884 my $library_a = $builder->build_object(
1885 { class => 'Koha::Libraries', value => { branchname => 'TEST_A' } } );
1886 my $library_z = $builder->build_object(
1887 { class => 'Koha::Libraries', value => { branchname => 'TEST_Z' } } );
1888 my $biblio = $builder->build_sample_biblio( { serial => 0 } );
1889 my $item1 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1890 my $item2 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1891 my $item3 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1895 # order_by homebranch.branchname
1896 $item1->discard_changes->update( { homebranch => $library_z->branchcode } );
1897 $item2->discard_changes->update( { homebranch => $library_a->branchcode } );
1898 $item3->discard_changes->update( { homebranch => $library_z->branchcode } );
1899 is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1900 [ $item2->itemnumber, $item1->itemnumber, $item3->itemnumber ],
1901 "not a serial - order by homebranch" );
1903 # order_by me.enumchron
1904 $biblio->items->update( { homebranch => $library_a->branchcode } );
1905 $item1->discard_changes->update( { enumchron => 'cc' } );
1906 $item2->discard_changes->update( { enumchron => 'bb' } );
1907 $item3->discard_changes->update( { enumchron => 'aa' } );
1908 is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1909 [ $item3->itemnumber, $item2->itemnumber, $item1->itemnumber ],
1910 "not a serial - order by enumchron" );
1912 # order_by -desc => 'me.dateaccessioned'
1913 $biblio->items->update( { enumchron => undef } );
1914 $item1->discard_changes->update( { dateaccessioned => '2022-08-19' } );
1915 $item2->discard_changes->update( { dateaccessioned => '2022-07-19' } );
1916 $item3->discard_changes->update( { dateaccessioned => '2022-09-19' } );
1917 is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1918 [ $item3->itemnumber, $item1->itemnumber, $item2->itemnumber ],
1919 "not a serial - order by date accessioned desc" );
1924 my $sub_freq = $builder->build( { source => 'SubscriptionFrequency' } );
1926 $builder->build( { source => 'SubscriptionNumberpattern' } );
1927 my $subscription = $builder->build_object(
1929 class => 'Koha::Subscriptions',
1931 biblionumber => $biblio->biblionumber,
1932 periodicity => $sub_freq->{id},
1933 numberpattern => $sub_np->{id},
1934 published_on_template => "[% publisheddatetext %] [% biblionumber %]",
1938 $builder->build_object(
1940 class => 'Koha::Subscription::Histories',
1942 subscriptionid => $subscription->subscriptionid,
1943 biblionumber => $biblio->biblionumber
1948 $biblio->update( { serial => 1 } );
1950 C4::Serials::NewIssue( "serialseq", $subscription->subscriptionid,
1951 $biblio->biblionumber, 1, undef, undef, "publisheddatetext",
1952 "notes", "routingnotes" );
1953 C4::Serials::AddItem2Serial( $serialid1, $item1->itemnumber );
1955 C4::Serials::NewIssue( "serialseq", $subscription->subscriptionid,
1956 $biblio->biblionumber, 1, undef, undef, "publisheddatetext",
1957 "notes", "routingnotes" );
1958 C4::Serials::AddItem2Serial( $serialid2, $item2->itemnumber );
1960 C4::Serials::NewIssue( "serialseq", $subscription->subscriptionid,
1961 $biblio->biblionumber, 1, undef, undef, "publisheddatetext",
1962 "notes", "routingnotes" );
1963 C4::Serials::AddItem2Serial( $serialid3, $item3->itemnumber );
1964 my $serial1 = Koha::Serials->find($serialid1);
1965 my $serial2 = Koha::Serials->find($serialid2);
1966 my $serial3 = Koha::Serials->find($serialid3);
1968 # order_by serial.publisheddate
1969 $serial1->discard_changes->update( { publisheddate => '2022-09-19' } );
1970 $serial2->discard_changes->update( { publisheddate => '2022-07-19' } );
1971 $serial3->discard_changes->update( { publisheddate => '2022-08-19' } );
1973 [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1974 [ $item2->itemnumber, $item3->itemnumber, $item1->itemnumber ],
1975 "serial - order by publisheddate"
1978 # order_by me.enumchron
1979 $serial1->discard_changes->update({ publisheddate => '2022-08-19' });
1980 $serial2->discard_changes->update({ publisheddate => '2022-08-19' });
1981 $serial3->discard_changes->update({ publisheddate => '2022-08-19' });
1982 $item1->discard_changes->update( { enumchron => 'cc' } );
1983 $item2->discard_changes->update( { enumchron => 'bb' } );
1984 $item3->discard_changes->update( { enumchron => 'aa' } );
1985 is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1986 [ $item3->itemnumber, $item2->itemnumber, $item1->itemnumber ],
1987 "serial - order by enumchron" );
1989 is( $serial1->publisheddatetext, "publisheddatetext " . $biblio->biblionumber, "Column publisheddatetext rendered correctly from template for serial1" );
1990 is( $serial2->publisheddatetext, "publisheddatetext " . $biblio->biblionumber, "Column publisheddatetext rendered correctly from template for serial2" );
1991 is( $serial3->publisheddatetext, "publisheddatetext " . $biblio->biblionumber, "Column publisheddatetext rendered correctly from template for serial3" );
1995 $schema->storage->txn_rollback;
1999 subtest 'filter_by_for_hold' => sub {
2003 $schema->storage->txn_begin;
2005 my $biblio = $builder->build_sample_biblio;
2006 my $library = $builder->build_object({ class => 'Koha::Libraries' });
2008 t::lib::Mocks::mock_preference('IndependentBranches', 0); # more robust tests
2010 is( $biblio->items->filter_by_for_hold->count, 0, 'no item yet' );
2011 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 1 } );
2012 is( $biblio->items->filter_by_for_hold->count, 0, 'no item for hold' );
2013 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 0 } );
2014 is( $biblio->items->filter_by_for_hold->count, 1, '1 item for hold' );
2015 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => -1 } );
2016 is( $biblio->items->filter_by_for_hold->count, 2, '2 items for hold' );
2018 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 0, library => $library->id } );
2019 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 1, library => $library->id } );
2020 is( $biblio->items->filter_by_for_hold->count, 3, '3 items for hold - itemlost' );
2022 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 0, library => $library->id } );
2023 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 1, library => $library->id } );
2024 is( $biblio->items->filter_by_for_hold->count, 4, '4 items for hold - withdrawn' );
2026 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, damaged => 0 } );
2027 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, damaged => 1 } );
2028 t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0);
2029 is( $biblio->items->filter_by_for_hold->count, 5, '5 items for hold - not damaged if not AllowHoldsOnDamagedItems' );
2030 t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1);
2031 is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - damaged if AllowHoldsOnDamagedItems' );
2033 my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' });
2034 my $not_holdable_itemtype = $itemtype->itemtype;
2035 $builder->build_sample_item(
2037 biblionumber => $biblio->biblionumber,
2038 itype => $not_holdable_itemtype,
2041 Koha::CirculationRules->set_rule(
2043 branchcode => undef,
2044 itemtype => $not_holdable_itemtype,
2045 rule_name => 'holdallowed',
2046 rule_value => 'not_allowed',
2049 is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - holdallowed=not_allowed' );
2051 # Remove rule, test notforloan on itemtype
2052 Koha::CirculationRules->set_rule(
2054 branchcode => undef,
2055 itemtype => $not_holdable_itemtype,
2056 rule_name => 'holdallowed',
2057 rule_value => undef,
2060 is( $biblio->items->filter_by_for_hold->count, 7, '7 items for hold - rule deleted' );
2061 $itemtype->notforloan(1)->store;
2062 is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - notforloan' );
2065 my $mock_context = Test::MockModule->new('C4::Context');
2066 $mock_context->mock( 'only_my_library', 1 );
2067 $mock_context->mock( 'mybranch', $library->id );
2068 is( $biblio->items->filter_by_for_hold->count, 2, '2 items for hold, filtered by IndependentBranches' );
2071 t::lib::Mocks::mock_preference('item-level_itypes', 0);
2072 $biblio->biblioitem->itemtype($not_holdable_itemtype)->store;
2073 is( $biblio->items->filter_by_for_hold->count, 0, '0 item-level_itypes=0' );
2075 t::lib::Mocks::mock_preference('item-level_itypes', 1);
2077 $schema->storage->txn_rollback;
2080 subtest 'filter_by_bookable' => sub {
2083 $schema->storage->txn_begin;
2085 my $biblio = $builder->build_sample_biblio;
2088 my $bookable_item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } );
2090 # not bookable items
2091 my $non_bookable_item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 0 } );
2092 my $non_bookable_item2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 0 } );
2094 is( $biblio->items->filter_by_bookable->count, 1, "filter_by_bookable returns the correct number of items" );
2096 $biblio->items->filter_by_bookable->next->itemnumber, $bookable_item1->itemnumber,
2097 "the correct item is returned from filter_by_bookable"
2100 $schema->storage->txn_rollback;