Bug 19532: (follow-up) aria-hidden attr on OPAC, and more
[koha.git] / t / db_dependent / Koha / Items.t
1 #!/usr/bin/perl
2
3 # Copyright 2016 Koha Development team
4 #
5 # This file is part of Koha
6 #
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.
11 #
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.
16 #
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>.
19
20 use Modern::Perl;
21
22 use Test::More tests => 15;
23
24 use Test::MockModule;
25 use Test::Exception;
26 use Time::Fake;
27
28 use C4::Circulation qw( AddIssue LostItem AddReturn );
29 use C4::Context;
30 use Koha::Item;
31 use Koha::Item::Transfer::Limits;
32 use Koha::Items;
33 use Koha::Database;
34 use Koha::DateUtils qw( dt_from_string );
35
36 use t::lib::TestBuilder;
37 use t::lib::Mocks;
38 use t::lib::Dates;
39
40 my $schema = Koha::Database->new->schema;
41 $schema->storage->txn_begin;
42
43 my $dbh     = C4::Context->dbh;
44
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},
53 });
54 my $new_item_2   = $builder->build_sample_item({
55     biblionumber => $biblio->biblionumber,
56     homebranch       => $library->{branchcode},
57     holdingbranch    => $library->{branchcode},
58 });
59
60
61 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
62
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' );
65
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' );
68
69 subtest 'store' => sub {
70     plan tests => 7;
71
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(
75         {
76             homebranch    => $library->{branchcode},
77             holdingbranch => $library->{branchcode},
78             biblionumber  => $biblio->biblionumber,
79             location      => 'my_loc',
80         }
81     )->store->get_from_storage;
82
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' );
87     is(
88         $item->itype,
89         $biblio->biblioitem->itemtype,
90         'items.itype must have been set to biblioitem.itemtype is not given'
91     );
92     $item->delete;
93
94     subtest 'permanent_location' => sub {
95         plan tests => 2;
96
97         subtest 'location passed to ->store' => sub {
98             plan tests => 7;
99
100             my $location = 'my_loc';
101             my $attributes = {
102                 homebranch    => $library->{branchcode},
103                 holdingbranch => $library->{branchcode},
104                 biblionumber  => $biblio->biblionumber,
105                 location      => $location,
106             };
107
108             {
109                 # NewItemsDefaultLocation not set
110                 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', '' );
111
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' );
118                 $item->delete;
119
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' );
126                 $item->delete;
127             }
128
129             {
130                 # NewItemsDefaultLocation set
131                 my $default_location = 'default_location';
132                 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', $default_location );
133
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' );
140                 $item->delete;
141
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' );
150                 $item->delete;
151             }
152         };
153
154         subtest 'location NOT passed to ->store' => sub {
155             plan tests => 7;
156
157             my $attributes = {
158                 homebranch    => $library->{branchcode},
159                 holdingbranch => $library->{branchcode},
160                 biblionumber  => $biblio->biblionumber,
161             };
162
163             {
164                 # NewItemsDefaultLocation not set
165                 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', '' );
166
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' );
173                 $item->delete;
174
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' );
181                 $item->delete;
182             }
183
184             {
185                 # NewItemsDefaultLocation set
186                 my $default_location = 'default_location';
187                 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', $default_location );
188
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' );
195                 $item->delete;
196
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' );
205                 $item->delete;
206             }
207         };
208
209     };
210
211     subtest '*_on updates' => sub {
212         plan tests => 9;
213
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'.
216
217         my @fields = qw( itemlost withdrawn damaged );
218         my $today = dt_from_string();
219         my $yesterday = $today->clone()->subtract( days => 1 );
220
221         for my $field ( @fields ) {
222             my $item = $builder->build_sample_item(
223                 {
224                     itemlost     => 0,
225                     itemlost_on  => undef,
226                     withdrawn    => 0,
227                     withdrawn_on => undef,
228                     damaged      => 0,
229                     damaged_on   => undef
230                 }
231             );
232             my $field_on = $field . '_on';
233
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" );
240
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" );
247
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");
252
253             Time::Fake->reset;
254         }
255     };
256
257     subtest '_lost_found_trigger' => sub {
258         plan tests => 10;
259
260         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
261         t::lib::Mocks::mock_preference( 'WhenLostForgiveFine',          0 );
262
263         my $processfee_amount  = 20;
264         my $replacement_amount = 99.00;
265         my $item_type          = $builder->build_object(
266             {
267                 class => 'Koha::ItemTypes',
268                 value => {
269                     notforloan         => undef,
270                     rentalcharge       => 0,
271                     defaultreplacecost => undef,
272                     processfee         => $processfee_amount,
273                     rentalcharge_daily => 0,
274                 }
275             }
276         );
277         my $library = $builder->build_object( { class => 'Koha::Libraries' } );
278
279         $biblio = $builder->build_sample_biblio( { author => 'Hall, Daria' } );
280
281         subtest 'Full write-off tests' => sub {
282
283             plan tests => 12;
284
285             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
286             my $manager =
287               $builder->build_object( { class => "Koha::Patrons" } );
288             t::lib::Mocks::mock_userenv(
289                 { patron => $manager, branchcode => $manager->branchcode } );
290
291             my $item = $builder->build_sample_item(
292                 {
293                     biblionumber     => $biblio->biblionumber,
294                     library          => $library->branchcode,
295                     replacementprice => $replacement_amount,
296                     itype            => $item_type->itemtype,
297                 }
298             );
299
300             C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
301
302             # Simulate item marked as lost
303             $item->itemlost(3)->store;
304             C4::Circulation::LostItem( $item->itemnumber, 1 );
305
306             my $processing_fee_lines = Koha::Account::Lines->search(
307                 {
308                     borrowernumber  => $patron->id,
309                     itemnumber      => $item->itemnumber,
310                     debit_type_code => 'PROCESSING'
311                 }
312             );
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,
317                 $processfee_amount,
318                 'The right PROCESSING amount is generated' );
319             is( $processing_fee_line->amountoutstanding + 0,
320                 $processfee_amount,
321                 'The right PROCESSING amountoutstanding is generated' );
322
323             my $lost_fee_lines = Koha::Account::Lines->search(
324                 {
325                     borrowernumber  => $patron->id,
326                     itemnumber      => $item->itemnumber,
327                     debit_type_code => 'LOST'
328                 }
329             );
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,
335                 $replacement_amount,
336                 'The right LOST amountoutstanding is generated' );
337             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
338
339             my $account = $patron->account;
340             my $debts   = $account->outstanding_debits;
341
342             # Write off the debt
343             my $credit = $account->add_credit(
344                 {
345                     amount    => $account->balance,
346                     type      => 'WRITEOFF',
347                     interface => 'test',
348                 }
349             );
350             $credit->apply( { debits => [ $debts->as_list ] } );
351
352             # Simulate item marked as found
353             $item->itemlost(0)->store;
354             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 0, 'No LOST_FOUND account line added' );
355
356             $lost_fee_line->discard_changes;    # reload from DB
357             is( $lost_fee_line->amountoutstanding + 0,
358                 0, 'Lost fee has no outstanding amount' );
359             is( $lost_fee_line->debit_type_code,
360                 'LOST', 'Lost fee now still has account type of LOST' );
361             is( $lost_fee_line->status, 'FOUND',
362                 "Lost fee now has account status of FOUND - No Refund" );
363
364             is( $patron->account->balance,
365                 -0, 'The patron balance is 0, everything was written off' );
366         };
367
368         subtest 'Full payment tests' => sub {
369
370             plan tests => 14;
371
372             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
373
374             my $item = $builder->build_sample_item(
375                 {
376                     biblionumber     => $biblio->biblionumber,
377                     library          => $library->branchcode,
378                     replacementprice => $replacement_amount,
379                     itype            => $item_type->itemtype
380                 }
381             );
382
383             my $issue =
384               C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
385
386             # Simulate item marked as lost
387             $item->itemlost(1)->store;
388             C4::Circulation::LostItem( $item->itemnumber, 1 );
389
390             my $processing_fee_lines = Koha::Account::Lines->search(
391                 {
392                     borrowernumber  => $patron->id,
393                     itemnumber      => $item->itemnumber,
394                     debit_type_code => 'PROCESSING'
395                 }
396             );
397             is( $processing_fee_lines->count,
398                 1, 'Only one processing fee produced' );
399             my $processing_fee_line = $processing_fee_lines->next;
400             is( $processing_fee_line->amount + 0,
401                 $processfee_amount,
402                 'The right PROCESSING amount is generated' );
403             is( $processing_fee_line->amountoutstanding + 0,
404                 $processfee_amount,
405                 'The right PROCESSING amountoutstanding is generated' );
406
407             my $lost_fee_lines = Koha::Account::Lines->search(
408                 {
409                     borrowernumber  => $patron->id,
410                     itemnumber      => $item->itemnumber,
411                     debit_type_code => 'LOST'
412                 }
413             );
414             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
415             my $lost_fee_line = $lost_fee_lines->next;
416             is( $lost_fee_line->amount + 0,
417                 $replacement_amount, 'The right LOST amount is generated' );
418             is( $lost_fee_line->amountoutstanding + 0,
419                 $replacement_amount,
420                 'The right LOST amountountstanding is generated' );
421
422             my $account = $patron->account;
423             my $debts   = $account->outstanding_debits;
424
425             # Pay off the debt
426             my $credit = $account->add_credit(
427                 {
428                     amount    => $account->balance,
429                     type      => 'PAYMENT',
430                     interface => 'test',
431                 }
432             );
433             $credit->apply( { debits => [ $debts->as_list ] } );
434
435             # Simulate item marked as found
436             $item->itemlost(0)->store;
437             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
438
439             my $credit_return = Koha::Account::Lines->search(
440                 {
441                     itemnumber       => $item->itemnumber,
442                     credit_type_code => 'LOST_FOUND'
443                 },
444                 { rows => 1 }
445             )->single;
446
447             ok( $credit_return, 'An account line of type LOST_FOUND is added' );
448             is( $credit_return->amount + 0,
449                 -99.00,
450                 'The account line of type LOST_FOUND has an amount of -99' );
451             is(
452                 $credit_return->amountoutstanding + 0,
453                 -99.00,
454 'The account line of type LOST_FOUND has an amountoutstanding of -99'
455             );
456
457             $lost_fee_line->discard_changes;
458             is( $lost_fee_line->amountoutstanding + 0,
459                 0, 'Lost fee has no outstanding amount' );
460             is( $lost_fee_line->debit_type_code,
461                 'LOST', 'Lost fee now still has account type of LOST' );
462             is( $lost_fee_line->status, 'FOUND',
463                 "Lost fee now has account status of FOUND" );
464
465             is( $patron->account->balance, -99,
466 'The patron balance is -99, a credit that equals the lost fee payment'
467             );
468         };
469
470         subtest 'Test without payment or write off' => sub {
471
472             plan tests => 14;
473
474             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
475
476             my $item = $builder->build_sample_item(
477                 {
478                     biblionumber     => $biblio->biblionumber,
479                     library          => $library->branchcode,
480                     replacementprice => 23.00,
481                     replacementprice => $replacement_amount,
482                     itype            => $item_type->itemtype
483                 }
484             );
485
486             my $issue =
487               C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
488
489             # Simulate item marked as lost
490             $item->itemlost(3)->store;
491             C4::Circulation::LostItem( $item->itemnumber, 1 );
492
493             my $processing_fee_lines = Koha::Account::Lines->search(
494                 {
495                     borrowernumber  => $patron->id,
496                     itemnumber      => $item->itemnumber,
497                     debit_type_code => 'PROCESSING'
498                 }
499             );
500             is( $processing_fee_lines->count,
501                 1, 'Only one processing fee produced' );
502             my $processing_fee_line = $processing_fee_lines->next;
503             is( $processing_fee_line->amount + 0,
504                 $processfee_amount,
505                 'The right PROCESSING amount is generated' );
506             is( $processing_fee_line->amountoutstanding + 0,
507                 $processfee_amount,
508                 'The right PROCESSING amountoutstanding is generated' );
509
510             my $lost_fee_lines = Koha::Account::Lines->search(
511                 {
512                     borrowernumber  => $patron->id,
513                     itemnumber      => $item->itemnumber,
514                     debit_type_code => 'LOST'
515                 }
516             );
517             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
518             my $lost_fee_line = $lost_fee_lines->next;
519             is( $lost_fee_line->amount + 0,
520                 $replacement_amount, 'The right LOST amount is generated' );
521             is( $lost_fee_line->amountoutstanding + 0,
522                 $replacement_amount,
523                 'The right LOST amountountstanding is generated' );
524
525             # Simulate item marked as found
526             $item->itemlost(0)->store;
527             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
528
529             my $credit_return = Koha::Account::Lines->search(
530                 {
531                     itemnumber       => $item->itemnumber,
532                     credit_type_code => 'LOST_FOUND'
533                 },
534                 { rows => 1 }
535             )->single;
536
537             ok( $credit_return, 'An account line of type LOST_FOUND is added' );
538             is( $credit_return->amount + 0,
539                 -99.00,
540                 'The account line of type LOST_FOUND has an amount of -99' );
541             is(
542                 $credit_return->amountoutstanding + 0,
543                 0,
544 'The account line of type LOST_FOUND has an amountoutstanding of 0'
545             );
546
547             $lost_fee_line->discard_changes;
548             is( $lost_fee_line->amountoutstanding + 0,
549                 0, 'Lost fee has no outstanding amount' );
550             is( $lost_fee_line->debit_type_code,
551                 'LOST', 'Lost fee now still has account type of LOST' );
552             is( $lost_fee_line->status, 'FOUND',
553                 "Lost fee now has account status of FOUND" );
554
555             is( $patron->account->balance,
556                 20, 'The patron balance is 20, still owes the processing fee' );
557         };
558
559         subtest
560           'Test with partial payment and write off, and remaining debt' =>
561           sub {
562
563             plan tests => 19;
564
565             t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 0 );
566
567             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
568             my $item = $builder->build_sample_item(
569                 {
570                     biblionumber     => $biblio->biblionumber,
571                     library          => $library->branchcode,
572                     replacementprice => $replacement_amount,
573                     itype            => $item_type->itemtype
574                 }
575             );
576
577             my $issue =
578               C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
579
580             # Simulate item marked as lost
581             $item->itemlost(1)->store;
582             C4::Circulation::LostItem( $item->itemnumber, 1 );
583
584             my $processing_fee_lines = Koha::Account::Lines->search(
585                 {
586                     borrowernumber  => $patron->id,
587                     itemnumber      => $item->itemnumber,
588                     debit_type_code => 'PROCESSING'
589                 }
590             );
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,
595                 $processfee_amount,
596                 'The right PROCESSING amount is generated' );
597             is( $processing_fee_line->amountoutstanding + 0,
598                 $processfee_amount,
599                 'The right PROCESSING amountoutstanding is generated' );
600
601             my $lost_fee_lines = Koha::Account::Lines->search(
602                 {
603                     borrowernumber  => $patron->id,
604                     itemnumber      => $item->itemnumber,
605                     debit_type_code => 'LOST'
606                 }
607             );
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,
613                 $replacement_amount,
614                 'The right LOST amountountstanding is generated' );
615
616             my $account = $patron->account;
617             is(
618                 $account->balance,
619                 $processfee_amount + $replacement_amount,
620                 'Balance is PROCESSING + LOST'
621             );
622
623             # Partially pay fee (99 - 27 = 72)
624             my $payment_amount = 24;
625             my $payment        = $account->add_credit(
626                 {
627                     amount    => $payment_amount,
628                     type      => 'PAYMENT',
629                     interface => 'test',
630                 }
631             );
632
633             $payment->apply( { debits => [$lost_fee_line] } );
634
635             # Partially write off fee (72 - 20 = 52)
636             my $write_off_amount = 20;
637             my $write_off        = $account->add_credit(
638                 {
639                     amount    => $write_off_amount,
640                     type      => 'WRITEOFF',
641                     interface => 'test',
642                 }
643             );
644             $write_off->apply( { debits => [$lost_fee_line] } );
645
646
647             my $payment_amount_2 = 3;
648             my $payment_2        = $account->add_credit(
649                 {
650                     amount    => $payment_amount_2,
651                     type      => 'PAYMENT',
652                     interface => 'test',
653                 }
654             );
655
656             $payment_2->apply(
657                 { debits => [$lost_fee_line] } );
658
659             # Partially write off fee (52 - 5 = 47)
660             my $write_off_amount_2 = 5;
661             my $write_off_2        = $account->add_credit(
662                 {
663                     amount    => $write_off_amount_2,
664                     type      => 'WRITEOFF',
665                     interface => 'test',
666                 }
667             );
668
669             $write_off_2->apply(
670                 { debits => [$lost_fee_line] } );
671
672             is(
673                 $account->balance,
674                 $processfee_amount +
675                   $replacement_amount -
676                   $payment_amount -
677                   $write_off_amount -
678                   $payment_amount_2 -
679                   $write_off_amount_2,
680                 'Balance is PROCESSING + LOST - PAYMENT 1 - WRITEOFF - PAYMENT 2 - WRITEOFF 2'
681             );
682
683             # VOID payment_2 and writeoff_2
684             $payment_2->void({ interface => 'test' });
685             $write_off_2->void({ interface => 'test' });
686
687             is(
688                 $account->balance,
689                 $processfee_amount +
690                   $replacement_amount -
691                   $payment_amount -
692                   $write_off_amount,
693                 'Balance is PROCESSING + LOST - PAYMENT 1 - WRITEOFF (PAYMENT 2 and WRITEOFF 2 VOIDED)'
694             );
695
696             # Store the amountoutstanding value
697             $lost_fee_line->discard_changes;
698             my $outstanding = $lost_fee_line->amountoutstanding;
699             is(
700                 $outstanding + 0,
701                 $replacement_amount - $payment_amount - $write_off_amount,
702                 "Lost Fee Outstanding is LOST - PAYMENT 1 - WRITEOFF"
703             );
704
705             # Simulate item marked as found
706             $item->itemlost(0)->store;
707             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
708
709             my $credit_return = Koha::Account::Lines->search(
710                 {
711                     itemnumber       => $item->itemnumber,
712                     credit_type_code => 'LOST_FOUND'
713                 },
714                 { rows => 1 }
715             )->single;
716
717             ok( $credit_return, 'An account line of type LOST_FOUND is added' );
718
719             is(
720                 $account->balance,
721                 $processfee_amount - $payment_amount,
722                 'Balance is PROCESSING - PAYMENT (LOST_FOUND)'
723             );
724
725             $lost_fee_line->discard_changes;
726             is( $lost_fee_line->amountoutstanding + 0,
727                 0, 'Lost fee has no outstanding amount' );
728             is( $lost_fee_line->debit_type_code,
729                 'LOST', 'Lost fee now still has account type of LOST' );
730             is( $lost_fee_line->status, 'FOUND',
731                 "Lost fee now has account status of FOUND" );
732
733             is(
734                 $credit_return->amount + 0,
735                 ( $payment_amount + $outstanding ) * -1,
736 'The account line of type LOST_FOUND has an amount equal to the payment 1 + outstanding'
737             );
738             is(
739                 $credit_return->amountoutstanding + 0,
740                 $payment_amount * -1,
741 'The account line of type LOST_FOUND has an amountoutstanding equal to the payment'
742             );
743
744             is(
745                 $account->balance,
746                 $processfee_amount - $payment_amount,
747 'The patron balance is the difference between the PROCESSING and the credit'
748             );
749           };
750
751         subtest 'Partial payment, existing debits and AccountAutoReconcile' =>
752           sub {
753
754             plan tests => 10;
755
756             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
757             my $barcode            = 'KD123456793';
758             my $replacement_amount = 100;
759             my $processfee_amount  = 20;
760
761             my $item_type = $builder->build_object(
762                 {
763                     class => 'Koha::ItemTypes',
764                     value => {
765                         notforloan         => undef,
766                         rentalcharge       => 0,
767                         defaultreplacecost => undef,
768                         processfee         => 0,
769                         rentalcharge_daily => 0,
770                     }
771                 }
772             );
773             my $item = Koha::Item->new(
774                 {
775                     biblionumber     => $biblio->biblionumber,
776                     homebranch       => $library->branchcode,
777                     holdingbranch    => $library->branchcode,
778                     barcode          => $barcode,
779                     replacementprice => $replacement_amount,
780                     itype            => $item_type->itemtype
781                 },
782             )->store;
783
784             my $issue =
785               C4::Circulation::AddIssue( $patron->unblessed, $barcode );
786
787             # Simulate item marked as lost
788             $item->itemlost(1)->store;
789             C4::Circulation::LostItem( $item->itemnumber, 1 );
790
791             my $lost_fee_lines = Koha::Account::Lines->search(
792                 {
793                     borrowernumber  => $patron->id,
794                     itemnumber      => $item->itemnumber,
795                     debit_type_code => 'LOST'
796                 }
797             );
798             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
799             my $lost_fee_line = $lost_fee_lines->next;
800             is( $lost_fee_line->amount + 0,
801                 $replacement_amount, 'The right LOST amount is generated' );
802             is( $lost_fee_line->amountoutstanding + 0,
803                 $replacement_amount,
804                 'The right LOST amountountstanding is generated' );
805
806             my $account = $patron->account;
807             is( $account->balance, $replacement_amount, 'Balance is L' );
808
809             # Partially pay fee
810             my $payment_amount = 27;
811             my $payment        = $account->add_credit(
812                 {
813                     amount    => $payment_amount,
814                     type      => 'PAYMENT',
815                     interface => 'test',
816                 }
817             );
818             $payment->apply( { debits => [$lost_fee_line] } );
819
820             is(
821                 $account->balance,
822                 $replacement_amount - $payment_amount,
823                 'Payment applied'
824             );
825
826             my $manual_debit_amount = 80;
827             $account->add_debit(
828                 {
829                     amount    => $manual_debit_amount,
830                     type      => 'OVERDUE',
831                     interface => 'test'
832                 }
833             );
834
835             is(
836                 $account->balance,
837                 $manual_debit_amount + $replacement_amount - $payment_amount,
838                 'Manual debit applied'
839             );
840
841             t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
842
843             # Simulate item marked as found
844             $item->itemlost(0)->store;
845             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
846
847             my $credit_return = Koha::Account::Lines->search(
848                 {
849                     itemnumber       => $item->itemnumber,
850                     credit_type_code => 'LOST_FOUND'
851                 },
852                 { rows => 1 }
853             )->single;
854
855             ok( $credit_return, 'An account line of type LOST_FOUND is added' );
856
857             is(
858                 $account->balance,
859                 $manual_debit_amount - $payment_amount,
860                 'Balance is PROCESSING - payment (LOST_FOUND)'
861             );
862
863             my $manual_debit = Koha::Account::Lines->search(
864                 {
865                     borrowernumber  => $patron->id,
866                     debit_type_code => 'OVERDUE',
867                     status          => 'UNRETURNED'
868                 }
869             )->next;
870             is(
871                 $manual_debit->amountoutstanding + 0,
872                 $manual_debit_amount - $payment_amount,
873                 'reconcile_balance was called'
874             );
875           };
876
877         subtest 'Patron deleted' => sub {
878             plan tests => 1;
879
880             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
881             my $barcode            = 'KD123456794';
882             my $replacement_amount = 100;
883             my $processfee_amount  = 20;
884
885             my $item_type = $builder->build_object(
886                 {
887                     class => 'Koha::ItemTypes',
888                     value => {
889                         notforloan         => undef,
890                         rentalcharge       => 0,
891                         defaultreplacecost => undef,
892                         processfee         => 0,
893                         rentalcharge_daily => 0,
894                     }
895                 }
896             );
897             my $item = Koha::Item->new(
898                 {
899                     biblionumber     => $biblio->biblionumber,
900                     homebranch       => $library->branchcode,
901                     holdingbranch    => $library->branchcode,
902                     barcode          => $barcode,
903                     replacementprice => $replacement_amount,
904                     itype            => $item_type->itemtype
905                 },
906             )->store;
907
908             my $issue =
909               C4::Circulation::AddIssue( $patron->unblessed, $barcode );
910
911             # Simulate item marked as lost
912             $item->itemlost(1)->store;
913             C4::Circulation::LostItem( $item->itemnumber, 1 );
914
915             $issue->delete();
916             $patron->delete();
917
918             # Simulate item marked as found
919             $item->itemlost(0)->store;
920             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 0, 'No refund triggered' );
921
922         };
923
924         subtest 'restore fine | no overdue' => sub {
925
926             plan tests => 8;
927
928             my $manager =
929               $builder->build_object( { class => "Koha::Patrons" } );
930             t::lib::Mocks::mock_userenv(
931                 { patron => $manager, branchcode => $manager->branchcode } );
932
933             # Set lostreturn_policy to 'restore' for tests
934             my $specific_rule_restore = $builder->build(
935                 {
936                     source => 'CirculationRule',
937                     value  => {
938                         branchcode   => $manager->branchcode,
939                         categorycode => undef,
940                         itemtype     => undef,
941                         rule_name    => 'lostreturn',
942                         rule_value   => 'restore'
943                     }
944                 }
945             );
946
947             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
948
949             my $item = $builder->build_sample_item(
950                 {
951                     biblionumber     => $biblio->biblionumber,
952                     library          => $library->branchcode,
953                     replacementprice => $replacement_amount,
954                     itype            => $item_type->itemtype
955                 }
956             );
957
958             my $issue =
959               C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
960
961             # Simulate item marked as lost
962             $item->itemlost(1)->store;
963             C4::Circulation::LostItem( $item->itemnumber, 1 );
964
965             my $processing_fee_lines = Koha::Account::Lines->search(
966                 {
967                     borrowernumber  => $patron->id,
968                     itemnumber      => $item->itemnumber,
969                     debit_type_code => 'PROCESSING'
970                 }
971             );
972             is( $processing_fee_lines->count,
973                 1, 'Only one processing fee produced' );
974             my $processing_fee_line = $processing_fee_lines->next;
975             is( $processing_fee_line->amount + 0,
976                 $processfee_amount,
977                 'The right PROCESSING amount is generated' );
978             is( $processing_fee_line->amountoutstanding + 0,
979                 $processfee_amount,
980                 'The right PROCESSING amountoutstanding is generated' );
981
982             my $lost_fee_lines = Koha::Account::Lines->search(
983                 {
984                     borrowernumber  => $patron->id,
985                     itemnumber      => $item->itemnumber,
986                     debit_type_code => 'LOST'
987                 }
988             );
989             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
990             my $lost_fee_line = $lost_fee_lines->next;
991             is( $lost_fee_line->amount + 0,
992                 $replacement_amount, 'The right LOST amount is generated' );
993             is( $lost_fee_line->amountoutstanding + 0,
994                 $replacement_amount,
995                 'The right LOST amountountstanding is generated' );
996
997             my $account = $patron->account;
998             my $debts   = $account->outstanding_debits;
999
1000             # Pay off the debt
1001             my $credit = $account->add_credit(
1002                 {
1003                     amount    => $account->balance,
1004                     type      => 'PAYMENT',
1005                     interface => 'test',
1006                 }
1007             );
1008             $credit->apply( { debits => [ $debts->as_list ] } );
1009
1010             # Simulate item marked as found
1011             $item->itemlost(0)->store;
1012             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1013             is( scalar ( grep { $_->message eq 'lost_restored' } @{$item->object_messages} ), 0, 'Restore not triggered when there is no overdue fine found' );
1014         };
1015
1016         subtest 'restore fine | unforgiven overdue' => sub {
1017
1018             plan tests => 10;
1019
1020             # Set lostreturn_policy to 'restore' for tests
1021             my $manager =
1022               $builder->build_object( { class => "Koha::Patrons" } );
1023             t::lib::Mocks::mock_userenv(
1024                 { patron => $manager, branchcode => $manager->branchcode } );
1025             my $specific_rule_restore = $builder->build(
1026                 {
1027                     source => 'CirculationRule',
1028                     value  => {
1029                         branchcode   => $manager->branchcode,
1030                         categorycode => undef,
1031                         itemtype     => undef,
1032                         rule_name    => 'lostreturn',
1033                         rule_value   => 'restore'
1034                     }
1035                 }
1036             );
1037
1038             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1039
1040             my $item = $builder->build_sample_item(
1041                 {
1042                     biblionumber     => $biblio->biblionumber,
1043                     library          => $library->branchcode,
1044                     replacementprice => $replacement_amount,
1045                     itype            => $item_type->itemtype
1046                 }
1047             );
1048
1049             my $issue =
1050               C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
1051
1052             # Simulate item marked as lost
1053             $item->itemlost(1)->store;
1054             C4::Circulation::LostItem( $item->itemnumber, 1 );
1055
1056             my $processing_fee_lines = Koha::Account::Lines->search(
1057                 {
1058                     borrowernumber  => $patron->id,
1059                     itemnumber      => $item->itemnumber,
1060                     debit_type_code => 'PROCESSING'
1061                 }
1062             );
1063             is( $processing_fee_lines->count,
1064                 1, 'Only one processing fee produced' );
1065             my $processing_fee_line = $processing_fee_lines->next;
1066             is( $processing_fee_line->amount + 0,
1067                 $processfee_amount,
1068                 'The right PROCESSING amount is generated' );
1069             is( $processing_fee_line->amountoutstanding + 0,
1070                 $processfee_amount,
1071                 'The right PROCESSING amountoutstanding is generated' );
1072
1073             my $lost_fee_lines = Koha::Account::Lines->search(
1074                 {
1075                     borrowernumber  => $patron->id,
1076                     itemnumber      => $item->itemnumber,
1077                     debit_type_code => 'LOST'
1078                 }
1079             );
1080             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1081             my $lost_fee_line = $lost_fee_lines->next;
1082             is( $lost_fee_line->amount + 0,
1083                 $replacement_amount, 'The right LOST amount is generated' );
1084             is( $lost_fee_line->amountoutstanding + 0,
1085                 $replacement_amount,
1086                 'The right LOST amountountstanding is generated' );
1087
1088             my $account = $patron->account;
1089             my $debts   = $account->outstanding_debits;
1090
1091             # Pay off the debt
1092             my $credit = $account->add_credit(
1093                 {
1094                     amount    => $account->balance,
1095                     type      => 'PAYMENT',
1096                     interface => 'test',
1097                 }
1098             );
1099             $credit->apply( { debits => [ $debts->as_list ] } );
1100
1101             # Fine not forgiven
1102             my $overdue = $account->add_debit(
1103                 {
1104                     amount     => 30.00,
1105                     user_id    => $manager->borrowernumber,
1106                     library_id => $library->branchcode,
1107                     interface  => 'test',
1108                     item_id    => $item->itemnumber,
1109                     type       => 'OVERDUE',
1110                 }
1111             )->store();
1112             $overdue->status('LOST')->store();
1113             $overdue->discard_changes;
1114             is( $overdue->status, 'LOST',
1115                 'Overdue status set to LOST' );
1116
1117             # Simulate item marked as found
1118             $item->itemlost(0)->store;
1119             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1120             is( scalar ( grep { $_->message eq 'lost_restored' } @{$item->object_messages} ), 0, 'Restore not triggered when overdue was not forgiven' );
1121             $overdue->discard_changes;
1122             is( $overdue->status, 'FOUND',
1123                 'Overdue status updated to FOUND' );
1124         };
1125
1126         subtest 'restore fine | forgiven overdue' => sub {
1127
1128             plan tests => 12;
1129
1130             # Set lostreturn_policy to 'restore' for tests
1131             my $manager =
1132               $builder->build_object( { class => "Koha::Patrons" } );
1133             t::lib::Mocks::mock_userenv(
1134                 { patron => $manager, branchcode => $manager->branchcode } );
1135             my $specific_rule_restore = $builder->build(
1136                 {
1137                     source => 'CirculationRule',
1138                     value  => {
1139                         branchcode   => $manager->branchcode,
1140                         categorycode => undef,
1141                         itemtype     => undef,
1142                         rule_name    => 'lostreturn',
1143                         rule_value   => 'restore'
1144                     }
1145                 }
1146             );
1147
1148             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1149
1150             my $item = $builder->build_sample_item(
1151                 {
1152                     biblionumber     => $biblio->biblionumber,
1153                     library          => $library->branchcode,
1154                     replacementprice => $replacement_amount,
1155                     itype            => $item_type->itemtype
1156                 }
1157             );
1158
1159             my $issue =
1160               C4::Circulation::AddIssue( $patron->unblessed, $item->barcode );
1161
1162             # Simulate item marked as lost
1163             $item->itemlost(1)->store;
1164             C4::Circulation::LostItem( $item->itemnumber, 1 );
1165
1166             my $processing_fee_lines = Koha::Account::Lines->search(
1167                 {
1168                     borrowernumber  => $patron->id,
1169                     itemnumber      => $item->itemnumber,
1170                     debit_type_code => 'PROCESSING'
1171                 }
1172             );
1173             is( $processing_fee_lines->count,
1174                 1, 'Only one processing fee produced' );
1175             my $processing_fee_line = $processing_fee_lines->next;
1176             is( $processing_fee_line->amount + 0,
1177                 $processfee_amount,
1178                 'The right PROCESSING amount is generated' );
1179             is( $processing_fee_line->amountoutstanding + 0,
1180                 $processfee_amount,
1181                 'The right PROCESSING amountoutstanding is generated' );
1182
1183             my $lost_fee_lines = Koha::Account::Lines->search(
1184                 {
1185                     borrowernumber  => $patron->id,
1186                     itemnumber      => $item->itemnumber,
1187                     debit_type_code => 'LOST'
1188                 }
1189             );
1190             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1191             my $lost_fee_line = $lost_fee_lines->next;
1192             is( $lost_fee_line->amount + 0,
1193                 $replacement_amount, 'The right LOST amount is generated' );
1194             is( $lost_fee_line->amountoutstanding + 0,
1195                 $replacement_amount,
1196                 'The right LOST amountountstanding is generated' );
1197
1198             my $account = $patron->account;
1199             my $debts   = $account->outstanding_debits;
1200
1201             # Pay off the debt
1202             my $credit = $account->add_credit(
1203                 {
1204                     amount    => $account->balance,
1205                     type      => 'PAYMENT',
1206                     interface => 'test',
1207                 }
1208             );
1209             $credit->apply( { debits => [ $debts->as_list ] } );
1210
1211             # Add overdue
1212             my $overdue = $account->add_debit(
1213                 {
1214                     amount     => 30.00,
1215                     user_id    => $manager->borrowernumber,
1216                     library_id => $library->branchcode,
1217                     interface  => 'test',
1218                     item_id    => $item->itemnumber,
1219                     type       => 'OVERDUE',
1220                 }
1221             )->store();
1222             $overdue->status('LOST')->store();
1223             is( $overdue->status, 'LOST',
1224                 'Overdue status set to LOST' );
1225
1226             t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 0 );
1227
1228             # Forgive fine
1229             $credit = $account->add_credit(
1230                 {
1231                     amount     => 30.00,
1232                     user_id    => $manager->borrowernumber,
1233                     library_id => $library->branchcode,
1234                     interface  => 'test',
1235                     type       => 'FORGIVEN',
1236                     item_id    => $item->itemnumber
1237                 }
1238             );
1239             $credit->apply( { debits => [$overdue] } );
1240
1241             # Simulate item marked as found
1242             $item->itemlost(0)->store;
1243             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1244             is( scalar ( grep { $_->message eq 'lost_restored' } @{$item->object_messages} ), 1, 'Restore triggered when overdue was forgiven' );
1245             $overdue->discard_changes;
1246             is( $overdue->status, 'FOUND', 'Overdue status updated to FOUND' );
1247             is( $overdue->amountoutstanding, $overdue->amount, 'Overdue outstanding has been restored' );
1248             $credit->discard_changes;
1249             is( $credit->status, 'VOID', 'Overdue Forgival has been marked as VOID');
1250         };
1251
1252         subtest 'Continue when userenv is not set' => sub {
1253             plan tests => 1;
1254
1255             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1256             my $barcode            = 'KD123456795';
1257             my $replacement_amount = 100;
1258             my $processfee_amount  = 20;
1259
1260             my $item_type = $builder->build_object(
1261                 {
1262                     class => 'Koha::ItemTypes',
1263                     value => {
1264                         notforloan         => undef,
1265                         rentalcharge       => 0,
1266                         defaultreplacecost => undef,
1267                         processfee         => 0,
1268                         rentalcharge_daily => 0,
1269                     }
1270                 }
1271             );
1272             my $item = $builder->build_sample_item(
1273                 {
1274                     biblionumber     => $biblio->biblionumber,
1275                     homebranch       => $library->branchcode,
1276                     holdingbranch    => $library->branchcode,
1277                     barcode          => $barcode,
1278                     replacementprice => $replacement_amount,
1279                     itype            => $item_type->itemtype
1280                 }
1281             );
1282
1283             my $issue =
1284               C4::Circulation::AddIssue( $patron->unblessed, $barcode );
1285
1286             # Simulate item marked as lost
1287             $item->itemlost(1)->store;
1288             C4::Circulation::LostItem( $item->itemnumber, 1 );
1289
1290             # Unset the userenv
1291             C4::Context->_new_userenv(undef);
1292
1293             # Simluate item marked as found
1294             $item->itemlost(0)->store;
1295             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1296
1297         };
1298     };
1299
1300     subtest 'log_action' => sub {
1301         plan tests => 2;
1302         t::lib::Mocks::mock_preference( 'CataloguingLog', 1 );
1303
1304         my $item = Koha::Item->new(
1305             {
1306                 homebranch    => $library->{branchcode},
1307                 holdingbranch => $library->{branchcode},
1308                 biblionumber  => $biblio->biblionumber,
1309                 location      => 'my_loc',
1310             }
1311         )->store;
1312         is(
1313             Koha::ActionLogs->search(
1314                 {
1315                     module => 'CATALOGUING',
1316                     action => 'ADD',
1317                     object => $item->itemnumber,
1318                     info   => 'item'
1319                 }
1320             )->count,
1321             1,
1322             "Item creation logged"
1323         );
1324
1325         $item->location('another_loc')->store;
1326         is(
1327             Koha::ActionLogs->search(
1328                 {
1329                     module => 'CATALOGUING',
1330                     action => 'MODIFY',
1331                     object => $item->itemnumber
1332                 }
1333             )->count,
1334             1,
1335             "Item modification logged"
1336         );
1337     };
1338 };
1339
1340 subtest 'get_transfer' => sub {
1341     plan tests => 7;
1342
1343     my $transfer = $new_item_1->get_transfer();
1344     is( $transfer, undef, 'Koha::Item->get_transfer should return undef if the item is not in transit' );
1345
1346     my $library_to = $builder->build( { source => 'Branch' } );
1347
1348     my $transfer_1 = $builder->build_object(
1349         {
1350             class => 'Koha::Item::Transfers',
1351             value => {
1352                 itemnumber    => $new_item_1->itemnumber,
1353                 frombranch    => $new_item_1->holdingbranch,
1354                 tobranch      => $library_to->{branchcode},
1355                 reason        => 'Manual',
1356                 datesent      => undef,
1357                 datearrived   => undef,
1358                 datecancelled => undef,
1359                 daterequested => \'NOW()'
1360             }
1361         }
1362     );
1363
1364     $transfer = $new_item_1->get_transfer();
1365     is( ref($transfer), 'Koha::Item::Transfer', 'Koha::Item->get_transfer should return a Koha::Item::Transfers object' );
1366
1367     my $transfer_2 = $builder->build_object(
1368         {
1369             class => 'Koha::Item::Transfers',
1370             value => {
1371                 itemnumber    => $new_item_1->itemnumber,
1372                 frombranch    => $new_item_1->holdingbranch,
1373                 tobranch      => $library_to->{branchcode},
1374                 reason        => 'Manual',
1375                 datesent      => undef,
1376                 datearrived   => undef,
1377                 datecancelled => undef,
1378                 daterequested => \'NOW()'
1379             }
1380         }
1381     );
1382
1383     $transfer = $new_item_1->get_transfer();
1384     is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the oldest transfer request');
1385
1386     $transfer_2->datesent(\'NOW()')->store;
1387     $transfer = $new_item_1->get_transfer();
1388     is( $transfer->branchtransfer_id, $transfer_2->branchtransfer_id, 'Koha::Item->get_transfer returns the in_transit transfer');
1389
1390     my $transfer_3 = $builder->build_object(
1391         {
1392             class => 'Koha::Item::Transfers',
1393             value => {
1394                 itemnumber    => $new_item_1->itemnumber,
1395                 frombranch    => $new_item_1->holdingbranch,
1396                 tobranch      => $library_to->{branchcode},
1397                 reason        => 'Manual',
1398                 datesent      => undef,
1399                 datearrived   => undef,
1400                 datecancelled => undef,
1401                 daterequested => \'NOW()'
1402             }
1403         }
1404     );
1405
1406     $transfer_2->datearrived(\'NOW()')->store;
1407     $transfer = $new_item_1->get_transfer();
1408     is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the next queued transfer');
1409     is( $transfer->itemnumber, $new_item_1->itemnumber, 'Koha::Item->get_transfer returns the right items transfer' );
1410
1411     $transfer_1->datecancelled(\'NOW()')->store;
1412     $transfer = $new_item_1->get_transfer();
1413     is( $transfer->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfer ignores cancelled transfers');
1414 };
1415
1416 subtest 'holds' => sub {
1417     plan tests => 5;
1418
1419     my $biblio = $builder->build_sample_biblio();
1420     my $item   = $builder->build_sample_item({
1421         biblionumber => $biblio->biblionumber,
1422     });
1423     is($item->holds->count, 0, "Nothing returned if no holds");
1424     my $hold1 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'T' }});
1425     my $hold2 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1426     my $hold3 = $builder->build({ source => 'Reserve', value => { itemnumber=>$item->itemnumber, found => 'W' }});
1427
1428     is($item->holds()->count,3,"Three holds found");
1429     is($item->holds({found => 'W'})->count,2,"Two waiting holds found");
1430     is_deeply($item->holds({found => 'T'})->next->unblessed,$hold1,"Found transit holds matches the hold");
1431     is($item->holds({found => undef})->count, 0,"Nothing returned if no matching holds");
1432 };
1433
1434 subtest 'biblio' => sub {
1435     plan tests => 2;
1436
1437     my $biblio = $retrieved_item_1->biblio;
1438     is( ref( $biblio ), 'Koha::Biblio', 'Koha::Item->biblio should return a Koha::Biblio' );
1439     is( $biblio->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblio should return the correct biblio' );
1440 };
1441
1442 subtest 'biblioitem' => sub {
1443     plan tests => 2;
1444
1445     my $biblioitem = $retrieved_item_1->biblioitem;
1446     is( ref( $biblioitem ), 'Koha::Biblioitem', 'Koha::Item->biblioitem should return a Koha::Biblioitem' );
1447     is( $biblioitem->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblioitem should return the correct biblioitem' );
1448 };
1449
1450 # Restore userenv
1451 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
1452 subtest 'checkout' => sub {
1453     plan tests => 5;
1454     my $item = Koha::Items->find( $new_item_1->itemnumber );
1455     # No checkout yet
1456     my $checkout = $item->checkout;
1457     is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no current checkout on this item' );
1458
1459     # Add a checkout
1460     my $patron = $builder->build({ source => 'Borrower' });
1461     C4::Circulation::AddIssue( $patron, $item->barcode );
1462     $checkout = $retrieved_item_1->checkout;
1463     is( ref( $checkout ), 'Koha::Checkout', 'Koha::Item->checkout should return a Koha::Checkout' );
1464     is( $checkout->itemnumber, $item->itemnumber, 'Koha::Item->checkout should return the correct checkout' );
1465     is( $checkout->borrowernumber, $patron->{borrowernumber}, 'Koha::Item->checkout should return the correct checkout' );
1466
1467     # Do the return
1468     C4::Circulation::AddReturn( $item->barcode );
1469
1470     # There is no more checkout on this item, making sure it will not return old checkouts
1471     $checkout = $item->checkout;
1472     is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no *current* checkout on this item' );
1473 };
1474
1475 subtest 'can_be_transferred' => sub {
1476     plan tests => 5;
1477
1478     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
1479     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
1480
1481     my $biblio   = $builder->build_sample_biblio();
1482     my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
1483     my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
1484     my $item  = $builder->build_sample_item({
1485         biblionumber     => $biblio->biblionumber,
1486         homebranch       => $library1->branchcode,
1487         holdingbranch    => $library1->branchcode,
1488     });
1489
1490     is(Koha::Item::Transfer::Limits->search({
1491         fromBranch => $library1->branchcode,
1492         toBranch => $library2->branchcode,
1493     })->count, 0, 'There are no transfer limits between libraries.');
1494     ok($item->can_be_transferred({ to => $library2 }),
1495        'Item can be transferred between libraries.');
1496
1497     my $limit = Koha::Item::Transfer::Limit->new({
1498         fromBranch => $library1->branchcode,
1499         toBranch => $library2->branchcode,
1500         itemtype => $item->effective_itemtype,
1501     })->store;
1502     is(Koha::Item::Transfer::Limits->search({
1503         fromBranch => $library1->branchcode,
1504         toBranch => $library2->branchcode,
1505     })->count, 1, 'Given we have added a transfer limit,');
1506     is($item->can_be_transferred({ to => $library2 }), 0,
1507        'Item can no longer be transferred between libraries.');
1508     is($item->can_be_transferred({ to => $library2, from => $library1 }), 0,
1509        'We get the same result also if we pass the from-library parameter.');
1510 };
1511
1512 subtest 'filter_by_for_hold' => sub {
1513     plan tests => 12;
1514
1515     my $biblio = $builder->build_sample_biblio;
1516     is( $biblio->items->filter_by_for_hold->count, 0, 'no item yet' );
1517     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 1 } );
1518     is( $biblio->items->filter_by_for_hold->count, 0, 'no item for hold' );
1519     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 0 } );
1520     is( $biblio->items->filter_by_for_hold->count, 1, '1 item for hold' );
1521     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => -1 } );
1522     is( $biblio->items->filter_by_for_hold->count, 2, '2 items for hold' );
1523
1524     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 0 } );
1525     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 1 } );
1526     is( $biblio->items->filter_by_for_hold->count, 3, '3 items for hold - itemlost' );
1527
1528     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 0 } );
1529     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 1 } );
1530     is( $biblio->items->filter_by_for_hold->count, 4, '4 items for hold - withdrawn' );
1531
1532     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, damaged => 0 } );
1533     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, damaged => 1 } );
1534     t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0);
1535     is( $biblio->items->filter_by_for_hold->count, 5, '5 items for hold - not damaged if not AllowHoldsOnDamagedItems' );
1536     t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1);
1537     is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - damaged if AllowHoldsOnDamagedItems' );
1538
1539     my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' });
1540     my $not_holdable_itemtype = $itemtype->itemtype;
1541     $builder->build_sample_item(
1542         {
1543             biblionumber => $biblio->biblionumber,
1544             itype        => $not_holdable_itemtype,
1545         }
1546     );
1547     Koha::CirculationRules->set_rule(
1548         {
1549             branchcode   => undef,
1550             itemtype     => $not_holdable_itemtype,
1551             rule_name    => 'holdallowed',
1552             rule_value   => 'not_allowed',
1553         }
1554     );
1555     is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - holdallowed=not_allowed' );
1556
1557     # Remove rule, test notforloan on itemtype
1558     Koha::CirculationRules->set_rule(
1559         {
1560             branchcode   => undef,
1561             itemtype     => $not_holdable_itemtype,
1562             rule_name    => 'holdallowed',
1563             rule_value   => undef,
1564         }
1565     );
1566     is( $biblio->items->filter_by_for_hold->count, 7, '7 items for hold - rule deleted' );
1567     $itemtype->notforloan(1)->store;
1568     is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - notforloan' );
1569
1570     t::lib::Mocks::mock_preference('item-level_itypes', 0);
1571     $biblio->biblioitem->itemtype($not_holdable_itemtype)->store;
1572     is( $biblio->items->filter_by_for_hold->count, 0, '0 item-level_itypes=0' );
1573
1574     t::lib::Mocks::mock_preference('item-level_itypes', 1);
1575
1576     $biblio->delete;
1577 };
1578
1579 # Reset nb_of_items prior to testing delete
1580 $nb_of_items = Koha::Items->search->count;
1581
1582 # Test delete
1583 $retrieved_item_1->delete;
1584 is( Koha::Items->search->count, $nb_of_items - 1, 'Delete should have deleted the item' );
1585
1586 $schema->storage->txn_rollback;
1587
1588 subtest 'filter_by_visible_in_opac() tests' => sub {
1589
1590     plan tests => 12;
1591
1592     $schema->storage->txn_begin;
1593
1594     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1595     my $mocked_category = Test::MockModule->new('Koha::Patron::Category');
1596     my $exception = 1;
1597     $mocked_category->mock( 'override_hidden_items', sub {
1598         return $exception;
1599     });
1600
1601     # have a fresh biblio
1602     my $biblio = $builder->build_sample_biblio;
1603     # have two itemtypes
1604     my $itype_1 = $builder->build_object({ class => 'Koha::ItemTypes' });
1605     my $itype_2 = $builder->build_object({ class => 'Koha::ItemTypes' });
1606     # have 5 items on that biblio
1607     my $item_1 = $builder->build_sample_item(
1608         {
1609             biblionumber => $biblio->biblionumber,
1610             itemlost     => -1,
1611             itype        => $itype_1->itemtype,
1612             withdrawn    => 1,
1613             copynumber   => undef
1614         }
1615     );
1616     my $item_2 = $builder->build_sample_item(
1617         {
1618             biblionumber => $biblio->biblionumber,
1619             itemlost     => 0,
1620             itype        => $itype_2->itemtype,
1621             withdrawn    => 2,
1622             copynumber   => undef
1623         }
1624     );
1625     my $item_3 = $builder->build_sample_item(
1626         {
1627             biblionumber => $biblio->biblionumber,
1628             itemlost     => 1,
1629             itype        => $itype_1->itemtype,
1630             withdrawn    => 3,
1631             copynumber   => undef
1632         }
1633     );
1634     my $item_4 = $builder->build_sample_item(
1635         {
1636             biblionumber => $biblio->biblionumber,
1637             itemlost     => 0,
1638             itype        => $itype_2->itemtype,
1639             withdrawn    => 4,
1640             copynumber   => undef
1641         }
1642     );
1643     my $item_5 = $builder->build_sample_item(
1644         {
1645             biblionumber => $biblio->biblionumber,
1646             itemlost     => 0,
1647             itype        => $itype_1->itemtype,
1648             withdrawn    => 5,
1649             copynumber   => undef
1650         }
1651     );
1652     my $item_6 = $builder->build_sample_item(
1653         {
1654             biblionumber => $biblio->biblionumber,
1655             itemlost     => 2,
1656             itype        => $itype_1->itemtype,
1657             withdrawn    => 5,
1658             copynumber   => undef
1659         }
1660     );
1661
1662     my $rules = undef;
1663
1664     my $mocked_context = Test::MockModule->new('C4::Context');
1665     $mocked_context->mock( 'yaml_preference', sub {
1666         return $rules;
1667     });
1668
1669     t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
1670     is( $biblio->items->filter_by_visible_in_opac->count,
1671         6, 'No rules passed, hidelostitems unset' );
1672
1673     is( $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1674         6, 'No rules passed, hidelostitems unset, patron exception changes nothing' );
1675
1676     $rules = { copynumber => [ 2 ] };
1677
1678     t::lib::Mocks::mock_preference( 'hidelostitems', 1 );
1679     is(
1680         $biblio->items->filter_by_visible_in_opac->count,
1681         3,
1682         'No rules passed, hidelostitems set'
1683     );
1684
1685     is(
1686         $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1687         3,
1688         'No rules passed, hidelostitems set, patron exception changes nothing'
1689     );
1690
1691     $rules = { withdrawn => [ 1, 2 ], copynumber => [ 2 ] };
1692     is(
1693         $biblio->items->filter_by_visible_in_opac->count,
1694         2,
1695         'Rules on withdrawn, hidelostitems set'
1696     );
1697
1698     is(
1699         $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1700         3,
1701         'hidelostitems set, rules on withdrawn but patron override passed'
1702     );
1703
1704     $rules = { itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1705     is(
1706         $biblio->items->filter_by_visible_in_opac->count,
1707         2,
1708         'Rules on itype, hidelostitems set'
1709     );
1710
1711     $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1712     is(
1713         $biblio->items->filter_by_visible_in_opac->count,
1714         1,
1715         'Rules on itype and withdrawn, hidelostitems set'
1716     );
1717     is(
1718         $biblio->items->filter_by_visible_in_opac
1719           ->next->itemnumber,
1720         $item_4->itemnumber,
1721         'The right item is returned'
1722     );
1723
1724     $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_2->itemtype ], copynumber => [ 2 ] };
1725     is(
1726         $biblio->items->filter_by_visible_in_opac->count,
1727         1,
1728         'Rules on itype and withdrawn, hidelostitems set'
1729     );
1730     is(
1731         $biblio->items->filter_by_visible_in_opac
1732           ->next->itemnumber,
1733         $item_5->itemnumber,
1734         'The right item is returned'
1735     );
1736
1737     # Make sure the warning on the about page will work
1738     $rules = { itemlost => ['AB'] };
1739     my $c = Koha::Items->filter_by_visible_in_opac->count;
1740     my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
1741     is( $warnings[2], q{Truncated incorrect DOUBLE value: 'AB'});
1742
1743     $schema->storage->txn_rollback;
1744 };
1745
1746 subtest 'filter_out_lost() tests' => sub {
1747
1748     plan tests => 2;
1749
1750     $schema->storage->txn_begin;
1751
1752     # have a fresh biblio
1753     my $biblio = $builder->build_sample_biblio;
1754     # have 3 items on that biblio
1755     my $item_1 = $builder->build_sample_item(
1756         {
1757             biblionumber => $biblio->biblionumber,
1758             itemlost     => -1,
1759         }
1760     );
1761     my $item_2 = $builder->build_sample_item(
1762         {
1763             biblionumber => $biblio->biblionumber,
1764             itemlost     => 0,
1765         }
1766     );
1767     my $item_3 = $builder->build_sample_item(
1768         {
1769             biblionumber => $biblio->biblionumber,
1770             itemlost     => 1,
1771         }
1772     );
1773
1774     is( $biblio->items->filter_out_lost->next->itemnumber, $item_2->itemnumber, 'Right item returned' );
1775     is( $biblio->items->filter_out_lost->count, 1, 'Only one item is not lost' );
1776
1777     $schema->storage->txn_rollback;
1778 };
1779
1780 subtest 'move_to_biblio() tests' => sub {
1781
1782     plan tests => 2;
1783
1784     $schema->storage->txn_begin;
1785
1786     my $biblio1 = $builder->build_sample_biblio;
1787     my $biblio2 = $builder->build_sample_biblio;
1788     my $item1 = $builder->build_sample_item({ biblionumber => $biblio1->biblionumber });
1789     my $item2 = $builder->build_sample_item({ biblionumber => $biblio1->biblionumber });
1790
1791     $biblio1->items->move_to_biblio($biblio2);
1792
1793     $item1->discard_changes;
1794     $item2->discard_changes;
1795
1796     is($item1->biblionumber, $biblio2->biblionumber, "Item 1 moved");
1797     is($item2->biblionumber, $biblio2->biblionumber, "Item 2 moved");
1798
1799     $schema->storage->txn_rollback;
1800
1801 };