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 subtest 'location passed to ->store' => sub {
100 my $location = 'my_loc';
102 homebranch => $library->{branchcode},
103 holdingbranch => $library->{branchcode},
104 biblionumber => $biblio->biblionumber,
105 location => $location,
109 # NewItemsDefaultLocation not set
110 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', '' );
112 # Not passing permanent_location on creating the item
113 my $item = Koha::Item->new($attributes)->store->get_from_storage;
114 is( $item->location, $location,
115 'location must have been set to location if given' );
116 is( $item->permanent_location, $item->location,
117 'permanent_location must have been set to location if not given' );
120 # Passing permanent_location on creating the item
121 $item = Koha::Item->new(
122 { %$attributes, permanent_location => 'perm_loc' } )
123 ->store->get_from_storage;
124 is( $item->permanent_location, 'perm_loc',
125 'permanent_location must have been kept if given' );
130 # NewItemsDefaultLocation set
131 my $default_location = 'default_location';
132 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', $default_location );
134 # Not passing permanent_location on creating the item
135 my $item = Koha::Item->new($attributes)->store->get_from_storage;
136 is( $item->location, $location,
137 'location must have been kept if given' );
138 is( $item->permanent_location, $location,
139 'permanent_location must have been set to the location given' );
142 # Passing permanent_location on creating the item
143 $item = Koha::Item->new(
144 { %$attributes, permanent_location => 'perm_loc' } )
145 ->store->get_from_storage;
146 is( $item->location, $location,
147 'location must have been kept if given' );
148 is( $item->permanent_location, 'perm_loc',
149 'permanent_location must have been kept if given' );
154 subtest 'location NOT passed to ->store' => sub {
158 homebranch => $library->{branchcode},
159 holdingbranch => $library->{branchcode},
160 biblionumber => $biblio->biblionumber,
164 # NewItemsDefaultLocation not set
165 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', '' );
167 # Not passing permanent_location on creating the item
168 my $item = Koha::Item->new($attributes)->store->get_from_storage;
169 is( $item->location, undef,
170 'location not passed and no default, it is undef' );
171 is( $item->permanent_location, $item->location,
172 'permanent_location must have been set to location if not given' );
175 # Passing permanent_location on creating the item
176 $item = Koha::Item->new(
177 { %$attributes, permanent_location => 'perm_loc' } )
178 ->store->get_from_storage;
179 is( $item->permanent_location, 'perm_loc',
180 'permanent_location must have been kept if given' );
185 # NewItemsDefaultLocation set
186 my $default_location = 'default_location';
187 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', $default_location );
189 # Not passing permanent_location on creating the item
190 my $item = Koha::Item->new($attributes)->store->get_from_storage;
191 is( $item->location, $default_location,
192 'location must have been set to default location if not given' );
193 is( $item->permanent_location, $default_location,
194 'permanent_location must have been set to the default location as well' );
197 # Passing permanent_location on creating the item
198 $item = Koha::Item->new(
199 { %$attributes, permanent_location => 'perm_loc' } )
200 ->store->get_from_storage;
201 is( $item->location, $default_location,
202 'location must have been set to default location if not given' );
203 is( $item->permanent_location, 'perm_loc',
204 'permanent_location must have been kept if given' );
211 subtest '*_on updates' => sub {
214 # Once the '_on' value is set (triggered by the related field turning from false to true)
215 # it should not be re-set for any changes outside of the related field being 'unset'.
217 my @fields = qw( itemlost withdrawn damaged );
218 my $today = dt_from_string();
219 my $yesterday = $today->clone()->subtract( days => 1 );
221 for my $field ( @fields ) {
222 my $item = $builder->build_sample_item(
225 itemlost_on => undef,
227 withdrawn_on => undef,
232 my $field_on = $field . '_on';
234 # Set field for the first time
235 Time::Fake->offset( $yesterday->epoch );
236 $item->$field(1)->store;
237 $item->get_from_storage;
238 is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
239 0, $field_on . " was set upon first truthy setting" );
241 # Update the field to a new 'true' value
242 Time::Fake->offset( $today->epoch );
243 $item->$field(2)->store;
244 $item->get_from_storage;
245 is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
246 0, $field_on . " was not updated upon second truthy setting" );
248 # Update the field to a new 'false' value
249 $item->$field(0)->store;
250 $item->get_from_storage;
251 is($item->$field_on, undef, $field_on . " was unset upon untruthy setting");
257 subtest '_lost_found_trigger' => sub {
260 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
261 t::lib::Mocks::mock_preference( 'WhenLostForgiveFine', 0 );
263 my $processfee_amount = 20;
264 my $replacement_amount = 99.00;
265 my $item_type = $builder->build_object(
267 class => 'Koha::ItemTypes',
271 defaultreplacecost => undef,
272 processfee => $processfee_amount,
273 rentalcharge_daily => 0,
277 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
279 $biblio = $builder->build_sample_biblio( { author => 'Hall, Daria' } );
281 subtest 'Full write-off tests' => sub {
285 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
287 $builder->build_object( { class => "Koha::Patrons" } );
288 t::lib::Mocks::mock_userenv(
289 { patron => $manager, branchcode => $manager->branchcode } );
291 my $item = $builder->build_sample_item(
293 biblionumber => $biblio->biblionumber,
294 library => $library->branchcode,
295 replacementprice => $replacement_amount,
296 itype => $item_type->itemtype,
300 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
302 # Simulate item marked as lost
303 $item->itemlost(3)->store;
304 C4::Circulation::LostItem( $item->itemnumber, 1 );
306 my $processing_fee_lines = Koha::Account::Lines->search(
308 borrowernumber => $patron->id,
309 itemnumber => $item->itemnumber,
310 debit_type_code => 'PROCESSING'
313 is( $processing_fee_lines->count,
314 1, 'Only one processing fee produced' );
315 my $processing_fee_line = $processing_fee_lines->next;
316 is( $processing_fee_line->amount + 0,
318 'The right PROCESSING amount is generated' );
319 is( $processing_fee_line->amountoutstanding + 0,
321 'The right PROCESSING amountoutstanding is generated' );
323 my $lost_fee_lines = Koha::Account::Lines->search(
325 borrowernumber => $patron->id,
326 itemnumber => $item->itemnumber,
327 debit_type_code => 'LOST'
330 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
331 my $lost_fee_line = $lost_fee_lines->next;
332 is( $lost_fee_line->amount + 0,
333 $replacement_amount, 'The right LOST amount is generated' );
334 is( $lost_fee_line->amountoutstanding + 0,
336 'The right LOST amountoutstanding is generated' );
337 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
339 my $account = $patron->account;
340 my $debts = $account->outstanding_debits;
343 my $credit = $account->add_credit(
345 amount => $account->balance,
351 { debits => [ $debts->as_list ], offset_type => 'Writeoff' } );
353 # Simulate item marked as found
354 $item->itemlost(0)->store;
355 is( $item->{_refunded}, undef, 'No LOST_FOUND account line added' );
357 $lost_fee_line->discard_changes; # reload from DB
358 is( $lost_fee_line->amountoutstanding + 0,
359 0, 'Lost fee has no outstanding amount' );
360 is( $lost_fee_line->debit_type_code,
361 'LOST', 'Lost fee now still has account type of LOST' );
362 is( $lost_fee_line->status, 'FOUND',
363 "Lost fee now has account status of FOUND - No Refund" );
365 is( $patron->account->balance,
366 -0, 'The patron balance is 0, everything was written off' );
369 subtest 'Full payment tests' => sub {
373 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
375 my $item = $builder->build_sample_item(
377 biblionumber => $biblio->biblionumber,
378 library => $library->branchcode,
379 replacementprice => $replacement_amount,
380 itype => $item_type->itemtype
385 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
387 # Simulate item marked as lost
388 $item->itemlost(1)->store;
389 C4::Circulation::LostItem( $item->itemnumber, 1 );
391 my $processing_fee_lines = Koha::Account::Lines->search(
393 borrowernumber => $patron->id,
394 itemnumber => $item->itemnumber,
395 debit_type_code => 'PROCESSING'
398 is( $processing_fee_lines->count,
399 1, 'Only one processing fee produced' );
400 my $processing_fee_line = $processing_fee_lines->next;
401 is( $processing_fee_line->amount + 0,
403 'The right PROCESSING amount is generated' );
404 is( $processing_fee_line->amountoutstanding + 0,
406 'The right PROCESSING amountoutstanding is generated' );
408 my $lost_fee_lines = Koha::Account::Lines->search(
410 borrowernumber => $patron->id,
411 itemnumber => $item->itemnumber,
412 debit_type_code => 'LOST'
415 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
416 my $lost_fee_line = $lost_fee_lines->next;
417 is( $lost_fee_line->amount + 0,
418 $replacement_amount, 'The right LOST amount is generated' );
419 is( $lost_fee_line->amountoutstanding + 0,
421 'The right LOST amountountstanding is generated' );
423 my $account = $patron->account;
424 my $debts = $account->outstanding_debits;
427 my $credit = $account->add_credit(
429 amount => $account->balance,
435 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
437 # Simulate item marked as found
438 $item->itemlost(0)->store;
439 is( $item->{_refunded}, 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 $lost_fee_line->discard_changes;
460 is( $lost_fee_line->amountoutstanding + 0,
461 0, 'Lost fee has no outstanding amount' );
462 is( $lost_fee_line->debit_type_code,
463 'LOST', 'Lost fee now still has account type of LOST' );
464 is( $lost_fee_line->status, 'FOUND',
465 "Lost fee now has account status of FOUND" );
467 is( $patron->account->balance, -99,
468 'The patron balance is -99, a credit that equals the lost fee payment'
472 subtest 'Test without payment or write off' => sub {
476 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
478 my $item = $builder->build_sample_item(
480 biblionumber => $biblio->biblionumber,
481 library => $library->branchcode,
482 replacementprice => 23.00,
483 replacementprice => $replacement_amount,
484 itype => $item_type->itemtype
489 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
491 # Simulate item marked as lost
492 $item->itemlost(3)->store;
493 C4::Circulation::LostItem( $item->itemnumber, 1 );
495 my $processing_fee_lines = Koha::Account::Lines->search(
497 borrowernumber => $patron->id,
498 itemnumber => $item->itemnumber,
499 debit_type_code => 'PROCESSING'
502 is( $processing_fee_lines->count,
503 1, 'Only one processing fee produced' );
504 my $processing_fee_line = $processing_fee_lines->next;
505 is( $processing_fee_line->amount + 0,
507 'The right PROCESSING amount is generated' );
508 is( $processing_fee_line->amountoutstanding + 0,
510 'The right PROCESSING amountoutstanding is generated' );
512 my $lost_fee_lines = Koha::Account::Lines->search(
514 borrowernumber => $patron->id,
515 itemnumber => $item->itemnumber,
516 debit_type_code => 'LOST'
519 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
520 my $lost_fee_line = $lost_fee_lines->next;
521 is( $lost_fee_line->amount + 0,
522 $replacement_amount, 'The right LOST amount is generated' );
523 is( $lost_fee_line->amountoutstanding + 0,
525 'The right LOST amountountstanding is generated' );
527 # Simulate item marked as found
528 $item->itemlost(0)->store;
529 is( $item->{_refunded}, 1, 'Refund triggered' );
531 my $credit_return = Koha::Account::Lines->search(
533 itemnumber => $item->itemnumber,
534 credit_type_code => 'LOST_FOUND'
539 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
540 is( $credit_return->amount + 0,
542 'The account line of type LOST_FOUND has an amount of -99' );
544 $credit_return->amountoutstanding + 0,
546 'The account line of type LOST_FOUND has an amountoutstanding of 0'
549 $lost_fee_line->discard_changes;
550 is( $lost_fee_line->amountoutstanding + 0,
551 0, 'Lost fee has no outstanding amount' );
552 is( $lost_fee_line->debit_type_code,
553 'LOST', 'Lost fee now still has account type of LOST' );
554 is( $lost_fee_line->status, 'FOUND',
555 "Lost fee now has account status of FOUND" );
557 is( $patron->account->balance,
558 20, 'The patron balance is 20, still owes the processing fee' );
562 'Test with partial payement and write off, and remaining debt' =>
567 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
568 my $item = $builder->build_sample_item(
570 biblionumber => $biblio->biblionumber,
571 library => $library->branchcode,
572 replacementprice => $replacement_amount,
573 itype => $item_type->itemtype
578 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
580 # Simulate item marked as lost
581 $item->itemlost(1)->store;
582 C4::Circulation::LostItem( $item->itemnumber, 1 );
584 my $processing_fee_lines = Koha::Account::Lines->search(
586 borrowernumber => $patron->id,
587 itemnumber => $item->itemnumber,
588 debit_type_code => 'PROCESSING'
591 is( $processing_fee_lines->count,
592 1, 'Only one processing fee produced' );
593 my $processing_fee_line = $processing_fee_lines->next;
594 is( $processing_fee_line->amount + 0,
596 'The right PROCESSING amount is generated' );
597 is( $processing_fee_line->amountoutstanding + 0,
599 'The right PROCESSING amountoutstanding is generated' );
601 my $lost_fee_lines = Koha::Account::Lines->search(
603 borrowernumber => $patron->id,
604 itemnumber => $item->itemnumber,
605 debit_type_code => 'LOST'
608 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
609 my $lost_fee_line = $lost_fee_lines->next;
610 is( $lost_fee_line->amount + 0,
611 $replacement_amount, 'The right LOST amount is generated' );
612 is( $lost_fee_line->amountoutstanding + 0,
614 'The right LOST amountountstanding is generated' );
616 my $account = $patron->account;
619 $processfee_amount + $replacement_amount,
620 'Balance is PROCESSING + L'
624 my $payment_amount = 27;
625 my $payment = $account->add_credit(
627 amount => $payment_amount,
634 { debits => [$lost_fee_line], offset_type => 'Payment' } );
636 # Partially write off fee
637 my $write_off_amount = 25;
638 my $write_off = $account->add_credit(
640 amount => $write_off_amount,
646 { debits => [$lost_fee_line], offset_type => 'Writeoff' } );
651 $replacement_amount -
654 'Payment and write off applied'
657 # Store the amountoutstanding value
658 $lost_fee_line->discard_changes;
659 my $outstanding = $lost_fee_line->amountoutstanding;
661 # Simulate item marked as found
662 $item->itemlost(0)->store;
663 is( $item->{_refunded}, 1, 'Refund triggered' );
665 my $credit_return = Koha::Account::Lines->search(
667 itemnumber => $item->itemnumber,
668 credit_type_code => 'LOST_FOUND'
673 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
677 $processfee_amount - $payment_amount,
678 'Balance is PROCESSING - PAYMENT (LOST_FOUND)'
681 $lost_fee_line->discard_changes;
682 is( $lost_fee_line->amountoutstanding + 0,
683 0, 'Lost fee has no outstanding amount' );
684 is( $lost_fee_line->debit_type_code,
685 'LOST', 'Lost fee now still has account type of LOST' );
686 is( $lost_fee_line->status, 'FOUND',
687 "Lost fee now has account status of FOUND" );
690 $credit_return->amount + 0,
691 ( $payment_amount + $outstanding ) * -1,
692 'The account line of type LOST_FOUND has an amount equal to the payment + outstanding'
695 $credit_return->amountoutstanding + 0,
696 $payment_amount * -1,
697 'The account line of type LOST_FOUND has an amountoutstanding equal to the payment'
702 $processfee_amount - $payment_amount,
703 'The patron balance is the difference between the PROCESSING and the credit'
707 subtest 'Partial payment, existing debits and AccountAutoReconcile' =>
712 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
713 my $barcode = 'KD123456793';
714 my $replacement_amount = 100;
715 my $processfee_amount = 20;
717 my $item_type = $builder->build_object(
719 class => 'Koha::ItemTypes',
723 defaultreplacecost => undef,
725 rentalcharge_daily => 0,
729 my $item = Koha::Item->new(
731 biblionumber => $biblio->biblionumber,
732 homebranch => $library->branchcode,
733 holdingbranch => $library->branchcode,
735 replacementprice => $replacement_amount,
736 itype => $item_type->itemtype
741 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
743 # Simulate item marked as lost
744 $item->itemlost(1)->store;
745 C4::Circulation::LostItem( $item->itemnumber, 1 );
747 my $lost_fee_lines = Koha::Account::Lines->search(
749 borrowernumber => $patron->id,
750 itemnumber => $item->itemnumber,
751 debit_type_code => 'LOST'
754 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
755 my $lost_fee_line = $lost_fee_lines->next;
756 is( $lost_fee_line->amount + 0,
757 $replacement_amount, 'The right LOST amount is generated' );
758 is( $lost_fee_line->amountoutstanding + 0,
760 'The right LOST amountountstanding is generated' );
762 my $account = $patron->account;
763 is( $account->balance, $replacement_amount, 'Balance is L' );
766 my $payment_amount = 27;
767 my $payment = $account->add_credit(
769 amount => $payment_amount,
775 { debits => [$lost_fee_line], offset_type => 'Payment' } );
779 $replacement_amount - $payment_amount,
783 my $manual_debit_amount = 80;
786 amount => $manual_debit_amount,
794 $manual_debit_amount + $replacement_amount - $payment_amount,
795 'Manual debit applied'
798 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
800 # Simulate item marked as found
801 $item->itemlost(0)->store;
802 is( $item->{_refunded}, 1, 'Refund triggered' );
804 my $credit_return = Koha::Account::Lines->search(
806 itemnumber => $item->itemnumber,
807 credit_type_code => 'LOST_FOUND'
812 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
816 $manual_debit_amount - $payment_amount,
817 'Balance is PROCESSING - payment (LOST_FOUND)'
820 my $manual_debit = Koha::Account::Lines->search(
822 borrowernumber => $patron->id,
823 debit_type_code => 'OVERDUE',
824 status => 'UNRETURNED'
828 $manual_debit->amountoutstanding + 0,
829 $manual_debit_amount - $payment_amount,
830 'reconcile_balance was called'
834 subtest 'Patron deleted' => sub {
837 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
838 my $barcode = 'KD123456794';
839 my $replacement_amount = 100;
840 my $processfee_amount = 20;
842 my $item_type = $builder->build_object(
844 class => 'Koha::ItemTypes',
848 defaultreplacecost => undef,
850 rentalcharge_daily => 0,
854 my $item = Koha::Item->new(
856 biblionumber => $biblio->biblionumber,
857 homebranch => $library->branchcode,
858 holdingbranch => $library->branchcode,
860 replacementprice => $replacement_amount,
861 itype => $item_type->itemtype
866 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
868 # Simulate item marked as lost
869 $item->itemlost(1)->store;
870 C4::Circulation::LostItem( $item->itemnumber, 1 );
875 # Simulate item marked as found
876 $item->itemlost(0)->store;
877 is( $item->{_refunded}, undef, 'No refund triggered' );
881 subtest 'restore fine | no overdue' => sub {
886 $builder->build_object( { class => "Koha::Patrons" } );
887 t::lib::Mocks::mock_userenv(
888 { patron => $manager, branchcode => $manager->branchcode } );
890 # Set lostreturn_policy to 'restore' for tests
891 my $specific_rule_restore = $builder->build(
893 source => 'CirculationRule',
895 branchcode => $manager->branchcode,
896 categorycode => undef,
898 rule_name => 'lostreturn',
899 rule_value => 'restore'
904 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
906 my $item = $builder->build_sample_item(
908 biblionumber => $biblio->biblionumber,
909 library => $library->branchcode,
910 replacementprice => $replacement_amount,
911 itype => $item_type->itemtype
916 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
918 # Simulate item marked as lost
919 $item->itemlost(1)->store;
920 C4::Circulation::LostItem( $item->itemnumber, 1 );
922 my $processing_fee_lines = Koha::Account::Lines->search(
924 borrowernumber => $patron->id,
925 itemnumber => $item->itemnumber,
926 debit_type_code => 'PROCESSING'
929 is( $processing_fee_lines->count,
930 1, 'Only one processing fee produced' );
931 my $processing_fee_line = $processing_fee_lines->next;
932 is( $processing_fee_line->amount + 0,
934 'The right PROCESSING amount is generated' );
935 is( $processing_fee_line->amountoutstanding + 0,
937 'The right PROCESSING amountoutstanding is generated' );
939 my $lost_fee_lines = Koha::Account::Lines->search(
941 borrowernumber => $patron->id,
942 itemnumber => $item->itemnumber,
943 debit_type_code => 'LOST'
946 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
947 my $lost_fee_line = $lost_fee_lines->next;
948 is( $lost_fee_line->amount + 0,
949 $replacement_amount, 'The right LOST amount is generated' );
950 is( $lost_fee_line->amountoutstanding + 0,
952 'The right LOST amountountstanding is generated' );
954 my $account = $patron->account;
955 my $debts = $account->outstanding_debits;
958 my $credit = $account->add_credit(
960 amount => $account->balance,
966 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
968 # Simulate item marked as found
969 $item->itemlost(0)->store;
970 is( $item->{_refunded}, 1, 'Refund triggered' );
971 is( $item->{_restored}, undef, 'Restore not triggered when there is no overdue fine found' );
974 subtest 'restore fine | unforgiven overdue' => sub {
978 # Set lostreturn_policy to 'restore' for tests
980 $builder->build_object( { class => "Koha::Patrons" } );
981 t::lib::Mocks::mock_userenv(
982 { patron => $manager, branchcode => $manager->branchcode } );
983 my $specific_rule_restore = $builder->build(
985 source => 'CirculationRule',
987 branchcode => $manager->branchcode,
988 categorycode => undef,
990 rule_name => 'lostreturn',
991 rule_value => 'restore'
996 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
998 my $item = $builder->build_sample_item(
1000 biblionumber => $biblio->biblionumber,
1001 library => $library->branchcode,
1002 replacementprice => $replacement_amount,
1003 itype => $item_type->itemtype
1008 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
1010 # Simulate item marked as lost
1011 $item->itemlost(1)->store;
1012 C4::Circulation::LostItem( $item->itemnumber, 1 );
1014 my $processing_fee_lines = Koha::Account::Lines->search(
1016 borrowernumber => $patron->id,
1017 itemnumber => $item->itemnumber,
1018 debit_type_code => 'PROCESSING'
1021 is( $processing_fee_lines->count,
1022 1, 'Only one processing fee produced' );
1023 my $processing_fee_line = $processing_fee_lines->next;
1024 is( $processing_fee_line->amount + 0,
1026 'The right PROCESSING amount is generated' );
1027 is( $processing_fee_line->amountoutstanding + 0,
1029 'The right PROCESSING amountoutstanding is generated' );
1031 my $lost_fee_lines = Koha::Account::Lines->search(
1033 borrowernumber => $patron->id,
1034 itemnumber => $item->itemnumber,
1035 debit_type_code => 'LOST'
1038 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1039 my $lost_fee_line = $lost_fee_lines->next;
1040 is( $lost_fee_line->amount + 0,
1041 $replacement_amount, 'The right LOST amount is generated' );
1042 is( $lost_fee_line->amountoutstanding + 0,
1043 $replacement_amount,
1044 'The right LOST amountountstanding is generated' );
1046 my $account = $patron->account;
1047 my $debts = $account->outstanding_debits;
1050 my $credit = $account->add_credit(
1052 amount => $account->balance,
1054 interface => 'test',
1058 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
1061 my $overdue = $account->add_debit(
1064 user_id => $manager->borrowernumber,
1065 library_id => $library->branchcode,
1066 interface => 'test',
1067 item_id => $item->itemnumber,
1071 $overdue->status('LOST')->store();
1072 $overdue->discard_changes;
1073 is( $overdue->status, 'LOST',
1074 'Overdue status set to LOST' );
1076 # Simulate item marked as found
1077 $item->itemlost(0)->store;
1078 is( $item->{_refunded}, 1, 'Refund triggered' );
1079 is( $item->{_restored}, undef, 'Restore not triggered when overdue was not forgiven' );
1080 $overdue->discard_changes;
1081 is( $overdue->status, 'FOUND',
1082 'Overdue status updated to FOUND' );
1085 subtest 'restore fine | forgiven overdue' => sub {
1089 # Set lostreturn_policy to 'restore' for tests
1091 $builder->build_object( { class => "Koha::Patrons" } );
1092 t::lib::Mocks::mock_userenv(
1093 { patron => $manager, branchcode => $manager->branchcode } );
1094 my $specific_rule_restore = $builder->build(
1096 source => 'CirculationRule',
1098 branchcode => $manager->branchcode,
1099 categorycode => undef,
1101 rule_name => 'lostreturn',
1102 rule_value => 'restore'
1107 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1109 my $item = $builder->build_sample_item(
1111 biblionumber => $biblio->biblionumber,
1112 library => $library->branchcode,
1113 replacementprice => $replacement_amount,
1114 itype => $item_type->itemtype
1119 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
1121 # Simulate item marked as lost
1122 $item->itemlost(1)->store;
1123 C4::Circulation::LostItem( $item->itemnumber, 1 );
1125 my $processing_fee_lines = Koha::Account::Lines->search(
1127 borrowernumber => $patron->id,
1128 itemnumber => $item->itemnumber,
1129 debit_type_code => 'PROCESSING'
1132 is( $processing_fee_lines->count,
1133 1, 'Only one processing fee produced' );
1134 my $processing_fee_line = $processing_fee_lines->next;
1135 is( $processing_fee_line->amount + 0,
1137 'The right PROCESSING amount is generated' );
1138 is( $processing_fee_line->amountoutstanding + 0,
1140 'The right PROCESSING amountoutstanding is generated' );
1142 my $lost_fee_lines = Koha::Account::Lines->search(
1144 borrowernumber => $patron->id,
1145 itemnumber => $item->itemnumber,
1146 debit_type_code => 'LOST'
1149 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1150 my $lost_fee_line = $lost_fee_lines->next;
1151 is( $lost_fee_line->amount + 0,
1152 $replacement_amount, 'The right LOST amount is generated' );
1153 is( $lost_fee_line->amountoutstanding + 0,
1154 $replacement_amount,
1155 'The right LOST amountountstanding is generated' );
1157 my $account = $patron->account;
1158 my $debts = $account->outstanding_debits;
1161 my $credit = $account->add_credit(
1163 amount => $account->balance,
1165 interface => 'test',
1169 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
1172 my $overdue = $account->add_debit(
1175 user_id => $manager->borrowernumber,
1176 library_id => $library->branchcode,
1177 interface => 'test',
1178 item_id => $item->itemnumber,
1182 $overdue->status('LOST')->store();
1183 is( $overdue->status, 'LOST',
1184 'Overdue status set to LOST' );
1186 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 0 );
1189 $credit = $account->add_credit(
1192 user_id => $manager->borrowernumber,
1193 library_id => $library->branchcode,
1194 interface => 'test',
1196 item_id => $item->itemnumber
1200 { debits => [$overdue], offset_type => 'Forgiven' } );
1202 # Simulate item marked as found
1203 $item->itemlost(0)->store;
1204 is( $item->{_refunded}, 1, 'Refund triggered' );
1205 is( $item->{_restored}, 1, 'Restore triggered when overdue was forgiven' );
1206 $overdue->discard_changes;
1207 is( $overdue->status, 'FOUND', 'Overdue status updated to FOUND' );
1208 is( $overdue->amountoutstanding, $overdue->amount, 'Overdue outstanding has been restored' );
1209 $credit->discard_changes;
1210 is( $credit->status, 'VOID', 'Overdue Forgival has been marked as VOID');
1213 subtest 'Continue when userenv is not set' => sub {
1216 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1217 my $barcode = 'KD123456795';
1218 my $replacement_amount = 100;
1219 my $processfee_amount = 20;
1221 my $item_type = $builder->build_object(
1223 class => 'Koha::ItemTypes',
1225 notforloan => undef,
1227 defaultreplacecost => undef,
1229 rentalcharge_daily => 0,
1233 my $item = $builder->build_sample_item(
1235 biblionumber => $biblio->biblionumber,
1236 homebranch => $library->branchcode,
1237 holdingbranch => $library->branchcode,
1238 barcode => $barcode,
1239 replacementprice => $replacement_amount,
1240 itype => $item_type->itemtype
1245 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
1247 # Simulate item marked as lost
1248 $item->itemlost(1)->store;
1249 C4::Circulation::LostItem( $item->itemnumber, 1 );
1252 C4::Context->_new_userenv(undef);
1254 # Simluate item marked as found
1255 $item->itemlost(0)->store;
1256 is( $item->{_refunded}, 1, 'No refund triggered' );
1261 subtest 'log_action' => sub {
1263 t::lib::Mocks::mock_preference( 'CataloguingLog', 1 );
1265 my $item = Koha::Item->new(
1267 homebranch => $library->{branchcode},
1268 holdingbranch => $library->{branchcode},
1269 biblionumber => $biblio->biblionumber,
1270 location => 'my_loc',
1274 Koha::ActionLogs->search(
1276 module => 'CATALOGUING',
1278 object => $item->itemnumber,
1283 "Item creation logged"
1286 $item->location('another_loc')->store;
1288 Koha::ActionLogs->search(
1290 module => 'CATALOGUING',
1292 object => $item->itemnumber
1296 "Item modification logged"
1301 subtest 'get_transfer' => sub {
1304 my $transfer = $new_item_1->get_transfer();
1305 is( $transfer, undef, 'Koha::Item->get_transfer should return undef if the item is not in transit' );
1307 my $library_to = $builder->build( { source => 'Branch' } );
1309 my $transfer_1 = $builder->build_object(
1311 class => 'Koha::Item::Transfers',
1313 itemnumber => $new_item_1->itemnumber,
1314 frombranch => $new_item_1->holdingbranch,
1315 tobranch => $library_to->{branchcode},
1318 datearrived => undef,
1319 datecancelled => undef,
1320 daterequested => \'NOW()'
1325 $transfer = $new_item_1->get_transfer();
1326 is( ref($transfer), 'Koha::Item::Transfer', 'Koha::Item->get_transfer should return a Koha::Item::Transfers object' );
1328 my $transfer_2 = $builder->build_object(
1330 class => 'Koha::Item::Transfers',
1332 itemnumber => $new_item_1->itemnumber,
1333 frombranch => $new_item_1->holdingbranch,
1334 tobranch => $library_to->{branchcode},
1337 datearrived => undef,
1338 datecancelled => undef,
1339 daterequested => \'NOW()'
1344 $transfer = $new_item_1->get_transfer();
1345 is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the oldest transfer request');
1347 $transfer_2->datesent(\'NOW()')->store;
1348 $transfer = $new_item_1->get_transfer();
1349 is( $transfer->branchtransfer_id, $transfer_2->branchtransfer_id, 'Koha::Item->get_transfer returns the in_transit transfer');
1351 my $transfer_3 = $builder->build_object(
1353 class => 'Koha::Item::Transfers',
1355 itemnumber => $new_item_1->itemnumber,
1356 frombranch => $new_item_1->holdingbranch,
1357 tobranch => $library_to->{branchcode},
1360 datearrived => undef,
1361 datecancelled => undef,
1362 daterequested => \'NOW()'
1367 $transfer_2->datearrived(\'NOW()')->store;
1368 $transfer = $new_item_1->get_transfer();
1369 is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the next queued transfer');
1370 is( $transfer->itemnumber, $new_item_1->itemnumber, 'Koha::Item->get_transfer returns the right items transfer' );
1372 $transfer_1->datecancelled(\'NOW()')->store;
1373 $transfer = $new_item_1->get_transfer();
1374 is( $transfer->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfer ignores cancelled transfers');
1377 subtest 'holds' => sub {
1380 my $biblio = $builder->build_sample_biblio();
1381 my $item = $builder->build_sample_item({
1382 biblionumber => $biblio->biblionumber,
1384 is($item->holds->count, 0, "Nothing returned if no holds");
1385 my $hold1 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'T' }});
1386 my $hold2 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1387 my $hold3 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1389 is($item->holds()->count,3,"Three holds found");
1390 is($item->holds({found => 'W'})->count,2,"Two waiting holds found");
1391 is_deeply($item->holds({found => 'T'})->next->unblessed,$hold1,"Found transit holds matches the hold");
1392 is($item->holds({found => undef})->count, 0,"Nothing returned if no matching holds");
1395 subtest 'biblio' => sub {
1398 my $biblio = $retrieved_item_1->biblio;
1399 is( ref( $biblio ), 'Koha::Biblio', 'Koha::Item->biblio should return a Koha::Biblio' );
1400 is( $biblio->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblio should return the correct biblio' );
1403 subtest 'biblioitem' => sub {
1406 my $biblioitem = $retrieved_item_1->biblioitem;
1407 is( ref( $biblioitem ), 'Koha::Biblioitem', 'Koha::Item->biblioitem should return a Koha::Biblioitem' );
1408 is( $biblioitem->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblioitem should return the correct biblioitem' );
1412 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
1413 subtest 'checkout' => sub {
1415 my $item = Koha::Items->find( $new_item_1->itemnumber );
1417 my $checkout = $item->checkout;
1418 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no current checkout on this item' );
1421 my $patron = $builder->build({ source => 'Borrower' });
1422 C4::Circulation::AddIssue( $patron, $item->barcode );
1423 $checkout = $retrieved_item_1->checkout;
1424 is( ref( $checkout ), 'Koha::Checkout', 'Koha::Item->checkout should return a Koha::Checkout' );
1425 is( $checkout->itemnumber, $item->itemnumber, 'Koha::Item->checkout should return the correct checkout' );
1426 is( $checkout->borrowernumber, $patron->{borrowernumber}, 'Koha::Item->checkout should return the correct checkout' );
1429 C4::Circulation::AddReturn( $item->barcode );
1431 # There is no more checkout on this item, making sure it will not return old checkouts
1432 $checkout = $item->checkout;
1433 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no *current* checkout on this item' );
1436 subtest 'can_be_transferred' => sub {
1439 t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
1440 t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
1442 my $biblio = $builder->build_sample_biblio();
1443 my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
1444 my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
1445 my $item = $builder->build_sample_item({
1446 biblionumber => $biblio->biblionumber,
1447 homebranch => $library1->branchcode,
1448 holdingbranch => $library1->branchcode,
1451 is(Koha::Item::Transfer::Limits->search({
1452 fromBranch => $library1->branchcode,
1453 toBranch => $library2->branchcode,
1454 })->count, 0, 'There are no transfer limits between libraries.');
1455 ok($item->can_be_transferred({ to => $library2 }),
1456 'Item can be transferred between libraries.');
1458 my $limit = Koha::Item::Transfer::Limit->new({
1459 fromBranch => $library1->branchcode,
1460 toBranch => $library2->branchcode,
1461 itemtype => $item->effective_itemtype,
1463 is(Koha::Item::Transfer::Limits->search({
1464 fromBranch => $library1->branchcode,
1465 toBranch => $library2->branchcode,
1466 })->count, 1, 'Given we have added a transfer limit,');
1467 is($item->can_be_transferred({ to => $library2 }), 0,
1468 'Item can no longer be transferred between libraries.');
1469 is($item->can_be_transferred({ to => $library2, from => $library1 }), 0,
1470 'We get the same result also if we pass the from-library parameter.');
1473 subtest 'filter_by_for_loan' => sub {
1476 my $biblio = $builder->build_sample_biblio;
1477 is( $biblio->items->filter_by_for_loan->count, 0, 'no item yet' );
1478 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 1 } );
1479 is( $biblio->items->filter_by_for_loan->count, 0, 'no item for loan' );
1480 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 0 } );
1481 is( $biblio->items->filter_by_for_loan->count, 1, '1 item for loan' );
1486 # Reset nb_of_items prior to testing delete
1487 $nb_of_items = Koha::Items->search->count;
1490 $retrieved_item_1->delete;
1491 is( Koha::Items->search->count, $nb_of_items - 1, 'Delete should have deleted the item' );
1493 $schema->storage->txn_rollback;
1495 subtest 'filter_by_visible_in_opac() tests' => sub {
1499 $schema->storage->txn_begin;
1501 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1502 my $mocked_category = Test::MockModule->new('Koha::Patron::Category');
1504 $mocked_category->mock( 'override_hidden_items', sub {
1508 # have a fresh biblio
1509 my $biblio = $builder->build_sample_biblio;
1510 # have two itemtypes
1511 my $itype_1 = $builder->build_object({ class => 'Koha::ItemTypes' });
1512 my $itype_2 = $builder->build_object({ class => 'Koha::ItemTypes' });
1513 # have 5 items on that biblio
1514 my $item_1 = $builder->build_sample_item(
1516 biblionumber => $biblio->biblionumber,
1518 itype => $itype_1->itemtype,
1523 my $item_2 = $builder->build_sample_item(
1525 biblionumber => $biblio->biblionumber,
1527 itype => $itype_2->itemtype,
1532 my $item_3 = $builder->build_sample_item(
1534 biblionumber => $biblio->biblionumber,
1536 itype => $itype_1->itemtype,
1541 my $item_4 = $builder->build_sample_item(
1543 biblionumber => $biblio->biblionumber,
1545 itype => $itype_2->itemtype,
1550 my $item_5 = $builder->build_sample_item(
1552 biblionumber => $biblio->biblionumber,
1554 itype => $itype_1->itemtype,
1559 my $item_6 = $builder->build_sample_item(
1561 biblionumber => $biblio->biblionumber,
1563 itype => $itype_1->itemtype,
1571 my $mocked_context = Test::MockModule->new('C4::Context');
1572 $mocked_context->mock( 'yaml_preference', sub {
1576 t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
1577 is( $biblio->items->filter_by_visible_in_opac->count,
1578 6, 'No rules passed, hidelostitems unset' );
1580 is( $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1581 6, 'No rules passed, hidelostitems unset, patron exception changes nothing' );
1583 $rules = { copynumber => [ 2 ] };
1585 t::lib::Mocks::mock_preference( 'hidelostitems', 1 );
1587 $biblio->items->filter_by_visible_in_opac->count,
1589 'No rules passed, hidelostitems set'
1593 $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1595 'No rules passed, hidelostitems set, patron exception changes nothing'
1598 $rules = { withdrawn => [ 1, 2 ], copynumber => [ 2 ] };
1600 $biblio->items->filter_by_visible_in_opac->count,
1602 'Rules on withdrawn, hidelostitems set'
1606 $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1608 'hidelostitems set, rules on withdrawn but patron override passed'
1611 $rules = { itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1613 $biblio->items->filter_by_visible_in_opac->count,
1615 'Rules on itype, hidelostitems set'
1618 $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1620 $biblio->items->filter_by_visible_in_opac->count,
1622 'Rules on itype and withdrawn, hidelostitems set'
1625 $biblio->items->filter_by_visible_in_opac
1627 $item_4->itemnumber,
1628 'The right item is returned'
1631 $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_2->itemtype ], copynumber => [ 2 ] };
1633 $biblio->items->filter_by_visible_in_opac->count,
1635 'Rules on itype and withdrawn, hidelostitems set'
1638 $biblio->items->filter_by_visible_in_opac
1640 $item_5->itemnumber,
1641 'The right item is returned'
1644 # Make sure the warning on the about page will work
1645 $rules = { itemlost => ['AB'] };
1646 my $c = Koha::Items->filter_by_visible_in_opac->count;
1647 my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
1648 is( $warnings[2], q{Truncated incorrect DOUBLE value: 'AB'});
1650 $schema->storage->txn_rollback;
1653 subtest 'filter_out_lost() tests' => sub {
1657 $schema->storage->txn_begin;
1659 # have a fresh biblio
1660 my $biblio = $builder->build_sample_biblio;
1661 # have 3 items on that biblio
1662 my $item_1 = $builder->build_sample_item(
1664 biblionumber => $biblio->biblionumber,
1668 my $item_2 = $builder->build_sample_item(
1670 biblionumber => $biblio->biblionumber,
1674 my $item_3 = $builder->build_sample_item(
1676 biblionumber => $biblio->biblionumber,
1681 is( $biblio->items->filter_out_lost->next->itemnumber, $item_2->itemnumber, 'Right item returned' );
1682 is( $biblio->items->filter_out_lost->count, 1, 'Only one item is not lost' );
1684 $schema->storage->txn_rollback;