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 => 14;
31 use Koha::Item::Transfer::Limits;
34 use Koha::DateUtils qw( dt_from_string );
36 use t::lib::TestBuilder;
40 my $schema = Koha::Database->new->schema;
41 $schema->storage->txn_begin;
43 my $dbh = C4::Context->dbh;
45 my $builder = t::lib::TestBuilder->new;
46 my $library = $builder->build( { source => 'Branch' } );
47 my $nb_of_items = Koha::Items->search->count;
48 my $biblio = $builder->build_sample_biblio();
49 my $new_item_1 = $builder->build_sample_item({
50 biblionumber => $biblio->biblionumber,
51 homebranch => $library->{branchcode},
52 holdingbranch => $library->{branchcode},
54 my $new_item_2 = $builder->build_sample_item({
55 biblionumber => $biblio->biblionumber,
56 homebranch => $library->{branchcode},
57 holdingbranch => $library->{branchcode},
61 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
63 like( $new_item_1->itemnumber, qr|^\d+$|, 'Adding a new item should have set the itemnumber' );
64 is( Koha::Items->search->count, $nb_of_items + 2, 'The 2 items should have been added' );
66 my $retrieved_item_1 = Koha::Items->find( $new_item_1->itemnumber );
67 is( $retrieved_item_1->barcode, $new_item_1->barcode, 'Find a item by id should return the correct item' );
69 subtest 'store' => sub {
72 my $biblio = $builder->build_sample_biblio;
73 my $today = dt_from_string->set( hour => 0, minute => 0, second => 0 );
74 my $item = Koha::Item->new(
76 homebranch => $library->{branchcode},
77 holdingbranch => $library->{branchcode},
78 biblionumber => $biblio->biblionumber,
81 )->store->get_from_storage;
83 is( t::lib::Dates::compare( $item->replacementpricedate, $today ),
84 0, 'replacementpricedate must have been set to today if not given' );
85 is( t::lib::Dates::compare( $item->datelastseen, $today ),
86 0, 'datelastseen must have been set to today if not given' );
89 $biblio->biblioitem->itemtype,
90 'items.itype must have been set to biblioitem.itemtype is not given'
94 subtest 'permanent_location' => sub {
97 my $location = 'my_loc';
99 homebranch => $library->{branchcode},
100 holdingbranch => $library->{branchcode},
101 biblionumber => $biblio->biblionumber,
102 location => $location,
106 # NewItemsDefaultLocation not set
107 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', '' );
109 # Not passing permanent_location on creating the item
110 my $item = Koha::Item->new($attributes)->store->get_from_storage;
111 is( $item->location, $location,
112 'location must have been set to location if given' );
113 is( $item->permanent_location, $item->location,
114 'permanent_location must have been set to location if not given' );
117 # Passing permanent_location on creating the item
118 $item = Koha::Item->new(
119 { %$attributes, permanent_location => 'perm_loc' } )
120 ->store->get_from_storage;
121 is( $item->permanent_location, 'perm_loc',
122 'permanent_location must have been kept if given' );
127 # NewItemsDefaultLocation set
128 my $default_location = 'default_location';
129 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', $default_location );
131 # Not passing permanent_location on creating the item
132 my $item = Koha::Item->new($attributes)->store->get_from_storage;
133 is( $item->location, $default_location,
134 'location must have been set to default_location even if given' # FIXME this sounds wrong! Must be done in any cases?
136 is( $item->permanent_location, $location,
137 'permanent_location must have been set to the location given' );
140 # Passing permanent_location on creating the item
141 $item = Koha::Item->new(
142 { %$attributes, permanent_location => 'perm_loc' } )
143 ->store->get_from_storage;
144 is( $item->location, $default_location,
145 'location must have been set to default_location even if given' # FIXME this sounds wrong! Must be done in any cases?
147 is( $item->permanent_location, $location,
148 'permanent_location must have been set to the location given'
155 subtest '*_on updates' => sub {
158 # Once the '_on' value is set (triggered by the related field turning from false to true)
159 # it should not be re-set for any changes outside of the related field being 'unset'.
161 my @fields = qw( itemlost withdrawn damaged );
162 my $today = dt_from_string();
163 my $yesterday = $today->clone()->subtract( days => 1 );
165 for my $field ( @fields ) {
166 my $item = $builder->build_sample_item(
169 itemlost_on => undef,
171 withdrawn_on => undef,
176 my $field_on = $field . '_on';
178 # Set field for the first time
179 Time::Fake->offset( $yesterday->epoch );
180 $item->$field(1)->store;
181 $item->get_from_storage;
182 is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
183 0, $field_on . " was set upon first truthy setting" );
185 # Update the field to a new 'true' value
186 Time::Fake->offset( $today->epoch );
187 $item->$field(2)->store;
188 $item->get_from_storage;
189 is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
190 0, $field_on . " was not updated upon second truthy setting" );
192 # Update the field to a new 'false' value
193 $item->$field(0)->store;
194 $item->get_from_storage;
195 is($item->$field_on, undef, $field_on . " was unset upon untruthy setting");
201 subtest '_lost_found_trigger' => sub {
204 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
205 t::lib::Mocks::mock_preference( 'WhenLostForgiveFine', 0 );
207 my $processfee_amount = 20;
208 my $replacement_amount = 99.00;
209 my $item_type = $builder->build_object(
211 class => 'Koha::ItemTypes',
215 defaultreplacecost => undef,
216 processfee => $processfee_amount,
217 rentalcharge_daily => 0,
221 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
223 $biblio = $builder->build_sample_biblio( { author => 'Hall, Daria' } );
225 subtest 'Full write-off tests' => sub {
229 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
231 $builder->build_object( { class => "Koha::Patrons" } );
232 t::lib::Mocks::mock_userenv(
233 { patron => $manager, branchcode => $manager->branchcode } );
235 my $item = $builder->build_sample_item(
237 biblionumber => $biblio->biblionumber,
238 library => $library->branchcode,
239 replacementprice => $replacement_amount,
240 itype => $item_type->itemtype,
244 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
246 # Simulate item marked as lost
247 $item->itemlost(3)->store;
248 C4::Circulation::LostItem( $item->itemnumber, 1 );
250 my $processing_fee_lines = Koha::Account::Lines->search(
252 borrowernumber => $patron->id,
253 itemnumber => $item->itemnumber,
254 debit_type_code => 'PROCESSING'
257 is( $processing_fee_lines->count,
258 1, 'Only one processing fee produced' );
259 my $processing_fee_line = $processing_fee_lines->next;
260 is( $processing_fee_line->amount + 0,
262 'The right PROCESSING amount is generated' );
263 is( $processing_fee_line->amountoutstanding + 0,
265 'The right PROCESSING amountoutstanding is generated' );
267 my $lost_fee_lines = Koha::Account::Lines->search(
269 borrowernumber => $patron->id,
270 itemnumber => $item->itemnumber,
271 debit_type_code => 'LOST'
274 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
275 my $lost_fee_line = $lost_fee_lines->next;
276 is( $lost_fee_line->amount + 0,
277 $replacement_amount, 'The right LOST amount is generated' );
278 is( $lost_fee_line->amountoutstanding + 0,
280 'The right LOST amountoutstanding is generated' );
281 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
283 my $account = $patron->account;
284 my $debts = $account->outstanding_debits;
287 my $credit = $account->add_credit(
289 amount => $account->balance,
295 { debits => [ $debts->as_list ], offset_type => 'Writeoff' } );
297 # Simulate item marked as found
298 $item->itemlost(0)->store;
299 is( $item->{_refunded}, undef, 'No LOST_FOUND account line added' );
301 $lost_fee_line->discard_changes; # reload from DB
302 is( $lost_fee_line->amountoutstanding + 0,
303 0, 'Lost fee has no outstanding amount' );
304 is( $lost_fee_line->debit_type_code,
305 'LOST', 'Lost fee now still has account type of LOST' );
306 is( $lost_fee_line->status, 'FOUND',
307 "Lost fee now has account status of FOUND - No Refund" );
309 is( $patron->account->balance,
310 -0, 'The patron balance is 0, everything was written off' );
313 subtest 'Full payment tests' => sub {
317 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
319 my $item = $builder->build_sample_item(
321 biblionumber => $biblio->biblionumber,
322 library => $library->branchcode,
323 replacementprice => $replacement_amount,
324 itype => $item_type->itemtype
329 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
331 # Simulate item marked as lost
332 $item->itemlost(1)->store;
333 C4::Circulation::LostItem( $item->itemnumber, 1 );
335 my $processing_fee_lines = Koha::Account::Lines->search(
337 borrowernumber => $patron->id,
338 itemnumber => $item->itemnumber,
339 debit_type_code => 'PROCESSING'
342 is( $processing_fee_lines->count,
343 1, 'Only one processing fee produced' );
344 my $processing_fee_line = $processing_fee_lines->next;
345 is( $processing_fee_line->amount + 0,
347 'The right PROCESSING amount is generated' );
348 is( $processing_fee_line->amountoutstanding + 0,
350 'The right PROCESSING amountoutstanding is generated' );
352 my $lost_fee_lines = Koha::Account::Lines->search(
354 borrowernumber => $patron->id,
355 itemnumber => $item->itemnumber,
356 debit_type_code => 'LOST'
359 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
360 my $lost_fee_line = $lost_fee_lines->next;
361 is( $lost_fee_line->amount + 0,
362 $replacement_amount, 'The right LOST amount is generated' );
363 is( $lost_fee_line->amountoutstanding + 0,
365 'The right LOST amountountstanding is generated' );
367 my $account = $patron->account;
368 my $debts = $account->outstanding_debits;
371 my $credit = $account->add_credit(
373 amount => $account->balance,
379 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
381 # Simulate item marked as found
382 $item->itemlost(0)->store;
383 is( $item->{_refunded}, 1, 'Refund triggered' );
385 my $credit_return = Koha::Account::Lines->search(
387 itemnumber => $item->itemnumber,
388 credit_type_code => 'LOST_FOUND'
393 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
394 is( $credit_return->amount + 0,
396 'The account line of type LOST_FOUND has an amount of -99' );
398 $credit_return->amountoutstanding + 0,
400 'The account line of type LOST_FOUND has an amountoutstanding of -99'
403 $lost_fee_line->discard_changes;
404 is( $lost_fee_line->amountoutstanding + 0,
405 0, 'Lost fee has no outstanding amount' );
406 is( $lost_fee_line->debit_type_code,
407 'LOST', 'Lost fee now still has account type of LOST' );
408 is( $lost_fee_line->status, 'FOUND',
409 "Lost fee now has account status of FOUND" );
411 is( $patron->account->balance, -99,
412 'The patron balance is -99, a credit that equals the lost fee payment'
416 subtest 'Test without payment or write off' => sub {
420 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
422 my $item = $builder->build_sample_item(
424 biblionumber => $biblio->biblionumber,
425 library => $library->branchcode,
426 replacementprice => 23.00,
427 replacementprice => $replacement_amount,
428 itype => $item_type->itemtype
433 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
435 # Simulate item marked as lost
436 $item->itemlost(3)->store;
437 C4::Circulation::LostItem( $item->itemnumber, 1 );
439 my $processing_fee_lines = Koha::Account::Lines->search(
441 borrowernumber => $patron->id,
442 itemnumber => $item->itemnumber,
443 debit_type_code => 'PROCESSING'
446 is( $processing_fee_lines->count,
447 1, 'Only one processing fee produced' );
448 my $processing_fee_line = $processing_fee_lines->next;
449 is( $processing_fee_line->amount + 0,
451 'The right PROCESSING amount is generated' );
452 is( $processing_fee_line->amountoutstanding + 0,
454 'The right PROCESSING amountoutstanding is generated' );
456 my $lost_fee_lines = Koha::Account::Lines->search(
458 borrowernumber => $patron->id,
459 itemnumber => $item->itemnumber,
460 debit_type_code => 'LOST'
463 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
464 my $lost_fee_line = $lost_fee_lines->next;
465 is( $lost_fee_line->amount + 0,
466 $replacement_amount, 'The right LOST amount is generated' );
467 is( $lost_fee_line->amountoutstanding + 0,
469 'The right LOST amountountstanding is generated' );
471 # Simulate item marked as found
472 $item->itemlost(0)->store;
473 is( $item->{_refunded}, 1, 'Refund triggered' );
475 my $credit_return = Koha::Account::Lines->search(
477 itemnumber => $item->itemnumber,
478 credit_type_code => 'LOST_FOUND'
483 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
484 is( $credit_return->amount + 0,
486 'The account line of type LOST_FOUND has an amount of -99' );
488 $credit_return->amountoutstanding + 0,
490 'The account line of type LOST_FOUND has an amountoutstanding of 0'
493 $lost_fee_line->discard_changes;
494 is( $lost_fee_line->amountoutstanding + 0,
495 0, 'Lost fee has no outstanding amount' );
496 is( $lost_fee_line->debit_type_code,
497 'LOST', 'Lost fee now still has account type of LOST' );
498 is( $lost_fee_line->status, 'FOUND',
499 "Lost fee now has account status of FOUND" );
501 is( $patron->account->balance,
502 20, 'The patron balance is 20, still owes the processing fee' );
506 'Test with partial payement and write off, and remaining debt' =>
511 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
512 my $item = $builder->build_sample_item(
514 biblionumber => $biblio->biblionumber,
515 library => $library->branchcode,
516 replacementprice => $replacement_amount,
517 itype => $item_type->itemtype
522 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
524 # Simulate item marked as lost
525 $item->itemlost(1)->store;
526 C4::Circulation::LostItem( $item->itemnumber, 1 );
528 my $processing_fee_lines = Koha::Account::Lines->search(
530 borrowernumber => $patron->id,
531 itemnumber => $item->itemnumber,
532 debit_type_code => 'PROCESSING'
535 is( $processing_fee_lines->count,
536 1, 'Only one processing fee produced' );
537 my $processing_fee_line = $processing_fee_lines->next;
538 is( $processing_fee_line->amount + 0,
540 'The right PROCESSING amount is generated' );
541 is( $processing_fee_line->amountoutstanding + 0,
543 'The right PROCESSING amountoutstanding is generated' );
545 my $lost_fee_lines = Koha::Account::Lines->search(
547 borrowernumber => $patron->id,
548 itemnumber => $item->itemnumber,
549 debit_type_code => 'LOST'
552 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
553 my $lost_fee_line = $lost_fee_lines->next;
554 is( $lost_fee_line->amount + 0,
555 $replacement_amount, 'The right LOST amount is generated' );
556 is( $lost_fee_line->amountoutstanding + 0,
558 'The right LOST amountountstanding is generated' );
560 my $account = $patron->account;
563 $processfee_amount + $replacement_amount,
564 'Balance is PROCESSING + L'
568 my $payment_amount = 27;
569 my $payment = $account->add_credit(
571 amount => $payment_amount,
578 { debits => [$lost_fee_line], offset_type => 'Payment' } );
580 # Partially write off fee
581 my $write_off_amount = 25;
582 my $write_off = $account->add_credit(
584 amount => $write_off_amount,
590 { debits => [$lost_fee_line], offset_type => 'Writeoff' } );
595 $replacement_amount -
598 'Payment and write off applied'
601 # Store the amountoutstanding value
602 $lost_fee_line->discard_changes;
603 my $outstanding = $lost_fee_line->amountoutstanding;
605 # Simulate item marked as found
606 $item->itemlost(0)->store;
607 is( $item->{_refunded}, 1, 'Refund triggered' );
609 my $credit_return = Koha::Account::Lines->search(
611 itemnumber => $item->itemnumber,
612 credit_type_code => 'LOST_FOUND'
617 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
621 $processfee_amount - $payment_amount,
622 'Balance is PROCESSING - PAYMENT (LOST_FOUND)'
625 $lost_fee_line->discard_changes;
626 is( $lost_fee_line->amountoutstanding + 0,
627 0, 'Lost fee has no outstanding amount' );
628 is( $lost_fee_line->debit_type_code,
629 'LOST', 'Lost fee now still has account type of LOST' );
630 is( $lost_fee_line->status, 'FOUND',
631 "Lost fee now has account status of FOUND" );
634 $credit_return->amount + 0,
635 ( $payment_amount + $outstanding ) * -1,
636 'The account line of type LOST_FOUND has an amount equal to the payment + outstanding'
639 $credit_return->amountoutstanding + 0,
640 $payment_amount * -1,
641 'The account line of type LOST_FOUND has an amountoutstanding equal to the payment'
646 $processfee_amount - $payment_amount,
647 'The patron balance is the difference between the PROCESSING and the credit'
651 subtest 'Partial payment, existing debits and AccountAutoReconcile' =>
656 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
657 my $barcode = 'KD123456793';
658 my $replacement_amount = 100;
659 my $processfee_amount = 20;
661 my $item_type = $builder->build_object(
663 class => 'Koha::ItemTypes',
667 defaultreplacecost => undef,
669 rentalcharge_daily => 0,
673 my $item = Koha::Item->new(
675 biblionumber => $biblio->biblionumber,
676 homebranch => $library->branchcode,
677 holdingbranch => $library->branchcode,
679 replacementprice => $replacement_amount,
680 itype => $item_type->itemtype
685 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
687 # Simulate item marked as lost
688 $item->itemlost(1)->store;
689 C4::Circulation::LostItem( $item->itemnumber, 1 );
691 my $lost_fee_lines = Koha::Account::Lines->search(
693 borrowernumber => $patron->id,
694 itemnumber => $item->itemnumber,
695 debit_type_code => 'LOST'
698 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
699 my $lost_fee_line = $lost_fee_lines->next;
700 is( $lost_fee_line->amount + 0,
701 $replacement_amount, 'The right LOST amount is generated' );
702 is( $lost_fee_line->amountoutstanding + 0,
704 'The right LOST amountountstanding is generated' );
706 my $account = $patron->account;
707 is( $account->balance, $replacement_amount, 'Balance is L' );
710 my $payment_amount = 27;
711 my $payment = $account->add_credit(
713 amount => $payment_amount,
719 { debits => [$lost_fee_line], offset_type => 'Payment' } );
723 $replacement_amount - $payment_amount,
727 my $manual_debit_amount = 80;
730 amount => $manual_debit_amount,
738 $manual_debit_amount + $replacement_amount - $payment_amount,
739 'Manual debit applied'
742 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
744 # Simulate item marked as found
745 $item->itemlost(0)->store;
746 is( $item->{_refunded}, 1, 'Refund triggered' );
748 my $credit_return = Koha::Account::Lines->search(
750 itemnumber => $item->itemnumber,
751 credit_type_code => 'LOST_FOUND'
756 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
760 $manual_debit_amount - $payment_amount,
761 'Balance is PROCESSING - payment (LOST_FOUND)'
764 my $manual_debit = Koha::Account::Lines->search(
766 borrowernumber => $patron->id,
767 debit_type_code => 'OVERDUE',
768 status => 'UNRETURNED'
772 $manual_debit->amountoutstanding + 0,
773 $manual_debit_amount - $payment_amount,
774 'reconcile_balance was called'
778 subtest 'Patron deleted' => sub {
781 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
782 my $barcode = 'KD123456794';
783 my $replacement_amount = 100;
784 my $processfee_amount = 20;
786 my $item_type = $builder->build_object(
788 class => 'Koha::ItemTypes',
792 defaultreplacecost => undef,
794 rentalcharge_daily => 0,
798 my $item = Koha::Item->new(
800 biblionumber => $biblio->biblionumber,
801 homebranch => $library->branchcode,
802 holdingbranch => $library->branchcode,
804 replacementprice => $replacement_amount,
805 itype => $item_type->itemtype
810 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
812 # Simulate item marked as lost
813 $item->itemlost(1)->store;
814 C4::Circulation::LostItem( $item->itemnumber, 1 );
819 # Simulate item marked as found
820 $item->itemlost(0)->store;
821 is( $item->{_refunded}, undef, 'No refund triggered' );
825 subtest 'restore fine | no overdue' => sub {
830 $builder->build_object( { class => "Koha::Patrons" } );
831 t::lib::Mocks::mock_userenv(
832 { patron => $manager, branchcode => $manager->branchcode } );
834 # Set lostreturn_policy to 'restore' for tests
835 my $specific_rule_restore = $builder->build(
837 source => 'CirculationRule',
839 branchcode => $manager->branchcode,
840 categorycode => undef,
842 rule_name => 'lostreturn',
843 rule_value => 'restore'
848 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
850 my $item = $builder->build_sample_item(
852 biblionumber => $biblio->biblionumber,
853 library => $library->branchcode,
854 replacementprice => $replacement_amount,
855 itype => $item_type->itemtype
860 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
862 # Simulate item marked as lost
863 $item->itemlost(1)->store;
864 C4::Circulation::LostItem( $item->itemnumber, 1 );
866 my $processing_fee_lines = Koha::Account::Lines->search(
868 borrowernumber => $patron->id,
869 itemnumber => $item->itemnumber,
870 debit_type_code => 'PROCESSING'
873 is( $processing_fee_lines->count,
874 1, 'Only one processing fee produced' );
875 my $processing_fee_line = $processing_fee_lines->next;
876 is( $processing_fee_line->amount + 0,
878 'The right PROCESSING amount is generated' );
879 is( $processing_fee_line->amountoutstanding + 0,
881 'The right PROCESSING amountoutstanding is generated' );
883 my $lost_fee_lines = Koha::Account::Lines->search(
885 borrowernumber => $patron->id,
886 itemnumber => $item->itemnumber,
887 debit_type_code => 'LOST'
890 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
891 my $lost_fee_line = $lost_fee_lines->next;
892 is( $lost_fee_line->amount + 0,
893 $replacement_amount, 'The right LOST amount is generated' );
894 is( $lost_fee_line->amountoutstanding + 0,
896 'The right LOST amountountstanding is generated' );
898 my $account = $patron->account;
899 my $debts = $account->outstanding_debits;
902 my $credit = $account->add_credit(
904 amount => $account->balance,
910 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
912 # Simulate item marked as found
913 $item->itemlost(0)->store;
914 is( $item->{_refunded}, 1, 'Refund triggered' );
915 is( $item->{_restored}, undef, 'Restore not triggered when there is no overdue fine found' );
918 subtest 'restore fine | unforgiven overdue' => sub {
922 # Set lostreturn_policy to 'restore' for tests
924 $builder->build_object( { class => "Koha::Patrons" } );
925 t::lib::Mocks::mock_userenv(
926 { patron => $manager, branchcode => $manager->branchcode } );
927 my $specific_rule_restore = $builder->build(
929 source => 'CirculationRule',
931 branchcode => $manager->branchcode,
932 categorycode => undef,
934 rule_name => 'lostreturn',
935 rule_value => 'restore'
940 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
942 my $item = $builder->build_sample_item(
944 biblionumber => $biblio->biblionumber,
945 library => $library->branchcode,
946 replacementprice => $replacement_amount,
947 itype => $item_type->itemtype
952 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
954 # Simulate item marked as lost
955 $item->itemlost(1)->store;
956 C4::Circulation::LostItem( $item->itemnumber, 1 );
958 my $processing_fee_lines = Koha::Account::Lines->search(
960 borrowernumber => $patron->id,
961 itemnumber => $item->itemnumber,
962 debit_type_code => 'PROCESSING'
965 is( $processing_fee_lines->count,
966 1, 'Only one processing fee produced' );
967 my $processing_fee_line = $processing_fee_lines->next;
968 is( $processing_fee_line->amount + 0,
970 'The right PROCESSING amount is generated' );
971 is( $processing_fee_line->amountoutstanding + 0,
973 'The right PROCESSING amountoutstanding is generated' );
975 my $lost_fee_lines = Koha::Account::Lines->search(
977 borrowernumber => $patron->id,
978 itemnumber => $item->itemnumber,
979 debit_type_code => 'LOST'
982 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
983 my $lost_fee_line = $lost_fee_lines->next;
984 is( $lost_fee_line->amount + 0,
985 $replacement_amount, 'The right LOST amount is generated' );
986 is( $lost_fee_line->amountoutstanding + 0,
988 'The right LOST amountountstanding is generated' );
990 my $account = $patron->account;
991 my $debts = $account->outstanding_debits;
994 my $credit = $account->add_credit(
996 amount => $account->balance,
1002 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
1005 my $overdue = $account->add_debit(
1008 user_id => $manager->borrowernumber,
1009 library_id => $library->branchcode,
1010 interface => 'test',
1011 item_id => $item->itemnumber,
1015 $overdue->status('LOST')->store();
1016 $overdue->discard_changes;
1017 is( $overdue->status, 'LOST',
1018 'Overdue status set to LOST' );
1020 # Simulate item marked as found
1021 $item->itemlost(0)->store;
1022 is( $item->{_refunded}, 1, 'Refund triggered' );
1023 is( $item->{_restored}, undef, 'Restore not triggered when overdue was not forgiven' );
1024 $overdue->discard_changes;
1025 is( $overdue->status, 'FOUND',
1026 'Overdue status updated to FOUND' );
1029 subtest 'restore fine | forgiven overdue' => sub {
1033 # Set lostreturn_policy to 'restore' for tests
1035 $builder->build_object( { class => "Koha::Patrons" } );
1036 t::lib::Mocks::mock_userenv(
1037 { patron => $manager, branchcode => $manager->branchcode } );
1038 my $specific_rule_restore = $builder->build(
1040 source => 'CirculationRule',
1042 branchcode => $manager->branchcode,
1043 categorycode => undef,
1045 rule_name => 'lostreturn',
1046 rule_value => 'restore'
1051 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1053 my $item = $builder->build_sample_item(
1055 biblionumber => $biblio->biblionumber,
1056 library => $library->branchcode,
1057 replacementprice => $replacement_amount,
1058 itype => $item_type->itemtype
1063 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
1065 # Simulate item marked as lost
1066 $item->itemlost(1)->store;
1067 C4::Circulation::LostItem( $item->itemnumber, 1 );
1069 my $processing_fee_lines = Koha::Account::Lines->search(
1071 borrowernumber => $patron->id,
1072 itemnumber => $item->itemnumber,
1073 debit_type_code => 'PROCESSING'
1076 is( $processing_fee_lines->count,
1077 1, 'Only one processing fee produced' );
1078 my $processing_fee_line = $processing_fee_lines->next;
1079 is( $processing_fee_line->amount + 0,
1081 'The right PROCESSING amount is generated' );
1082 is( $processing_fee_line->amountoutstanding + 0,
1084 'The right PROCESSING amountoutstanding is generated' );
1086 my $lost_fee_lines = Koha::Account::Lines->search(
1088 borrowernumber => $patron->id,
1089 itemnumber => $item->itemnumber,
1090 debit_type_code => 'LOST'
1093 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1094 my $lost_fee_line = $lost_fee_lines->next;
1095 is( $lost_fee_line->amount + 0,
1096 $replacement_amount, 'The right LOST amount is generated' );
1097 is( $lost_fee_line->amountoutstanding + 0,
1098 $replacement_amount,
1099 'The right LOST amountountstanding is generated' );
1101 my $account = $patron->account;
1102 my $debts = $account->outstanding_debits;
1105 my $credit = $account->add_credit(
1107 amount => $account->balance,
1109 interface => 'test',
1113 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
1116 my $overdue = $account->add_debit(
1119 user_id => $manager->borrowernumber,
1120 library_id => $library->branchcode,
1121 interface => 'test',
1122 item_id => $item->itemnumber,
1126 $overdue->status('LOST')->store();
1127 is( $overdue->status, 'LOST',
1128 'Overdue status set to LOST' );
1130 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 0 );
1133 $credit = $account->add_credit(
1136 user_id => $manager->borrowernumber,
1137 library_id => $library->branchcode,
1138 interface => 'test',
1140 item_id => $item->itemnumber
1144 { debits => [$overdue], offset_type => 'Forgiven' } );
1146 # Simulate item marked as found
1147 $item->itemlost(0)->store;
1148 is( $item->{_refunded}, 1, 'Refund triggered' );
1149 is( $item->{_restored}, 1, 'Restore triggered when overdue was forgiven' );
1150 $overdue->discard_changes;
1151 is( $overdue->status, 'FOUND', 'Overdue status updated to FOUND' );
1152 is( $overdue->amountoutstanding, $overdue->amount, 'Overdue outstanding has been restored' );
1153 $credit->discard_changes;
1154 is( $credit->status, 'VOID', 'Overdue Forgival has been marked as VOID');
1157 subtest 'Continue when userenv is not set' => sub {
1160 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1161 my $barcode = 'KD123456795';
1162 my $replacement_amount = 100;
1163 my $processfee_amount = 20;
1165 my $item_type = $builder->build_object(
1167 class => 'Koha::ItemTypes',
1169 notforloan => undef,
1171 defaultreplacecost => undef,
1173 rentalcharge_daily => 0,
1177 my $item = $builder->build_sample_item(
1179 biblionumber => $biblio->biblionumber,
1180 homebranch => $library->branchcode,
1181 holdingbranch => $library->branchcode,
1182 barcode => $barcode,
1183 replacementprice => $replacement_amount,
1184 itype => $item_type->itemtype
1189 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
1191 # Simulate item marked as lost
1192 $item->itemlost(1)->store;
1193 C4::Circulation::LostItem( $item->itemnumber, 1 );
1196 C4::Context->_new_userenv(undef);
1198 # Simluate item marked as found
1199 $item->itemlost(0)->store;
1200 is( $item->{_refunded}, 1, 'No refund triggered' );
1205 subtest 'log_action' => sub {
1207 t::lib::Mocks::mock_preference( 'CataloguingLog', 1 );
1209 my $item = Koha::Item->new(
1211 homebranch => $library->{branchcode},
1212 holdingbranch => $library->{branchcode},
1213 biblionumber => $biblio->biblionumber,
1214 location => 'my_loc',
1218 Koha::ActionLogs->search(
1220 module => 'CATALOGUING',
1222 object => $item->itemnumber,
1227 "Item creation logged"
1230 $item->location('another_loc')->store;
1232 Koha::ActionLogs->search(
1234 module => 'CATALOGUING',
1236 object => $item->itemnumber
1240 "Item modification logged"
1245 subtest 'get_transfer' => sub {
1248 my $transfer = $new_item_1->get_transfer();
1249 is( $transfer, undef, 'Koha::Item->get_transfer should return undef if the item is not in transit' );
1251 my $library_to = $builder->build( { source => 'Branch' } );
1253 my $transfer_1 = $builder->build_object(
1255 class => 'Koha::Item::Transfers',
1257 itemnumber => $new_item_1->itemnumber,
1258 frombranch => $new_item_1->holdingbranch,
1259 tobranch => $library_to->{branchcode},
1262 datearrived => undef,
1263 datecancelled => undef,
1264 daterequested => \'NOW()'
1269 $transfer = $new_item_1->get_transfer();
1270 is( ref($transfer), 'Koha::Item::Transfer', 'Koha::Item->get_transfer should return a Koha::Item::Transfers object' );
1272 my $transfer_2 = $builder->build_object(
1274 class => 'Koha::Item::Transfers',
1276 itemnumber => $new_item_1->itemnumber,
1277 frombranch => $new_item_1->holdingbranch,
1278 tobranch => $library_to->{branchcode},
1281 datearrived => undef,
1282 datecancelled => undef,
1283 daterequested => \'NOW()'
1288 $transfer = $new_item_1->get_transfer();
1289 is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the oldest transfer request');
1291 $transfer_2->datesent(\'NOW()')->store;
1292 $transfer = $new_item_1->get_transfer();
1293 is( $transfer->branchtransfer_id, $transfer_2->branchtransfer_id, 'Koha::Item->get_transfer returns the in_transit transfer');
1295 my $transfer_3 = $builder->build_object(
1297 class => 'Koha::Item::Transfers',
1299 itemnumber => $new_item_1->itemnumber,
1300 frombranch => $new_item_1->holdingbranch,
1301 tobranch => $library_to->{branchcode},
1304 datearrived => undef,
1305 datecancelled => undef,
1306 daterequested => \'NOW()'
1311 $transfer_2->datearrived(\'NOW()')->store;
1312 $transfer = $new_item_1->get_transfer();
1313 is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the next queued transfer');
1314 is( $transfer->itemnumber, $new_item_1->itemnumber, 'Koha::Item->get_transfer returns the right items transfer' );
1316 $transfer_1->datecancelled(\'NOW()')->store;
1317 $transfer = $new_item_1->get_transfer();
1318 is( $transfer->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfer ignores cancelled transfers');
1321 subtest 'holds' => sub {
1324 my $biblio = $builder->build_sample_biblio();
1325 my $item = $builder->build_sample_item({
1326 biblionumber => $biblio->biblionumber,
1328 is($item->holds->count, 0, "Nothing returned if no holds");
1329 my $hold1 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'T' }});
1330 my $hold2 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1331 my $hold3 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1333 is($item->holds()->count,3,"Three holds found");
1334 is($item->holds({found => 'W'})->count,2,"Two waiting holds found");
1335 is_deeply($item->holds({found => 'T'})->next->unblessed,$hold1,"Found transit holds matches the hold");
1336 is($item->holds({found => undef})->count, 0,"Nothing returned if no matching holds");
1339 subtest 'biblio' => sub {
1342 my $biblio = $retrieved_item_1->biblio;
1343 is( ref( $biblio ), 'Koha::Biblio', 'Koha::Item->biblio should return a Koha::Biblio' );
1344 is( $biblio->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblio should return the correct biblio' );
1347 subtest 'biblioitem' => sub {
1350 my $biblioitem = $retrieved_item_1->biblioitem;
1351 is( ref( $biblioitem ), 'Koha::Biblioitem', 'Koha::Item->biblioitem should return a Koha::Biblioitem' );
1352 is( $biblioitem->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblioitem should return the correct biblioitem' );
1356 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
1357 subtest 'checkout' => sub {
1359 my $item = Koha::Items->find( $new_item_1->itemnumber );
1361 my $checkout = $item->checkout;
1362 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no current checkout on this item' );
1365 my $patron = $builder->build({ source => 'Borrower' });
1366 C4::Circulation::AddIssue( $patron, $item->barcode );
1367 $checkout = $retrieved_item_1->checkout;
1368 is( ref( $checkout ), 'Koha::Checkout', 'Koha::Item->checkout should return a Koha::Checkout' );
1369 is( $checkout->itemnumber, $item->itemnumber, 'Koha::Item->checkout should return the correct checkout' );
1370 is( $checkout->borrowernumber, $patron->{borrowernumber}, 'Koha::Item->checkout should return the correct checkout' );
1373 C4::Circulation::AddReturn( $item->barcode );
1375 # There is no more checkout on this item, making sure it will not return old checkouts
1376 $checkout = $item->checkout;
1377 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no *current* checkout on this item' );
1380 subtest 'can_be_transferred' => sub {
1383 t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
1384 t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
1386 my $biblio = $builder->build_sample_biblio();
1387 my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
1388 my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
1389 my $item = $builder->build_sample_item({
1390 biblionumber => $biblio->biblionumber,
1391 homebranch => $library1->branchcode,
1392 holdingbranch => $library1->branchcode,
1395 is(Koha::Item::Transfer::Limits->search({
1396 fromBranch => $library1->branchcode,
1397 toBranch => $library2->branchcode,
1398 })->count, 0, 'There are no transfer limits between libraries.');
1399 ok($item->can_be_transferred({ to => $library2 }),
1400 'Item can be transferred between libraries.');
1402 my $limit = Koha::Item::Transfer::Limit->new({
1403 fromBranch => $library1->branchcode,
1404 toBranch => $library2->branchcode,
1405 itemtype => $item->effective_itemtype,
1407 is(Koha::Item::Transfer::Limits->search({
1408 fromBranch => $library1->branchcode,
1409 toBranch => $library2->branchcode,
1410 })->count, 1, 'Given we have added a transfer limit,');
1411 is($item->can_be_transferred({ to => $library2 }), 0,
1412 'Item can no longer be transferred between libraries.');
1413 is($item->can_be_transferred({ to => $library2, from => $library1 }), 0,
1414 'We get the same result also if we pass the from-library parameter.');
1417 subtest 'filter_by_for_loan' => sub {
1420 my $biblio = $builder->build_sample_biblio;
1421 is( $biblio->items->filter_by_for_loan->count, 0, 'no item yet' );
1422 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 1 } );
1423 is( $biblio->items->filter_by_for_loan->count, 0, 'no item for loan' );
1424 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 0 } );
1425 is( $biblio->items->filter_by_for_loan->count, 1, '1 item for loan' );
1430 # Reset nb_of_items prior to testing delete
1431 $nb_of_items = Koha::Items->search->count;
1434 $retrieved_item_1->delete;
1435 is( Koha::Items->search->count, $nb_of_items - 1, 'Delete should have deleted the item' );
1437 $schema->storage->txn_rollback;
1439 subtest 'filter_by_visible_in_opac() tests' => sub {
1443 $schema->storage->txn_begin;
1445 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1446 my $mocked_category = Test::MockModule->new('Koha::Patron::Category');
1448 $mocked_category->mock( 'override_hidden_items', sub {
1452 # have a fresh biblio
1453 my $biblio = $builder->build_sample_biblio;
1454 # have two itemtypes
1455 my $itype_1 = $builder->build_object({ class => 'Koha::ItemTypes' });
1456 my $itype_2 = $builder->build_object({ class => 'Koha::ItemTypes' });
1457 # have 5 items on that biblio
1458 my $item_1 = $builder->build_sample_item(
1460 biblionumber => $biblio->biblionumber,
1462 itype => $itype_1->itemtype,
1467 my $item_2 = $builder->build_sample_item(
1469 biblionumber => $biblio->biblionumber,
1471 itype => $itype_2->itemtype,
1476 my $item_3 = $builder->build_sample_item(
1478 biblionumber => $biblio->biblionumber,
1480 itype => $itype_1->itemtype,
1485 my $item_4 = $builder->build_sample_item(
1487 biblionumber => $biblio->biblionumber,
1489 itype => $itype_2->itemtype,
1494 my $item_5 = $builder->build_sample_item(
1496 biblionumber => $biblio->biblionumber,
1498 itype => $itype_1->itemtype,
1503 my $item_6 = $builder->build_sample_item(
1505 biblionumber => $biblio->biblionumber,
1507 itype => $itype_1->itemtype,
1515 my $mocked_context = Test::MockModule->new('C4::Context');
1516 $mocked_context->mock( 'yaml_preference', sub {
1520 t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
1521 is( $biblio->items->filter_by_visible_in_opac->count,
1522 6, 'No rules passed, hidelostitems unset' );
1524 is( $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1525 6, 'No rules passed, hidelostitems unset, patron exception changes nothing' );
1527 $rules = { copynumber => [ 2 ] };
1529 t::lib::Mocks::mock_preference( 'hidelostitems', 1 );
1531 $biblio->items->filter_by_visible_in_opac->count,
1533 'No rules passed, hidelostitems set'
1537 $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1539 'No rules passed, hidelostitems set, patron exception changes nothing'
1542 $rules = { withdrawn => [ 1, 2 ], copynumber => [ 2 ] };
1544 $biblio->items->filter_by_visible_in_opac->count,
1546 'Rules on withdrawn, hidelostitems set'
1550 $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1552 'hidelostitems set, rules on withdrawn but patron override passed'
1555 $rules = { itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1557 $biblio->items->filter_by_visible_in_opac->count,
1559 'Rules on itype, hidelostitems set'
1562 $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1564 $biblio->items->filter_by_visible_in_opac->count,
1566 'Rules on itype and withdrawn, hidelostitems set'
1569 $biblio->items->filter_by_visible_in_opac
1571 $item_4->itemnumber,
1572 'The right item is returned'
1575 $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_2->itemtype ], copynumber => [ 2 ] };
1577 $biblio->items->filter_by_visible_in_opac->count,
1579 'Rules on itype and withdrawn, hidelostitems set'
1582 $biblio->items->filter_by_visible_in_opac
1584 $item_5->itemnumber,
1585 'The right item is returned'
1588 $schema->storage->txn_rollback;
1591 subtest 'filter_out_lost() tests' => sub {
1595 $schema->storage->txn_begin;
1597 # have a fresh biblio
1598 my $biblio = $builder->build_sample_biblio;
1599 # have 3 items on that biblio
1600 my $item_1 = $builder->build_sample_item(
1602 biblionumber => $biblio->biblionumber,
1606 my $item_2 = $builder->build_sample_item(
1608 biblionumber => $biblio->biblionumber,
1612 my $item_3 = $builder->build_sample_item(
1614 biblionumber => $biblio->biblionumber,
1619 is( $biblio->items->filter_out_lost->next->itemnumber, $item_2->itemnumber, 'Right item returned' );
1620 is( $biblio->items->filter_out_lost->count, 1, 'Only one item is not lost' );
1622 $schema->storage->txn_rollback;