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'
92 is( $item->permanent_location, $item->location,
93 'permanent_location must have been set to location if not given' );
96 subtest '*_on updates' => sub {
99 # Once the '_on' value is set (triggered by the related field turning from false to true)
100 # it should not be re-set for any changes outside of the related field being 'unset'.
102 my @fields = qw( itemlost withdrawn damaged );
103 my $today = dt_from_string();
104 my $yesterday = $today->clone()->subtract( days => 1 );
106 for my $field ( @fields ) {
107 my $item = $builder->build_sample_item(
110 itemlost_on => undef,
112 withdrawn_on => undef,
117 my $field_on = $field . '_on';
119 # Set field for the first time
120 Time::Fake->offset( $yesterday->epoch );
121 $item->$field(1)->store;
122 $item->get_from_storage;
123 is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
124 0, $field_on . " was set upon first truthy setting" );
126 # Update the field to a new 'true' value
127 Time::Fake->offset( $today->epoch );
128 $item->$field(2)->store;
129 $item->get_from_storage;
130 is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
131 0, $field_on . " was not updated upon second truthy setting" );
133 # Update the field to a new 'false' value
134 $item->$field(0)->store;
135 $item->get_from_storage;
136 is($item->$field_on, undef, $field_on . " was unset upon untruthy setting");
142 subtest '_lost_found_trigger' => sub {
145 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
146 t::lib::Mocks::mock_preference( 'WhenLostForgiveFine', 0 );
148 my $processfee_amount = 20;
149 my $replacement_amount = 99.00;
150 my $item_type = $builder->build_object(
152 class => 'Koha::ItemTypes',
156 defaultreplacecost => undef,
157 processfee => $processfee_amount,
158 rentalcharge_daily => 0,
162 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
164 $biblio = $builder->build_sample_biblio( { author => 'Hall, Daria' } );
166 subtest 'Full write-off tests' => sub {
170 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
172 $builder->build_object( { class => "Koha::Patrons" } );
173 t::lib::Mocks::mock_userenv(
174 { patron => $manager, branchcode => $manager->branchcode } );
176 my $item = $builder->build_sample_item(
178 biblionumber => $biblio->biblionumber,
179 library => $library->branchcode,
180 replacementprice => $replacement_amount,
181 itype => $item_type->itemtype,
185 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
187 # Simulate item marked as lost
188 $item->itemlost(3)->store;
189 C4::Circulation::LostItem( $item->itemnumber, 1 );
191 my $processing_fee_lines = Koha::Account::Lines->search(
193 borrowernumber => $patron->id,
194 itemnumber => $item->itemnumber,
195 debit_type_code => 'PROCESSING'
198 is( $processing_fee_lines->count,
199 1, 'Only one processing fee produced' );
200 my $processing_fee_line = $processing_fee_lines->next;
201 is( $processing_fee_line->amount + 0,
203 'The right PROCESSING amount is generated' );
204 is( $processing_fee_line->amountoutstanding + 0,
206 'The right PROCESSING amountoutstanding is generated' );
208 my $lost_fee_lines = Koha::Account::Lines->search(
210 borrowernumber => $patron->id,
211 itemnumber => $item->itemnumber,
212 debit_type_code => 'LOST'
215 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
216 my $lost_fee_line = $lost_fee_lines->next;
217 is( $lost_fee_line->amount + 0,
218 $replacement_amount, 'The right LOST amount is generated' );
219 is( $lost_fee_line->amountoutstanding + 0,
221 'The right LOST amountoutstanding is generated' );
222 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
224 my $account = $patron->account;
225 my $debts = $account->outstanding_debits;
228 my $credit = $account->add_credit(
230 amount => $account->balance,
236 { debits => [ $debts->as_list ], offset_type => 'Writeoff' } );
238 # Simulate item marked as found
239 $item->itemlost(0)->store;
240 is( $item->{_refunded}, undef, 'No LOST_FOUND account line added' );
242 $lost_fee_line->discard_changes; # reload from DB
243 is( $lost_fee_line->amountoutstanding + 0,
244 0, 'Lost fee has no outstanding amount' );
245 is( $lost_fee_line->debit_type_code,
246 'LOST', 'Lost fee now still has account type of LOST' );
247 is( $lost_fee_line->status, 'FOUND',
248 "Lost fee now has account status of FOUND - No Refund" );
250 is( $patron->account->balance,
251 -0, 'The patron balance is 0, everything was written off' );
254 subtest 'Full payment tests' => sub {
258 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
260 my $item = $builder->build_sample_item(
262 biblionumber => $biblio->biblionumber,
263 library => $library->branchcode,
264 replacementprice => $replacement_amount,
265 itype => $item_type->itemtype
270 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
272 # Simulate item marked as lost
273 $item->itemlost(1)->store;
274 C4::Circulation::LostItem( $item->itemnumber, 1 );
276 my $processing_fee_lines = Koha::Account::Lines->search(
278 borrowernumber => $patron->id,
279 itemnumber => $item->itemnumber,
280 debit_type_code => 'PROCESSING'
283 is( $processing_fee_lines->count,
284 1, 'Only one processing fee produced' );
285 my $processing_fee_line = $processing_fee_lines->next;
286 is( $processing_fee_line->amount + 0,
288 'The right PROCESSING amount is generated' );
289 is( $processing_fee_line->amountoutstanding + 0,
291 'The right PROCESSING amountoutstanding is generated' );
293 my $lost_fee_lines = Koha::Account::Lines->search(
295 borrowernumber => $patron->id,
296 itemnumber => $item->itemnumber,
297 debit_type_code => 'LOST'
300 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
301 my $lost_fee_line = $lost_fee_lines->next;
302 is( $lost_fee_line->amount + 0,
303 $replacement_amount, 'The right LOST amount is generated' );
304 is( $lost_fee_line->amountoutstanding + 0,
306 'The right LOST amountountstanding is generated' );
308 my $account = $patron->account;
309 my $debts = $account->outstanding_debits;
312 my $credit = $account->add_credit(
314 amount => $account->balance,
320 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
322 # Simulate item marked as found
323 $item->itemlost(0)->store;
324 is( $item->{_refunded}, 1, 'Refund triggered' );
326 my $credit_return = Koha::Account::Lines->search(
328 itemnumber => $item->itemnumber,
329 credit_type_code => 'LOST_FOUND'
334 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
335 is( $credit_return->amount + 0,
337 'The account line of type LOST_FOUND has an amount of -99' );
339 $credit_return->amountoutstanding + 0,
341 'The account line of type LOST_FOUND has an amountoutstanding of -99'
344 $lost_fee_line->discard_changes;
345 is( $lost_fee_line->amountoutstanding + 0,
346 0, 'Lost fee has no outstanding amount' );
347 is( $lost_fee_line->debit_type_code,
348 'LOST', 'Lost fee now still has account type of LOST' );
349 is( $lost_fee_line->status, 'FOUND',
350 "Lost fee now has account status of FOUND" );
352 is( $patron->account->balance, -99,
353 'The patron balance is -99, a credit that equals the lost fee payment'
357 subtest 'Test without payment or write off' => sub {
361 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
363 my $item = $builder->build_sample_item(
365 biblionumber => $biblio->biblionumber,
366 library => $library->branchcode,
367 replacementprice => 23.00,
368 replacementprice => $replacement_amount,
369 itype => $item_type->itemtype
374 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
376 # Simulate item marked as lost
377 $item->itemlost(3)->store;
378 C4::Circulation::LostItem( $item->itemnumber, 1 );
380 my $processing_fee_lines = Koha::Account::Lines->search(
382 borrowernumber => $patron->id,
383 itemnumber => $item->itemnumber,
384 debit_type_code => 'PROCESSING'
387 is( $processing_fee_lines->count,
388 1, 'Only one processing fee produced' );
389 my $processing_fee_line = $processing_fee_lines->next;
390 is( $processing_fee_line->amount + 0,
392 'The right PROCESSING amount is generated' );
393 is( $processing_fee_line->amountoutstanding + 0,
395 'The right PROCESSING amountoutstanding is generated' );
397 my $lost_fee_lines = Koha::Account::Lines->search(
399 borrowernumber => $patron->id,
400 itemnumber => $item->itemnumber,
401 debit_type_code => 'LOST'
404 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
405 my $lost_fee_line = $lost_fee_lines->next;
406 is( $lost_fee_line->amount + 0,
407 $replacement_amount, 'The right LOST amount is generated' );
408 is( $lost_fee_line->amountoutstanding + 0,
410 'The right LOST amountountstanding is generated' );
412 # Simulate item marked as found
413 $item->itemlost(0)->store;
414 is( $item->{_refunded}, 1, 'Refund triggered' );
416 my $credit_return = Koha::Account::Lines->search(
418 itemnumber => $item->itemnumber,
419 credit_type_code => 'LOST_FOUND'
424 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
425 is( $credit_return->amount + 0,
427 'The account line of type LOST_FOUND has an amount of -99' );
429 $credit_return->amountoutstanding + 0,
431 'The account line of type LOST_FOUND has an amountoutstanding of 0'
434 $lost_fee_line->discard_changes;
435 is( $lost_fee_line->amountoutstanding + 0,
436 0, 'Lost fee has no outstanding amount' );
437 is( $lost_fee_line->debit_type_code,
438 'LOST', 'Lost fee now still has account type of LOST' );
439 is( $lost_fee_line->status, 'FOUND',
440 "Lost fee now has account status of FOUND" );
442 is( $patron->account->balance,
443 20, 'The patron balance is 20, still owes the processing fee' );
447 'Test with partial payement and write off, and remaining debt' =>
452 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
453 my $item = $builder->build_sample_item(
455 biblionumber => $biblio->biblionumber,
456 library => $library->branchcode,
457 replacementprice => $replacement_amount,
458 itype => $item_type->itemtype
463 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
465 # Simulate item marked as lost
466 $item->itemlost(1)->store;
467 C4::Circulation::LostItem( $item->itemnumber, 1 );
469 my $processing_fee_lines = Koha::Account::Lines->search(
471 borrowernumber => $patron->id,
472 itemnumber => $item->itemnumber,
473 debit_type_code => 'PROCESSING'
476 is( $processing_fee_lines->count,
477 1, 'Only one processing fee produced' );
478 my $processing_fee_line = $processing_fee_lines->next;
479 is( $processing_fee_line->amount + 0,
481 'The right PROCESSING amount is generated' );
482 is( $processing_fee_line->amountoutstanding + 0,
484 'The right PROCESSING amountoutstanding is generated' );
486 my $lost_fee_lines = Koha::Account::Lines->search(
488 borrowernumber => $patron->id,
489 itemnumber => $item->itemnumber,
490 debit_type_code => 'LOST'
493 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
494 my $lost_fee_line = $lost_fee_lines->next;
495 is( $lost_fee_line->amount + 0,
496 $replacement_amount, 'The right LOST amount is generated' );
497 is( $lost_fee_line->amountoutstanding + 0,
499 'The right LOST amountountstanding is generated' );
501 my $account = $patron->account;
504 $processfee_amount + $replacement_amount,
505 'Balance is PROCESSING + L'
509 my $payment_amount = 27;
510 my $payment = $account->add_credit(
512 amount => $payment_amount,
519 { debits => [$lost_fee_line], offset_type => 'Payment' } );
521 # Partially write off fee
522 my $write_off_amount = 25;
523 my $write_off = $account->add_credit(
525 amount => $write_off_amount,
531 { debits => [$lost_fee_line], offset_type => 'Writeoff' } );
536 $replacement_amount -
539 'Payment and write off applied'
542 # Store the amountoutstanding value
543 $lost_fee_line->discard_changes;
544 my $outstanding = $lost_fee_line->amountoutstanding;
546 # Simulate item marked as found
547 $item->itemlost(0)->store;
548 is( $item->{_refunded}, 1, 'Refund triggered' );
550 my $credit_return = Koha::Account::Lines->search(
552 itemnumber => $item->itemnumber,
553 credit_type_code => 'LOST_FOUND'
558 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
562 $processfee_amount - $payment_amount,
563 'Balance is PROCESSING - PAYMENT (LOST_FOUND)'
566 $lost_fee_line->discard_changes;
567 is( $lost_fee_line->amountoutstanding + 0,
568 0, 'Lost fee has no outstanding amount' );
569 is( $lost_fee_line->debit_type_code,
570 'LOST', 'Lost fee now still has account type of LOST' );
571 is( $lost_fee_line->status, 'FOUND',
572 "Lost fee now has account status of FOUND" );
575 $credit_return->amount + 0,
576 ( $payment_amount + $outstanding ) * -1,
577 'The account line of type LOST_FOUND has an amount equal to the payment + outstanding'
580 $credit_return->amountoutstanding + 0,
581 $payment_amount * -1,
582 'The account line of type LOST_FOUND has an amountoutstanding equal to the payment'
587 $processfee_amount - $payment_amount,
588 'The patron balance is the difference between the PROCESSING and the credit'
592 subtest 'Partial payment, existing debits and AccountAutoReconcile' =>
597 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
598 my $barcode = 'KD123456793';
599 my $replacement_amount = 100;
600 my $processfee_amount = 20;
602 my $item_type = $builder->build_object(
604 class => 'Koha::ItemTypes',
608 defaultreplacecost => undef,
610 rentalcharge_daily => 0,
614 my $item = Koha::Item->new(
616 biblionumber => $biblio->biblionumber,
617 homebranch => $library->branchcode,
618 holdingbranch => $library->branchcode,
620 replacementprice => $replacement_amount,
621 itype => $item_type->itemtype
626 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
628 # Simulate item marked as lost
629 $item->itemlost(1)->store;
630 C4::Circulation::LostItem( $item->itemnumber, 1 );
632 my $lost_fee_lines = Koha::Account::Lines->search(
634 borrowernumber => $patron->id,
635 itemnumber => $item->itemnumber,
636 debit_type_code => 'LOST'
639 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
640 my $lost_fee_line = $lost_fee_lines->next;
641 is( $lost_fee_line->amount + 0,
642 $replacement_amount, 'The right LOST amount is generated' );
643 is( $lost_fee_line->amountoutstanding + 0,
645 'The right LOST amountountstanding is generated' );
647 my $account = $patron->account;
648 is( $account->balance, $replacement_amount, 'Balance is L' );
651 my $payment_amount = 27;
652 my $payment = $account->add_credit(
654 amount => $payment_amount,
660 { debits => [$lost_fee_line], offset_type => 'Payment' } );
664 $replacement_amount - $payment_amount,
668 my $manual_debit_amount = 80;
671 amount => $manual_debit_amount,
679 $manual_debit_amount + $replacement_amount - $payment_amount,
680 'Manual debit applied'
683 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
685 # Simulate item marked as found
686 $item->itemlost(0)->store;
687 is( $item->{_refunded}, 1, 'Refund triggered' );
689 my $credit_return = Koha::Account::Lines->search(
691 itemnumber => $item->itemnumber,
692 credit_type_code => 'LOST_FOUND'
697 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
701 $manual_debit_amount - $payment_amount,
702 'Balance is PROCESSING - payment (LOST_FOUND)'
705 my $manual_debit = Koha::Account::Lines->search(
707 borrowernumber => $patron->id,
708 debit_type_code => 'OVERDUE',
709 status => 'UNRETURNED'
713 $manual_debit->amountoutstanding + 0,
714 $manual_debit_amount - $payment_amount,
715 'reconcile_balance was called'
719 subtest 'Patron deleted' => sub {
722 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
723 my $barcode = 'KD123456794';
724 my $replacement_amount = 100;
725 my $processfee_amount = 20;
727 my $item_type = $builder->build_object(
729 class => 'Koha::ItemTypes',
733 defaultreplacecost => undef,
735 rentalcharge_daily => 0,
739 my $item = Koha::Item->new(
741 biblionumber => $biblio->biblionumber,
742 homebranch => $library->branchcode,
743 holdingbranch => $library->branchcode,
745 replacementprice => $replacement_amount,
746 itype => $item_type->itemtype
751 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
753 # Simulate item marked as lost
754 $item->itemlost(1)->store;
755 C4::Circulation::LostItem( $item->itemnumber, 1 );
760 # Simulate item marked as found
761 $item->itemlost(0)->store;
762 is( $item->{_refunded}, undef, 'No refund triggered' );
766 subtest 'restore fine | no overdue' => sub {
771 $builder->build_object( { class => "Koha::Patrons" } );
772 t::lib::Mocks::mock_userenv(
773 { patron => $manager, branchcode => $manager->branchcode } );
775 # Set lostreturn_policy to 'restore' for tests
776 my $specific_rule_restore = $builder->build(
778 source => 'CirculationRule',
780 branchcode => $manager->branchcode,
781 categorycode => undef,
783 rule_name => 'lostreturn',
784 rule_value => 'restore'
789 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
791 my $item = $builder->build_sample_item(
793 biblionumber => $biblio->biblionumber,
794 library => $library->branchcode,
795 replacementprice => $replacement_amount,
796 itype => $item_type->itemtype
801 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
803 # Simulate item marked as lost
804 $item->itemlost(1)->store;
805 C4::Circulation::LostItem( $item->itemnumber, 1 );
807 my $processing_fee_lines = Koha::Account::Lines->search(
809 borrowernumber => $patron->id,
810 itemnumber => $item->itemnumber,
811 debit_type_code => 'PROCESSING'
814 is( $processing_fee_lines->count,
815 1, 'Only one processing fee produced' );
816 my $processing_fee_line = $processing_fee_lines->next;
817 is( $processing_fee_line->amount + 0,
819 'The right PROCESSING amount is generated' );
820 is( $processing_fee_line->amountoutstanding + 0,
822 'The right PROCESSING amountoutstanding is generated' );
824 my $lost_fee_lines = Koha::Account::Lines->search(
826 borrowernumber => $patron->id,
827 itemnumber => $item->itemnumber,
828 debit_type_code => 'LOST'
831 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
832 my $lost_fee_line = $lost_fee_lines->next;
833 is( $lost_fee_line->amount + 0,
834 $replacement_amount, 'The right LOST amount is generated' );
835 is( $lost_fee_line->amountoutstanding + 0,
837 'The right LOST amountountstanding is generated' );
839 my $account = $patron->account;
840 my $debts = $account->outstanding_debits;
843 my $credit = $account->add_credit(
845 amount => $account->balance,
851 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
853 # Simulate item marked as found
854 $item->itemlost(0)->store;
855 is( $item->{_refunded}, 1, 'Refund triggered' );
856 is( $item->{_restored}, undef, 'Restore not triggered when there is no overdue fine found' );
859 subtest 'restore fine | unforgiven overdue' => sub {
863 # Set lostreturn_policy to 'restore' for tests
865 $builder->build_object( { class => "Koha::Patrons" } );
866 t::lib::Mocks::mock_userenv(
867 { patron => $manager, branchcode => $manager->branchcode } );
868 my $specific_rule_restore = $builder->build(
870 source => 'CirculationRule',
872 branchcode => $manager->branchcode,
873 categorycode => undef,
875 rule_name => 'lostreturn',
876 rule_value => 'restore'
881 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
883 my $item = $builder->build_sample_item(
885 biblionumber => $biblio->biblionumber,
886 library => $library->branchcode,
887 replacementprice => $replacement_amount,
888 itype => $item_type->itemtype
893 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
895 # Simulate item marked as lost
896 $item->itemlost(1)->store;
897 C4::Circulation::LostItem( $item->itemnumber, 1 );
899 my $processing_fee_lines = Koha::Account::Lines->search(
901 borrowernumber => $patron->id,
902 itemnumber => $item->itemnumber,
903 debit_type_code => 'PROCESSING'
906 is( $processing_fee_lines->count,
907 1, 'Only one processing fee produced' );
908 my $processing_fee_line = $processing_fee_lines->next;
909 is( $processing_fee_line->amount + 0,
911 'The right PROCESSING amount is generated' );
912 is( $processing_fee_line->amountoutstanding + 0,
914 'The right PROCESSING amountoutstanding is generated' );
916 my $lost_fee_lines = Koha::Account::Lines->search(
918 borrowernumber => $patron->id,
919 itemnumber => $item->itemnumber,
920 debit_type_code => 'LOST'
923 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
924 my $lost_fee_line = $lost_fee_lines->next;
925 is( $lost_fee_line->amount + 0,
926 $replacement_amount, 'The right LOST amount is generated' );
927 is( $lost_fee_line->amountoutstanding + 0,
929 'The right LOST amountountstanding is generated' );
931 my $account = $patron->account;
932 my $debts = $account->outstanding_debits;
935 my $credit = $account->add_credit(
937 amount => $account->balance,
943 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
946 my $overdue = $account->add_debit(
949 user_id => $manager->borrowernumber,
950 library_id => $library->branchcode,
952 item_id => $item->itemnumber,
956 $overdue->status('LOST')->store();
957 $overdue->discard_changes;
958 is( $overdue->status, 'LOST',
959 'Overdue status set to LOST' );
961 # Simulate item marked as found
962 $item->itemlost(0)->store;
963 is( $item->{_refunded}, 1, 'Refund triggered' );
964 is( $item->{_restored}, undef, 'Restore not triggered when overdue was not forgiven' );
965 $overdue->discard_changes;
966 is( $overdue->status, 'FOUND',
967 'Overdue status updated to FOUND' );
970 subtest 'restore fine | forgiven overdue' => sub {
974 # Set lostreturn_policy to 'restore' for tests
976 $builder->build_object( { class => "Koha::Patrons" } );
977 t::lib::Mocks::mock_userenv(
978 { patron => $manager, branchcode => $manager->branchcode } );
979 my $specific_rule_restore = $builder->build(
981 source => 'CirculationRule',
983 branchcode => $manager->branchcode,
984 categorycode => undef,
986 rule_name => 'lostreturn',
987 rule_value => 'restore'
992 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
994 my $item = $builder->build_sample_item(
996 biblionumber => $biblio->biblionumber,
997 library => $library->branchcode,
998 replacementprice => $replacement_amount,
999 itype => $item_type->itemtype
1004 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
1006 # Simulate item marked as lost
1007 $item->itemlost(1)->store;
1008 C4::Circulation::LostItem( $item->itemnumber, 1 );
1010 my $processing_fee_lines = Koha::Account::Lines->search(
1012 borrowernumber => $patron->id,
1013 itemnumber => $item->itemnumber,
1014 debit_type_code => 'PROCESSING'
1017 is( $processing_fee_lines->count,
1018 1, 'Only one processing fee produced' );
1019 my $processing_fee_line = $processing_fee_lines->next;
1020 is( $processing_fee_line->amount + 0,
1022 'The right PROCESSING amount is generated' );
1023 is( $processing_fee_line->amountoutstanding + 0,
1025 'The right PROCESSING amountoutstanding is generated' );
1027 my $lost_fee_lines = Koha::Account::Lines->search(
1029 borrowernumber => $patron->id,
1030 itemnumber => $item->itemnumber,
1031 debit_type_code => 'LOST'
1034 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1035 my $lost_fee_line = $lost_fee_lines->next;
1036 is( $lost_fee_line->amount + 0,
1037 $replacement_amount, 'The right LOST amount is generated' );
1038 is( $lost_fee_line->amountoutstanding + 0,
1039 $replacement_amount,
1040 'The right LOST amountountstanding is generated' );
1042 my $account = $patron->account;
1043 my $debts = $account->outstanding_debits;
1046 my $credit = $account->add_credit(
1048 amount => $account->balance,
1050 interface => 'test',
1054 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
1057 my $overdue = $account->add_debit(
1060 user_id => $manager->borrowernumber,
1061 library_id => $library->branchcode,
1062 interface => 'test',
1063 item_id => $item->itemnumber,
1067 $overdue->status('LOST')->store();
1068 is( $overdue->status, 'LOST',
1069 'Overdue status set to LOST' );
1071 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 0 );
1074 $credit = $account->add_credit(
1077 user_id => $manager->borrowernumber,
1078 library_id => $library->branchcode,
1079 interface => 'test',
1081 item_id => $item->itemnumber
1085 { debits => [$overdue], offset_type => 'Forgiven' } );
1087 # Simulate item marked as found
1088 $item->itemlost(0)->store;
1089 is( $item->{_refunded}, 1, 'Refund triggered' );
1090 is( $item->{_restored}, 1, 'Restore triggered when overdue was forgiven' );
1091 $overdue->discard_changes;
1092 is( $overdue->status, 'FOUND', 'Overdue status updated to FOUND' );
1093 is( $overdue->amountoutstanding, $overdue->amount, 'Overdue outstanding has been restored' );
1094 $credit->discard_changes;
1095 is( $credit->status, 'VOID', 'Overdue Forgival has been marked as VOID');
1098 subtest 'Continue when userenv is not set' => sub {
1101 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1102 my $barcode = 'KD123456795';
1103 my $replacement_amount = 100;
1104 my $processfee_amount = 20;
1106 my $item_type = $builder->build_object(
1108 class => 'Koha::ItemTypes',
1110 notforloan => undef,
1112 defaultreplacecost => undef,
1114 rentalcharge_daily => 0,
1118 my $item = $builder->build_sample_item(
1120 biblionumber => $biblio->biblionumber,
1121 homebranch => $library->branchcode,
1122 holdingbranch => $library->branchcode,
1123 barcode => $barcode,
1124 replacementprice => $replacement_amount,
1125 itype => $item_type->itemtype
1130 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
1132 # Simulate item marked as lost
1133 $item->itemlost(1)->store;
1134 C4::Circulation::LostItem( $item->itemnumber, 1 );
1137 C4::Context->_new_userenv(undef);
1139 # Simluate item marked as found
1140 $item->itemlost(0)->store;
1141 is( $item->{_refunded}, 1, 'No refund triggered' );
1146 subtest 'log_action' => sub {
1148 t::lib::Mocks::mock_preference( 'CataloguingLog', 1 );
1150 my $item = Koha::Item->new(
1152 homebranch => $library->{branchcode},
1153 holdingbranch => $library->{branchcode},
1154 biblionumber => $biblio->biblionumber,
1155 location => 'my_loc',
1159 Koha::ActionLogs->search(
1161 module => 'CATALOGUING',
1163 object => $item->itemnumber,
1168 "Item creation logged"
1171 $item->location('another_loc')->store;
1173 Koha::ActionLogs->search(
1175 module => 'CATALOGUING',
1177 object => $item->itemnumber
1181 "Item modification logged"
1186 subtest 'get_transfer' => sub {
1189 my $transfer = $new_item_1->get_transfer();
1190 is( $transfer, undef, 'Koha::Item->get_transfer should return undef if the item is not in transit' );
1192 my $library_to = $builder->build( { source => 'Branch' } );
1194 C4::Circulation::transferbook({
1195 from_branch => $new_item_1->holdingbranch,
1196 to_branch => $library_to->{branchcode},
1197 barcode => $new_item_1->barcode,
1200 $transfer = $new_item_1->get_transfer();
1201 is( ref($transfer), 'Koha::Item::Transfer', 'Koha::Item->get_transfer should return a Koha::Item::Transfers object' );
1203 is( $transfer->itemnumber, $new_item_1->itemnumber, 'Koha::Item->get_transfer should return a valid Koha::Item::Transfers object' );
1206 subtest 'holds' => sub {
1209 my $biblio = $builder->build_sample_biblio();
1210 my $item = $builder->build_sample_item({
1211 biblionumber => $biblio->biblionumber,
1213 is($item->holds->count, 0, "Nothing returned if no holds");
1214 my $hold1 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'T' }});
1215 my $hold2 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1216 my $hold3 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1218 is($item->holds()->count,3,"Three holds found");
1219 is($item->holds({found => 'W'})->count,2,"Two waiting holds found");
1220 is_deeply($item->holds({found => 'T'})->next->unblessed,$hold1,"Found transit holds matches the hold");
1221 is($item->holds({found => undef})->count, 0,"Nothing returned if no matching holds");
1224 subtest 'biblio' => sub {
1227 my $biblio = $retrieved_item_1->biblio;
1228 is( ref( $biblio ), 'Koha::Biblio', 'Koha::Item->biblio should return a Koha::Biblio' );
1229 is( $biblio->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblio should return the correct biblio' );
1232 subtest 'biblioitem' => sub {
1235 my $biblioitem = $retrieved_item_1->biblioitem;
1236 is( ref( $biblioitem ), 'Koha::Biblioitem', 'Koha::Item->biblioitem should return a Koha::Biblioitem' );
1237 is( $biblioitem->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblioitem should return the correct biblioitem' );
1241 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
1242 subtest 'checkout' => sub {
1244 my $item = Koha::Items->find( $new_item_1->itemnumber );
1246 my $checkout = $item->checkout;
1247 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no current checkout on this item' );
1250 my $patron = $builder->build({ source => 'Borrower' });
1251 C4::Circulation::AddIssue( $patron, $item->barcode );
1252 $checkout = $retrieved_item_1->checkout;
1253 is( ref( $checkout ), 'Koha::Checkout', 'Koha::Item->checkout should return a Koha::Checkout' );
1254 is( $checkout->itemnumber, $item->itemnumber, 'Koha::Item->checkout should return the correct checkout' );
1255 is( $checkout->borrowernumber, $patron->{borrowernumber}, 'Koha::Item->checkout should return the correct checkout' );
1258 C4::Circulation::AddReturn( $item->barcode );
1260 # There is no more checkout on this item, making sure it will not return old checkouts
1261 $checkout = $item->checkout;
1262 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no *current* checkout on this item' );
1265 subtest 'can_be_transferred' => sub {
1268 t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
1269 t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
1271 my $biblio = $builder->build_sample_biblio();
1272 my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
1273 my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
1274 my $item = $builder->build_sample_item({
1275 biblionumber => $biblio->biblionumber,
1276 homebranch => $library1->branchcode,
1277 holdingbranch => $library1->branchcode,
1280 is(Koha::Item::Transfer::Limits->search({
1281 fromBranch => $library1->branchcode,
1282 toBranch => $library2->branchcode,
1283 })->count, 0, 'There are no transfer limits between libraries.');
1284 ok($item->can_be_transferred({ to => $library2 }),
1285 'Item can be transferred between libraries.');
1287 my $limit = Koha::Item::Transfer::Limit->new({
1288 fromBranch => $library1->branchcode,
1289 toBranch => $library2->branchcode,
1290 itemtype => $item->effective_itemtype,
1292 is(Koha::Item::Transfer::Limits->search({
1293 fromBranch => $library1->branchcode,
1294 toBranch => $library2->branchcode,
1295 })->count, 1, 'Given we have added a transfer limit,');
1296 is($item->can_be_transferred({ to => $library2 }), 0,
1297 'Item can no longer be transferred between libraries.');
1298 is($item->can_be_transferred({ to => $library2, from => $library1 }), 0,
1299 'We get the same result also if we pass the from-library parameter.');
1302 subtest 'filter_by_for_loan' => sub {
1305 my $biblio = $builder->build_sample_biblio;
1306 is( $biblio->items->filter_by_for_loan->count, 0, 'no item yet' );
1307 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 1 } );
1308 is( $biblio->items->filter_by_for_loan->count, 0, 'no item for loan' );
1309 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 0 } );
1310 is( $biblio->items->filter_by_for_loan->count, 1, '1 item for loan' );
1315 # Reset nb_of_items prior to testing delete
1316 $nb_of_items = Koha::Items->search->count;
1319 $retrieved_item_1->delete;
1320 is( Koha::Items->search->count, $nb_of_items - 1, 'Delete should have deleted the item' );
1322 $schema->storage->txn_rollback;
1324 subtest 'filter_by_visible_in_opac() tests' => sub {
1328 $schema->storage->txn_begin;
1330 my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1331 my $mocked_category = Test::MockModule->new('Koha::Patron::Category');
1333 $mocked_category->mock( 'override_hidden_items', sub {
1337 # have a fresh biblio
1338 my $biblio = $builder->build_sample_biblio;
1339 # have two itemtypes
1340 my $itype_1 = $builder->build_object({ class => 'Koha::ItemTypes' });
1341 my $itype_2 = $builder->build_object({ class => 'Koha::ItemTypes' });
1342 # have 5 items on that biblio
1343 my $item_1 = $builder->build_sample_item(
1345 biblionumber => $biblio->biblionumber,
1347 itype => $itype_1->itemtype,
1352 my $item_2 = $builder->build_sample_item(
1354 biblionumber => $biblio->biblionumber,
1356 itype => $itype_2->itemtype,
1361 my $item_3 = $builder->build_sample_item(
1363 biblionumber => $biblio->biblionumber,
1365 itype => $itype_1->itemtype,
1370 my $item_4 = $builder->build_sample_item(
1372 biblionumber => $biblio->biblionumber,
1374 itype => $itype_2->itemtype,
1379 my $item_5 = $builder->build_sample_item(
1381 biblionumber => $biblio->biblionumber,
1383 itype => $itype_1->itemtype,
1388 my $item_6 = $builder->build_sample_item(
1390 biblionumber => $biblio->biblionumber,
1392 itype => $itype_1->itemtype,
1400 my $mocked_context = Test::MockModule->new('C4::Context');
1401 $mocked_context->mock( 'yaml_preference', sub {
1405 t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
1406 is( $biblio->items->filter_by_visible_in_opac->count,
1407 6, 'No rules passed, hidelostitems unset' );
1409 is( $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1410 6, 'No rules passed, hidelostitems unset, patron exception changes nothing' );
1412 $rules = { copynumber => [ 2 ] };
1414 t::lib::Mocks::mock_preference( 'hidelostitems', 1 );
1416 $biblio->items->filter_by_visible_in_opac->count,
1418 'No rules passed, hidelostitems set'
1422 $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1424 'No rules passed, hidelostitems set, patron exception changes nothing'
1427 $rules = { withdrawn => [ 1, 2 ], copynumber => [ 2 ] };
1429 $biblio->items->filter_by_visible_in_opac->count,
1431 'Rules on withdrawn, hidelostitems set'
1435 $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1437 'hidelostitems set, rules on withdrawn but patron override passed'
1440 $rules = { itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1442 $biblio->items->filter_by_visible_in_opac->count,
1444 'Rules on itype, hidelostitems set'
1447 $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1449 $biblio->items->filter_by_visible_in_opac->count,
1451 'Rules on itype and withdrawn, hidelostitems set'
1454 $biblio->items->filter_by_visible_in_opac
1456 $item_4->itemnumber,
1457 'The right item is returned'
1460 $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_2->itemtype ], copynumber => [ 2 ] };
1462 $biblio->items->filter_by_visible_in_opac->count,
1464 'Rules on itype and withdrawn, hidelostitems set'
1467 $biblio->items->filter_by_visible_in_opac
1469 $item_5->itemnumber,
1470 'The right item is returned'
1473 $schema->storage->txn_rollback;
1476 subtest 'filter_out_lost() tests' => sub {
1480 $schema->storage->txn_begin;
1482 # have a fresh biblio
1483 my $biblio = $builder->build_sample_biblio;
1484 # have 3 items on that biblio
1485 my $item_1 = $builder->build_sample_item(
1487 biblionumber => $biblio->biblionumber,
1491 my $item_2 = $builder->build_sample_item(
1493 biblionumber => $biblio->biblionumber,
1497 my $item_3 = $builder->build_sample_item(
1499 biblionumber => $biblio->biblionumber,
1504 is( $biblio->items->filter_out_lost->next->itemnumber, $item_2->itemnumber, 'Right item returned' );
1505 is( $biblio->items->filter_out_lost->count, 1, 'Only one item is not lost' );
1507 $schema->storage->txn_rollback;