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 => 12;
29 use Koha::Item::Transfer::Limits;
32 use Koha::DateUtils qw( dt_from_string );
34 use t::lib::TestBuilder;
38 my $schema = Koha::Database->new->schema;
39 $schema->storage->txn_begin;
41 my $dbh = C4::Context->dbh;
43 my $builder = t::lib::TestBuilder->new;
44 my $library = $builder->build( { source => 'Branch' } );
45 my $nb_of_items = Koha::Items->search->count;
46 my $biblio = $builder->build_sample_biblio();
47 my $new_item_1 = $builder->build_sample_item({
48 biblionumber => $biblio->biblionumber,
49 homebranch => $library->{branchcode},
50 holdingbranch => $library->{branchcode},
52 my $new_item_2 = $builder->build_sample_item({
53 biblionumber => $biblio->biblionumber,
54 homebranch => $library->{branchcode},
55 holdingbranch => $library->{branchcode},
59 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
61 like( $new_item_1->itemnumber, qr|^\d+$|, 'Adding a new item should have set the itemnumber' );
62 is( Koha::Items->search->count, $nb_of_items + 2, 'The 2 items should have been added' );
64 my $retrieved_item_1 = Koha::Items->find( $new_item_1->itemnumber );
65 is( $retrieved_item_1->barcode, $new_item_1->barcode, 'Find a item by id should return the correct item' );
67 subtest 'store' => sub {
70 my $biblio = $builder->build_sample_biblio;
71 my $today = dt_from_string->set( hour => 0, minute => 0, second => 0 );
72 my $item = Koha::Item->new(
74 homebranch => $library->{branchcode},
75 holdingbranch => $library->{branchcode},
76 biblionumber => $biblio->biblionumber,
79 )->store->get_from_storage;
81 is( t::lib::Dates::compare( $item->replacementpricedate, $today ),
82 0, 'replacementpricedate must have been set to today if not given' );
83 is( t::lib::Dates::compare( $item->datelastseen, $today ),
84 0, 'datelastseen must have been set to today if not given' );
87 $biblio->biblioitem->itemtype,
88 'items.itype must have been set to biblioitem.itemtype is not given'
90 is( $item->permanent_location, $item->location,
91 'permanent_location must have been set to location if not given' );
94 subtest '*_on updates' => sub {
97 # Once the '_on' value is set (triggered by the related field turning from false to true)
98 # it should not be re-set for any changes outside of the related field being 'unset'.
100 my @fields = qw( itemlost withdrawn damaged );
101 my $today = dt_from_string();
102 my $yesterday = $today->clone()->subtract( days => 1 );
104 for my $field ( @fields ) {
105 my $item = $builder->build_sample_item(
108 itemlost_on => undef,
110 withdrawn_on => undef,
115 my $field_on = $field . '_on';
117 # Set field for the first time
118 Time::Fake->offset( $yesterday->epoch );
119 $item->$field(1)->store;
120 $item->get_from_storage;
121 is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
122 0, $field_on . " was set upon first truthy setting" );
124 # Update the field to a new 'true' value
125 Time::Fake->offset( $today->epoch );
126 $item->$field(2)->store;
127 $item->get_from_storage;
128 is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
129 0, $field_on . " was not updated upon second truthy setting" );
131 # Update the field to a new 'false' value
132 $item->$field(0)->store;
133 $item->get_from_storage;
134 is($item->$field_on, undef, $field_on . " was unset upon untruthy setting");
140 subtest '_lost_found_trigger' => sub {
143 t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
144 t::lib::Mocks::mock_preference( 'WhenLostForgiveFine', 0 );
146 my $processfee_amount = 20;
147 my $replacement_amount = 99.00;
148 my $item_type = $builder->build_object(
150 class => 'Koha::ItemTypes',
154 defaultreplacecost => undef,
155 processfee => $processfee_amount,
156 rentalcharge_daily => 0,
160 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
162 $biblio = $builder->build_sample_biblio( { author => 'Hall, Daria' } );
164 subtest 'Full write-off tests' => sub {
168 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
170 $builder->build_object( { class => "Koha::Patrons" } );
171 t::lib::Mocks::mock_userenv(
172 { patron => $manager, branchcode => $manager->branchcode } );
174 my $item = $builder->build_sample_item(
176 biblionumber => $biblio->biblionumber,
177 library => $library->branchcode,
178 replacementprice => $replacement_amount,
179 itype => $item_type->itemtype,
183 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
185 # Simulate item marked as lost
186 $item->itemlost(3)->store;
187 C4::Circulation::LostItem( $item->itemnumber, 1 );
189 my $processing_fee_lines = Koha::Account::Lines->search(
191 borrowernumber => $patron->id,
192 itemnumber => $item->itemnumber,
193 debit_type_code => 'PROCESSING'
196 is( $processing_fee_lines->count,
197 1, 'Only one processing fee produced' );
198 my $processing_fee_line = $processing_fee_lines->next;
199 is( $processing_fee_line->amount + 0,
201 'The right PROCESSING amount is generated' );
202 is( $processing_fee_line->amountoutstanding + 0,
204 'The right PROCESSING amountoutstanding is generated' );
206 my $lost_fee_lines = Koha::Account::Lines->search(
208 borrowernumber => $patron->id,
209 itemnumber => $item->itemnumber,
210 debit_type_code => 'LOST'
213 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
214 my $lost_fee_line = $lost_fee_lines->next;
215 is( $lost_fee_line->amount + 0,
216 $replacement_amount, 'The right LOST amount is generated' );
217 is( $lost_fee_line->amountoutstanding + 0,
219 'The right LOST amountoutstanding is generated' );
220 is( $lost_fee_line->status, undef, 'The LOST status was not set' );
222 my $account = $patron->account;
223 my $debts = $account->outstanding_debits;
226 my $credit = $account->add_credit(
228 amount => $account->balance,
234 { debits => [ $debts->as_list ], offset_type => 'Writeoff' } );
236 # Simulate item marked as found
237 $item->itemlost(0)->store;
238 is( $item->{_refunded}, undef, 'No LOST_FOUND account line added' );
240 $lost_fee_line->discard_changes; # reload from DB
241 is( $lost_fee_line->amountoutstanding + 0,
242 0, 'Lost fee has no outstanding amount' );
243 is( $lost_fee_line->debit_type_code,
244 'LOST', 'Lost fee now still has account type of LOST' );
245 is( $lost_fee_line->status, 'FOUND',
246 "Lost fee now has account status of FOUND - No Refund" );
248 is( $patron->account->balance,
249 -0, 'The patron balance is 0, everything was written off' );
252 subtest 'Full payment tests' => sub {
256 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
258 my $item = $builder->build_sample_item(
260 biblionumber => $biblio->biblionumber,
261 library => $library->branchcode,
262 replacementprice => $replacement_amount,
263 itype => $item_type->itemtype
268 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
270 # Simulate item marked as lost
271 $item->itemlost(1)->store;
272 C4::Circulation::LostItem( $item->itemnumber, 1 );
274 my $processing_fee_lines = Koha::Account::Lines->search(
276 borrowernumber => $patron->id,
277 itemnumber => $item->itemnumber,
278 debit_type_code => 'PROCESSING'
281 is( $processing_fee_lines->count,
282 1, 'Only one processing fee produced' );
283 my $processing_fee_line = $processing_fee_lines->next;
284 is( $processing_fee_line->amount + 0,
286 'The right PROCESSING amount is generated' );
287 is( $processing_fee_line->amountoutstanding + 0,
289 'The right PROCESSING amountoutstanding is generated' );
291 my $lost_fee_lines = Koha::Account::Lines->search(
293 borrowernumber => $patron->id,
294 itemnumber => $item->itemnumber,
295 debit_type_code => 'LOST'
298 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
299 my $lost_fee_line = $lost_fee_lines->next;
300 is( $lost_fee_line->amount + 0,
301 $replacement_amount, 'The right LOST amount is generated' );
302 is( $lost_fee_line->amountoutstanding + 0,
304 'The right LOST amountountstanding is generated' );
306 my $account = $patron->account;
307 my $debts = $account->outstanding_debits;
310 my $credit = $account->add_credit(
312 amount => $account->balance,
318 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
320 # Simulate item marked as found
321 $item->itemlost(0)->store;
322 is( $item->{_refunded}, 1, 'Refund triggered' );
324 my $credit_return = Koha::Account::Lines->search(
326 itemnumber => $item->itemnumber,
327 credit_type_code => 'LOST_FOUND'
332 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
333 is( $credit_return->amount + 0,
335 'The account line of type LOST_FOUND has an amount of -99' );
337 $credit_return->amountoutstanding + 0,
339 'The account line of type LOST_FOUND has an amountoutstanding of -99'
342 $lost_fee_line->discard_changes;
343 is( $lost_fee_line->amountoutstanding + 0,
344 0, 'Lost fee has no outstanding amount' );
345 is( $lost_fee_line->debit_type_code,
346 'LOST', 'Lost fee now still has account type of LOST' );
347 is( $lost_fee_line->status, 'FOUND',
348 "Lost fee now has account status of FOUND" );
350 is( $patron->account->balance, -99,
351 'The patron balance is -99, a credit that equals the lost fee payment'
355 subtest 'Test without payment or write off' => sub {
359 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
361 my $item = $builder->build_sample_item(
363 biblionumber => $biblio->biblionumber,
364 library => $library->branchcode,
365 replacementprice => 23.00,
366 replacementprice => $replacement_amount,
367 itype => $item_type->itemtype
372 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
374 # Simulate item marked as lost
375 $item->itemlost(3)->store;
376 C4::Circulation::LostItem( $item->itemnumber, 1 );
378 my $processing_fee_lines = Koha::Account::Lines->search(
380 borrowernumber => $patron->id,
381 itemnumber => $item->itemnumber,
382 debit_type_code => 'PROCESSING'
385 is( $processing_fee_lines->count,
386 1, 'Only one processing fee produced' );
387 my $processing_fee_line = $processing_fee_lines->next;
388 is( $processing_fee_line->amount + 0,
390 'The right PROCESSING amount is generated' );
391 is( $processing_fee_line->amountoutstanding + 0,
393 'The right PROCESSING amountoutstanding is generated' );
395 my $lost_fee_lines = Koha::Account::Lines->search(
397 borrowernumber => $patron->id,
398 itemnumber => $item->itemnumber,
399 debit_type_code => 'LOST'
402 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
403 my $lost_fee_line = $lost_fee_lines->next;
404 is( $lost_fee_line->amount + 0,
405 $replacement_amount, 'The right LOST amount is generated' );
406 is( $lost_fee_line->amountoutstanding + 0,
408 'The right LOST amountountstanding is generated' );
410 # Simulate item marked as found
411 $item->itemlost(0)->store;
412 is( $item->{_refunded}, 1, 'Refund triggered' );
414 my $credit_return = Koha::Account::Lines->search(
416 itemnumber => $item->itemnumber,
417 credit_type_code => 'LOST_FOUND'
422 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
423 is( $credit_return->amount + 0,
425 'The account line of type LOST_FOUND has an amount of -99' );
427 $credit_return->amountoutstanding + 0,
429 'The account line of type LOST_FOUND has an amountoutstanding of 0'
432 $lost_fee_line->discard_changes;
433 is( $lost_fee_line->amountoutstanding + 0,
434 0, 'Lost fee has no outstanding amount' );
435 is( $lost_fee_line->debit_type_code,
436 'LOST', 'Lost fee now still has account type of LOST' );
437 is( $lost_fee_line->status, 'FOUND',
438 "Lost fee now has account status of FOUND" );
440 is( $patron->account->balance,
441 20, 'The patron balance is 20, still owes the processing fee' );
445 'Test with partial payement and write off, and remaining debt' =>
450 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
451 my $item = $builder->build_sample_item(
453 biblionumber => $biblio->biblionumber,
454 library => $library->branchcode,
455 replacementprice => $replacement_amount,
456 itype => $item_type->itemtype
461 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
463 # Simulate item marked as lost
464 $item->itemlost(1)->store;
465 C4::Circulation::LostItem( $item->itemnumber, 1 );
467 my $processing_fee_lines = Koha::Account::Lines->search(
469 borrowernumber => $patron->id,
470 itemnumber => $item->itemnumber,
471 debit_type_code => 'PROCESSING'
474 is( $processing_fee_lines->count,
475 1, 'Only one processing fee produced' );
476 my $processing_fee_line = $processing_fee_lines->next;
477 is( $processing_fee_line->amount + 0,
479 'The right PROCESSING amount is generated' );
480 is( $processing_fee_line->amountoutstanding + 0,
482 'The right PROCESSING amountoutstanding is generated' );
484 my $lost_fee_lines = Koha::Account::Lines->search(
486 borrowernumber => $patron->id,
487 itemnumber => $item->itemnumber,
488 debit_type_code => 'LOST'
491 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
492 my $lost_fee_line = $lost_fee_lines->next;
493 is( $lost_fee_line->amount + 0,
494 $replacement_amount, 'The right LOST amount is generated' );
495 is( $lost_fee_line->amountoutstanding + 0,
497 'The right LOST amountountstanding is generated' );
499 my $account = $patron->account;
502 $processfee_amount + $replacement_amount,
503 'Balance is PROCESSING + L'
507 my $payment_amount = 27;
508 my $payment = $account->add_credit(
510 amount => $payment_amount,
517 { debits => [$lost_fee_line], offset_type => 'Payment' } );
519 # Partially write off fee
520 my $write_off_amount = 25;
521 my $write_off = $account->add_credit(
523 amount => $write_off_amount,
529 { debits => [$lost_fee_line], offset_type => 'Writeoff' } );
534 $replacement_amount -
537 'Payment and write off applied'
540 # Store the amountoutstanding value
541 $lost_fee_line->discard_changes;
542 my $outstanding = $lost_fee_line->amountoutstanding;
544 # Simulate item marked as found
545 $item->itemlost(0)->store;
546 is( $item->{_refunded}, 1, 'Refund triggered' );
548 my $credit_return = Koha::Account::Lines->search(
550 itemnumber => $item->itemnumber,
551 credit_type_code => 'LOST_FOUND'
556 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
560 $processfee_amount - $payment_amount,
561 'Balance is PROCESSING - PAYMENT (LOST_FOUND)'
564 $lost_fee_line->discard_changes;
565 is( $lost_fee_line->amountoutstanding + 0,
566 0, 'Lost fee has no outstanding amount' );
567 is( $lost_fee_line->debit_type_code,
568 'LOST', 'Lost fee now still has account type of LOST' );
569 is( $lost_fee_line->status, 'FOUND',
570 "Lost fee now has account status of FOUND" );
573 $credit_return->amount + 0,
574 ( $payment_amount + $outstanding ) * -1,
575 'The account line of type LOST_FOUND has an amount equal to the payment + outstanding'
578 $credit_return->amountoutstanding + 0,
579 $payment_amount * -1,
580 'The account line of type LOST_FOUND has an amountoutstanding equal to the payment'
585 $processfee_amount - $payment_amount,
586 'The patron balance is the difference between the PROCESSING and the credit'
590 subtest 'Partial payment, existing debits and AccountAutoReconcile' =>
595 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
596 my $barcode = 'KD123456793';
597 my $replacement_amount = 100;
598 my $processfee_amount = 20;
600 my $item_type = $builder->build_object(
602 class => 'Koha::ItemTypes',
606 defaultreplacecost => undef,
608 rentalcharge_daily => 0,
612 my $item = Koha::Item->new(
614 biblionumber => $biblio->biblionumber,
615 homebranch => $library->branchcode,
616 holdingbranch => $library->branchcode,
618 replacementprice => $replacement_amount,
619 itype => $item_type->itemtype
624 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
626 # Simulate item marked as lost
627 $item->itemlost(1)->store;
628 C4::Circulation::LostItem( $item->itemnumber, 1 );
630 my $lost_fee_lines = Koha::Account::Lines->search(
632 borrowernumber => $patron->id,
633 itemnumber => $item->itemnumber,
634 debit_type_code => 'LOST'
637 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
638 my $lost_fee_line = $lost_fee_lines->next;
639 is( $lost_fee_line->amount + 0,
640 $replacement_amount, 'The right LOST amount is generated' );
641 is( $lost_fee_line->amountoutstanding + 0,
643 'The right LOST amountountstanding is generated' );
645 my $account = $patron->account;
646 is( $account->balance, $replacement_amount, 'Balance is L' );
649 my $payment_amount = 27;
650 my $payment = $account->add_credit(
652 amount => $payment_amount,
658 { debits => [$lost_fee_line], offset_type => 'Payment' } );
662 $replacement_amount - $payment_amount,
666 my $manual_debit_amount = 80;
669 amount => $manual_debit_amount,
677 $manual_debit_amount + $replacement_amount - $payment_amount,
678 'Manual debit applied'
681 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
683 # Simulate item marked as found
684 $item->itemlost(0)->store;
685 is( $item->{_refunded}, 1, 'Refund triggered' );
687 my $credit_return = Koha::Account::Lines->search(
689 itemnumber => $item->itemnumber,
690 credit_type_code => 'LOST_FOUND'
695 ok( $credit_return, 'An account line of type LOST_FOUND is added' );
699 $manual_debit_amount - $payment_amount,
700 'Balance is PROCESSING - payment (LOST_FOUND)'
703 my $manual_debit = Koha::Account::Lines->search(
705 borrowernumber => $patron->id,
706 debit_type_code => 'OVERDUE',
707 status => 'UNRETURNED'
711 $manual_debit->amountoutstanding + 0,
712 $manual_debit_amount - $payment_amount,
713 'reconcile_balance was called'
717 subtest 'Patron deleted' => sub {
720 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
721 my $barcode = 'KD123456794';
722 my $replacement_amount = 100;
723 my $processfee_amount = 20;
725 my $item_type = $builder->build_object(
727 class => 'Koha::ItemTypes',
731 defaultreplacecost => undef,
733 rentalcharge_daily => 0,
737 my $item = Koha::Item->new(
739 biblionumber => $biblio->biblionumber,
740 homebranch => $library->branchcode,
741 holdingbranch => $library->branchcode,
743 replacementprice => $replacement_amount,
744 itype => $item_type->itemtype
749 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
751 # Simulate item marked as lost
752 $item->itemlost(1)->store;
753 C4::Circulation::LostItem( $item->itemnumber, 1 );
758 # Simulate item marked as found
759 $item->itemlost(0)->store;
760 is( $item->{_refunded}, undef, 'No refund triggered' );
764 subtest 'restore fine | no overdue' => sub {
769 $builder->build_object( { class => "Koha::Patrons" } );
770 t::lib::Mocks::mock_userenv(
771 { patron => $manager, branchcode => $manager->branchcode } );
773 # Set lostreturn_policy to 'restore' for tests
774 my $specific_rule_restore = $builder->build(
776 source => 'CirculationRule',
778 branchcode => $manager->branchcode,
779 categorycode => undef,
781 rule_name => 'lostreturn',
782 rule_value => 'restore'
787 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
789 my $item = $builder->build_sample_item(
791 biblionumber => $biblio->biblionumber,
792 library => $library->branchcode,
793 replacementprice => $replacement_amount,
794 itype => $item_type->itemtype
799 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
801 # Simulate item marked as lost
802 $item->itemlost(1)->store;
803 C4::Circulation::LostItem( $item->itemnumber, 1 );
805 my $processing_fee_lines = Koha::Account::Lines->search(
807 borrowernumber => $patron->id,
808 itemnumber => $item->itemnumber,
809 debit_type_code => 'PROCESSING'
812 is( $processing_fee_lines->count,
813 1, 'Only one processing fee produced' );
814 my $processing_fee_line = $processing_fee_lines->next;
815 is( $processing_fee_line->amount + 0,
817 'The right PROCESSING amount is generated' );
818 is( $processing_fee_line->amountoutstanding + 0,
820 'The right PROCESSING amountoutstanding is generated' );
822 my $lost_fee_lines = Koha::Account::Lines->search(
824 borrowernumber => $patron->id,
825 itemnumber => $item->itemnumber,
826 debit_type_code => 'LOST'
829 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
830 my $lost_fee_line = $lost_fee_lines->next;
831 is( $lost_fee_line->amount + 0,
832 $replacement_amount, 'The right LOST amount is generated' );
833 is( $lost_fee_line->amountoutstanding + 0,
835 'The right LOST amountountstanding is generated' );
837 my $account = $patron->account;
838 my $debts = $account->outstanding_debits;
841 my $credit = $account->add_credit(
843 amount => $account->balance,
849 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
851 # Simulate item marked as found
852 $item->itemlost(0)->store;
853 is( $item->{_refunded}, 1, 'Refund triggered' );
854 is( $item->{_restored}, undef, 'Restore not triggered when there is no overdue fine found' );
857 subtest 'restore fine | unforgiven overdue' => sub {
861 # Set lostreturn_policy to 'restore' for tests
863 $builder->build_object( { class => "Koha::Patrons" } );
864 t::lib::Mocks::mock_userenv(
865 { patron => $manager, branchcode => $manager->branchcode } );
866 my $specific_rule_restore = $builder->build(
868 source => 'CirculationRule',
870 branchcode => $manager->branchcode,
871 categorycode => undef,
873 rule_name => 'lostreturn',
874 rule_value => 'restore'
879 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
881 my $item = $builder->build_sample_item(
883 biblionumber => $biblio->biblionumber,
884 library => $library->branchcode,
885 replacementprice => $replacement_amount,
886 itype => $item_type->itemtype
891 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
893 # Simulate item marked as lost
894 $item->itemlost(1)->store;
895 C4::Circulation::LostItem( $item->itemnumber, 1 );
897 my $processing_fee_lines = Koha::Account::Lines->search(
899 borrowernumber => $patron->id,
900 itemnumber => $item->itemnumber,
901 debit_type_code => 'PROCESSING'
904 is( $processing_fee_lines->count,
905 1, 'Only one processing fee produced' );
906 my $processing_fee_line = $processing_fee_lines->next;
907 is( $processing_fee_line->amount + 0,
909 'The right PROCESSING amount is generated' );
910 is( $processing_fee_line->amountoutstanding + 0,
912 'The right PROCESSING amountoutstanding is generated' );
914 my $lost_fee_lines = Koha::Account::Lines->search(
916 borrowernumber => $patron->id,
917 itemnumber => $item->itemnumber,
918 debit_type_code => 'LOST'
921 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
922 my $lost_fee_line = $lost_fee_lines->next;
923 is( $lost_fee_line->amount + 0,
924 $replacement_amount, 'The right LOST amount is generated' );
925 is( $lost_fee_line->amountoutstanding + 0,
927 'The right LOST amountountstanding is generated' );
929 my $account = $patron->account;
930 my $debts = $account->outstanding_debits;
933 my $credit = $account->add_credit(
935 amount => $account->balance,
941 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
944 my $overdue = $account->add_debit(
947 user_id => $manager->borrowernumber,
948 library_id => $library->branchcode,
950 item_id => $item->itemnumber,
954 $overdue->status('LOST')->store();
955 $overdue->discard_changes;
956 is( $overdue->status, 'LOST',
957 'Overdue status set to LOST' );
959 # Simulate item marked as found
960 $item->itemlost(0)->store;
961 is( $item->{_refunded}, 1, 'Refund triggered' );
962 is( $item->{_restored}, undef, 'Restore not triggered when overdue was not forgiven' );
963 $overdue->discard_changes;
964 is( $overdue->status, 'FOUND',
965 'Overdue status updated to FOUND' );
968 subtest 'restore fine | forgiven overdue' => sub {
972 # Set lostreturn_policy to 'restore' for tests
974 $builder->build_object( { class => "Koha::Patrons" } );
975 t::lib::Mocks::mock_userenv(
976 { patron => $manager, branchcode => $manager->branchcode } );
977 my $specific_rule_restore = $builder->build(
979 source => 'CirculationRule',
981 branchcode => $manager->branchcode,
982 categorycode => undef,
984 rule_name => 'lostreturn',
985 rule_value => 'restore'
990 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
992 my $item = $builder->build_sample_item(
994 biblionumber => $biblio->biblionumber,
995 library => $library->branchcode,
996 replacementprice => $replacement_amount,
997 itype => $item_type->itemtype
1002 C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
1004 # Simulate item marked as lost
1005 $item->itemlost(1)->store;
1006 C4::Circulation::LostItem( $item->itemnumber, 1 );
1008 my $processing_fee_lines = Koha::Account::Lines->search(
1010 borrowernumber => $patron->id,
1011 itemnumber => $item->itemnumber,
1012 debit_type_code => 'PROCESSING'
1015 is( $processing_fee_lines->count,
1016 1, 'Only one processing fee produced' );
1017 my $processing_fee_line = $processing_fee_lines->next;
1018 is( $processing_fee_line->amount + 0,
1020 'The right PROCESSING amount is generated' );
1021 is( $processing_fee_line->amountoutstanding + 0,
1023 'The right PROCESSING amountoutstanding is generated' );
1025 my $lost_fee_lines = Koha::Account::Lines->search(
1027 borrowernumber => $patron->id,
1028 itemnumber => $item->itemnumber,
1029 debit_type_code => 'LOST'
1032 is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1033 my $lost_fee_line = $lost_fee_lines->next;
1034 is( $lost_fee_line->amount + 0,
1035 $replacement_amount, 'The right LOST amount is generated' );
1036 is( $lost_fee_line->amountoutstanding + 0,
1037 $replacement_amount,
1038 'The right LOST amountountstanding is generated' );
1040 my $account = $patron->account;
1041 my $debts = $account->outstanding_debits;
1044 my $credit = $account->add_credit(
1046 amount => $account->balance,
1048 interface => 'test',
1052 { debits => [ $debts->as_list ], offset_type => 'Payment' } );
1055 my $overdue = $account->add_debit(
1058 user_id => $manager->borrowernumber,
1059 library_id => $library->branchcode,
1060 interface => 'test',
1061 item_id => $item->itemnumber,
1065 $overdue->status('LOST')->store();
1066 is( $overdue->status, 'LOST',
1067 'Overdue status set to LOST' );
1069 t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 0 );
1072 $credit = $account->add_credit(
1075 user_id => $manager->borrowernumber,
1076 library_id => $library->branchcode,
1077 interface => 'test',
1079 item_id => $item->itemnumber
1083 { debits => [$overdue], offset_type => 'Forgiven' } );
1085 # Simulate item marked as found
1086 $item->itemlost(0)->store;
1087 is( $item->{_refunded}, 1, 'Refund triggered' );
1088 is( $item->{_restored}, 1, 'Restore triggered when overdue was forgiven' );
1089 $overdue->discard_changes;
1090 is( $overdue->status, 'FOUND', 'Overdue status updated to FOUND' );
1091 is( $overdue->amountoutstanding, $overdue->amount, 'Overdue outstanding has been restored' );
1092 $credit->discard_changes;
1093 is( $credit->status, 'VOID', 'Overdue Forgival has been marked as VOID');
1096 subtest 'Continue when userenv is not set' => sub {
1099 my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1100 my $barcode = 'KD123456795';
1101 my $replacement_amount = 100;
1102 my $processfee_amount = 20;
1104 my $item_type = $builder->build_object(
1106 class => 'Koha::ItemTypes',
1108 notforloan => undef,
1110 defaultreplacecost => undef,
1112 rentalcharge_daily => 0,
1116 my $item = $builder->build_sample_item(
1118 biblionumber => $biblio->biblionumber,
1119 homebranch => $library->branchcode,
1120 holdingbranch => $library->branchcode,
1121 barcode => $barcode,
1122 replacementprice => $replacement_amount,
1123 itype => $item_type->itemtype
1128 C4::Circulation::AddIssue( $patron->unblessed, $barcode );
1130 # Simulate item marked as lost
1131 $item->itemlost(1)->store;
1132 C4::Circulation::LostItem( $item->itemnumber, 1 );
1135 C4::Context->_new_userenv(undef);
1137 # Simluate item marked as found
1138 $item->itemlost(0)->store;
1139 is( $item->{_refunded}, 1, 'No refund triggered' );
1145 subtest 'get_transfer' => sub {
1148 my $transfer = $new_item_1->get_transfer();
1149 is( $transfer, undef, 'Koha::Item->get_transfer should return undef if the item is not in transit' );
1151 my $library_to = $builder->build( { source => 'Branch' } );
1153 C4::Circulation::transferbook({
1154 from_branch => $new_item_1->holdingbranch,
1155 to_branch => $library_to->{branchcode},
1156 barcode => $new_item_1->barcode,
1159 $transfer = $new_item_1->get_transfer();
1160 is( ref($transfer), 'Koha::Item::Transfer', 'Koha::Item->get_transfer should return a Koha::Item::Transfers object' );
1162 is( $transfer->itemnumber, $new_item_1->itemnumber, 'Koha::Item->get_transfer should return a valid Koha::Item::Transfers object' );
1165 subtest 'holds' => sub {
1168 my $biblio = $builder->build_sample_biblio();
1169 my $item = $builder->build_sample_item({
1170 biblionumber => $biblio->biblionumber,
1172 is($item->holds->count, 0, "Nothing returned if no holds");
1173 my $hold1 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'T' }});
1174 my $hold2 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1175 my $hold3 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1177 is($item->holds()->count,3,"Three holds found");
1178 is($item->holds({found => 'W'})->count,2,"Two waiting holds found");
1179 is_deeply($item->holds({found => 'T'})->next->unblessed,$hold1,"Found transit holds matches the hold");
1180 is($item->holds({found => undef})->count, 0,"Nothing returned if no matching holds");
1183 subtest 'biblio' => sub {
1186 my $biblio = $retrieved_item_1->biblio;
1187 is( ref( $biblio ), 'Koha::Biblio', 'Koha::Item->biblio should return a Koha::Biblio' );
1188 is( $biblio->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblio should return the correct biblio' );
1191 subtest 'biblioitem' => sub {
1194 my $biblioitem = $retrieved_item_1->biblioitem;
1195 is( ref( $biblioitem ), 'Koha::Biblioitem', 'Koha::Item->biblioitem should return a Koha::Biblioitem' );
1196 is( $biblioitem->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblioitem should return the correct biblioitem' );
1200 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
1201 subtest 'checkout' => sub {
1203 my $item = Koha::Items->find( $new_item_1->itemnumber );
1205 my $checkout = $item->checkout;
1206 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no current checkout on this item' );
1209 my $patron = $builder->build({ source => 'Borrower' });
1210 C4::Circulation::AddIssue( $patron, $item->barcode );
1211 $checkout = $retrieved_item_1->checkout;
1212 is( ref( $checkout ), 'Koha::Checkout', 'Koha::Item->checkout should return a Koha::Checkout' );
1213 is( $checkout->itemnumber, $item->itemnumber, 'Koha::Item->checkout should return the correct checkout' );
1214 is( $checkout->borrowernumber, $patron->{borrowernumber}, 'Koha::Item->checkout should return the correct checkout' );
1217 C4::Circulation::AddReturn( $item->barcode );
1219 # There is no more checkout on this item, making sure it will not return old checkouts
1220 $checkout = $item->checkout;
1221 is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no *current* checkout on this item' );
1224 subtest 'can_be_transferred' => sub {
1227 t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
1228 t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
1230 my $biblio = $builder->build_sample_biblio();
1231 my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
1232 my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
1233 my $item = $builder->build_sample_item({
1234 biblionumber => $biblio->biblionumber,
1235 homebranch => $library1->branchcode,
1236 holdingbranch => $library1->branchcode,
1239 is(Koha::Item::Transfer::Limits->search({
1240 fromBranch => $library1->branchcode,
1241 toBranch => $library2->branchcode,
1242 })->count, 0, 'There are no transfer limits between libraries.');
1243 ok($item->can_be_transferred({ to => $library2 }),
1244 'Item can be transferred between libraries.');
1246 my $limit = Koha::Item::Transfer::Limit->new({
1247 fromBranch => $library1->branchcode,
1248 toBranch => $library2->branchcode,
1249 itemtype => $item->effective_itemtype,
1251 is(Koha::Item::Transfer::Limits->search({
1252 fromBranch => $library1->branchcode,
1253 toBranch => $library2->branchcode,
1254 })->count, 1, 'Given we have added a transfer limit,');
1255 is($item->can_be_transferred({ to => $library2 }), 0,
1256 'Item can no longer be transferred between libraries.');
1257 is($item->can_be_transferred({ to => $library2, from => $library1 }), 0,
1258 'We get the same result also if we pass the from-library parameter.');
1261 subtest 'filter_by_for_loan' => sub {
1264 my $biblio = $builder->build_sample_biblio;
1265 is( $biblio->items->filter_by_for_loan->count, 0, 'no item yet' );
1266 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 1 } );
1267 is( $biblio->items->filter_by_for_loan->count, 0, 'no item for loan' );
1268 $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 0 } );
1269 is( $biblio->items->filter_by_for_loan->count, 1, '1 item for loan' );
1274 # Reset nb_of_items prior to testing delete
1275 $nb_of_items = Koha::Items->search->count;
1278 $retrieved_item_1->delete;
1279 is( Koha::Items->search->count, $nb_of_items - 1, 'Delete should have deleted the item' );
1281 $schema->storage->txn_rollback;