Bug 36687: (RM follow-up) Fix unit tests
[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 => 20;
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 C4::Serials qw( NewIssue AddItem2Serial );
31 use Koha::Item;
32 use Koha::Item::Transfer::Limits;
33 use Koha::Items;
34 use Koha::Database;
35 use Koha::DateUtils qw( dt_from_string );
36 use Koha::Statistics;
37
38 use t::lib::TestBuilder;
39 use t::lib::Mocks;
40 use t::lib::Dates;
41
42 my $schema = Koha::Database->new->schema;
43 $schema->storage->txn_begin;
44
45 my $dbh     = C4::Context->dbh;
46
47 my $builder     = t::lib::TestBuilder->new;
48 my $library     = $builder->build( { source => 'Branch' } );
49 my $nb_of_items = Koha::Items->search->count;
50 my $biblio      = $builder->build_sample_biblio();
51 my $new_item_1   = $builder->build_sample_item({
52     biblionumber => $biblio->biblionumber,
53     homebranch       => $library->{branchcode},
54     holdingbranch    => $library->{branchcode},
55 });
56 my $new_item_2   = $builder->build_sample_item({
57     biblionumber => $biblio->biblionumber,
58     homebranch       => $library->{branchcode},
59     holdingbranch    => $library->{branchcode},
60 });
61
62
63 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
64
65 like( $new_item_1->itemnumber, qr|^\d+$|, 'Adding a new item should have set the itemnumber' );
66 is( Koha::Items->search->count, $nb_of_items + 2, 'The 2 items should have been added' );
67
68 my $retrieved_item_1 = Koha::Items->find( $new_item_1->itemnumber );
69 is( $retrieved_item_1->barcode, $new_item_1->barcode, 'Find a item by id should return the correct item' );
70
71 subtest 'store' => sub {
72     plan tests => 8;
73
74     my $biblio = $builder->build_sample_biblio;
75     my $today  = dt_from_string->set( hour => 0, minute => 0, second => 0 );
76     my $item   = Koha::Item->new(
77         {
78             homebranch    => $library->{branchcode},
79             holdingbranch => $library->{branchcode},
80             biblionumber  => $biblio->biblionumber,
81             location      => 'my_loc',
82         }
83     )->store->get_from_storage;
84
85     is( t::lib::Dates::compare( $item->replacementpricedate, $today ),
86         0, 'replacementpricedate must have been set to today if not given' );
87     is( t::lib::Dates::compare( dt_from_string($item->datelastseen)->ymd, $today ),
88         0, 'datelastseen must have been set to today if not given' );
89     is(
90         $item->itype,
91         $biblio->biblioitem->itemtype,
92         'items.itype must have been set to biblioitem.itemtype is not given'
93     );
94     $item->delete;
95
96     subtest 'permanent_location' => sub {
97         plan tests => 2;
98
99         subtest 'location passed to ->store' => sub {
100             plan tests => 7;
101
102             my $location = 'my_loc';
103             my $attributes = {
104                 homebranch    => $library->{branchcode},
105                 holdingbranch => $library->{branchcode},
106                 biblionumber  => $biblio->biblionumber,
107                 location      => $location,
108             };
109
110             {
111                 # NewItemsDefaultLocation not set
112                 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', '' );
113
114                 # Not passing permanent_location on creating the item
115                 my $item = Koha::Item->new($attributes)->store->get_from_storage;
116                 is( $item->location, $location,
117                     'location must have been set to location if given' );
118                 is( $item->permanent_location, $item->location,
119                     'permanent_location must have been set to location if not given' );
120                 $item->delete;
121
122                 # Passing permanent_location on creating the item
123                 $item = Koha::Item->new(
124                     { %$attributes, permanent_location => 'perm_loc' } )
125                   ->store->get_from_storage;
126                 is( $item->permanent_location, 'perm_loc',
127                     'permanent_location must have been kept if given' );
128                 $item->delete;
129             }
130
131             {
132                 # NewItemsDefaultLocation set
133                 my $default_location = 'default_location';
134                 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', $default_location );
135
136                 # Not passing permanent_location on creating the item
137                 my $item = Koha::Item->new($attributes)->store->get_from_storage;
138                 is( $item->location, $location,
139                     'location must have been kept if given' );
140                 is( $item->permanent_location, $location,
141                     'permanent_location must have been set to the location given' );
142                 $item->delete;
143
144                 # Passing permanent_location on creating the item
145                 $item = Koha::Item->new(
146                     { %$attributes, permanent_location => 'perm_loc' } )
147                   ->store->get_from_storage;
148                 is( $item->location, $location,
149                     'location must have been kept if given' );
150                 is( $item->permanent_location, 'perm_loc',
151                     'permanent_location must have been kept if given' );
152                 $item->delete;
153             }
154         };
155
156         subtest 'location NOT passed to ->store' => sub {
157             plan tests => 7;
158
159             my $attributes = {
160                 homebranch    => $library->{branchcode},
161                 holdingbranch => $library->{branchcode},
162                 biblionumber  => $biblio->biblionumber,
163             };
164
165             {
166                 # NewItemsDefaultLocation not set
167                 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', '' );
168
169                 # Not passing permanent_location on creating the item
170                 my $item = Koha::Item->new($attributes)->store->get_from_storage;
171                 is( $item->location, undef,
172                     'location not passed and no default, it is undef' );
173                 is( $item->permanent_location, $item->location,
174                     'permanent_location must have been set to location if not given' );
175                 $item->delete;
176
177                 # Passing permanent_location on creating the item
178                 $item = Koha::Item->new(
179                     { %$attributes, permanent_location => 'perm_loc' } )
180                   ->store->get_from_storage;
181                 is( $item->permanent_location, 'perm_loc',
182                     'permanent_location must have been kept if given' );
183                 $item->delete;
184             }
185
186             {
187                 # NewItemsDefaultLocation set
188                 my $default_location = 'default_location';
189                 t::lib::Mocks::mock_preference( 'NewItemsDefaultLocation', $default_location );
190
191                 # Not passing permanent_location on creating the item
192                 my $item = Koha::Item->new($attributes)->store->get_from_storage;
193                 is( $item->location, $default_location,
194                     'location must have been set to default location if not given' );
195                 is( $item->permanent_location, $default_location,
196                     'permanent_location must have been set to the default location as well' );
197                 $item->delete;
198
199                 # Passing permanent_location on creating the item
200                 $item = Koha::Item->new(
201                     { %$attributes, permanent_location => 'perm_loc' } )
202                   ->store->get_from_storage;
203                 is( $item->location, $default_location,
204                     'location must have been set to default location if not given' );
205                 is( $item->permanent_location, 'perm_loc',
206                     'permanent_location must have been kept if given' );
207                 $item->delete;
208             }
209         };
210
211     };
212
213     subtest '*_on updates' => sub {
214         plan tests => 9;
215
216         # Once the '_on' value is set (triggered by the related field turning from false to true)
217         # it should not be re-set for any changes outside of the related field being 'unset'.
218
219         my @fields = qw( itemlost withdrawn damaged );
220         my $today = dt_from_string();
221         my $yesterday = $today->clone()->subtract( days => 1 );
222
223         for my $field ( @fields ) {
224             my $item = $builder->build_sample_item(
225                 {
226                     itemlost     => 0,
227                     itemlost_on  => undef,
228                     withdrawn    => 0,
229                     withdrawn_on => undef,
230                     damaged      => 0,
231                     damaged_on   => undef
232                 }
233             );
234             my $field_on = $field . '_on';
235
236             # Set field for the first time
237             Time::Fake->offset( $yesterday->epoch );
238             $item->$field(1)->store;
239             $item->get_from_storage;
240             is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
241                 0, $field_on . " was set upon first truthy setting" );
242
243             # Update the field to a new 'true' value
244             Time::Fake->offset( $today->epoch );
245             $item->$field(2)->store;
246             $item->get_from_storage;
247             is( t::lib::Dates::compare( $item->$field_on, $yesterday ),
248                 0, $field_on . " was not updated upon second truthy setting" );
249
250             # Update the field to a new 'false' value
251             $item->$field(0)->store;
252             $item->get_from_storage;
253             is($item->$field_on, undef, $field_on . " was unset upon untruthy setting");
254
255             Time::Fake->reset;
256         }
257     };
258
259     subtest '_lost_found_trigger' => sub {
260         plan tests => 10;
261
262         t::lib::Mocks::mock_preference( 'WhenLostChargeReplacementFee', 1 );
263         t::lib::Mocks::mock_preference( 'WhenLostForgiveFine',          0 );
264
265         my $processfee_amount  = 20;
266         my $replacement_amount = 99.00;
267         my $item_type          = $builder->build_object(
268             {
269                 class => 'Koha::ItemTypes',
270                 value => {
271                     notforloan         => 0,
272                     rentalcharge       => 0,
273                     defaultreplacecost => undef,
274                     processfee         => $processfee_amount,
275                     rentalcharge_daily => 0,
276                 }
277             }
278         );
279         my $library = $builder->build_object( { class => 'Koha::Libraries' } );
280
281         $biblio = $builder->build_sample_biblio( { author => 'Hall, Daria' } );
282
283         subtest 'Full write-off tests' => sub {
284
285             plan tests => 12;
286
287             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
288             my $manager =
289               $builder->build_object( { class => "Koha::Patrons" } );
290             t::lib::Mocks::mock_userenv(
291                 { patron => $manager, branchcode => $manager->branchcode } );
292
293             my $item = $builder->build_sample_item(
294                 {
295                     biblionumber     => $biblio->biblionumber,
296                     library          => $library->branchcode,
297                     replacementprice => $replacement_amount,
298                     itype            => $item_type->itemtype,
299                 }
300             );
301
302             C4::Circulation::AddIssue( $patron, $item->barcode );
303
304             # Simulate item marked as lost
305             $item->itemlost(3)->store;
306             C4::Circulation::LostItem( $item->itemnumber, 1 );
307
308             my $processing_fee_lines = Koha::Account::Lines->search(
309                 {
310                     borrowernumber  => $patron->id,
311                     itemnumber      => $item->itemnumber,
312                     debit_type_code => 'PROCESSING'
313                 }
314             );
315             is( $processing_fee_lines->count,
316                 1, 'Only one processing fee produced' );
317             my $processing_fee_line = $processing_fee_lines->next;
318             is( $processing_fee_line->amount + 0,
319                 $processfee_amount,
320                 'The right PROCESSING amount is generated' );
321             is( $processing_fee_line->amountoutstanding + 0,
322                 $processfee_amount,
323                 'The right PROCESSING amountoutstanding is generated' );
324
325             my $lost_fee_lines = Koha::Account::Lines->search(
326                 {
327                     borrowernumber  => $patron->id,
328                     itemnumber      => $item->itemnumber,
329                     debit_type_code => 'LOST'
330                 }
331             );
332             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
333             my $lost_fee_line = $lost_fee_lines->next;
334             is( $lost_fee_line->amount + 0,
335                 $replacement_amount, 'The right LOST amount is generated' );
336             is( $lost_fee_line->amountoutstanding + 0,
337                 $replacement_amount,
338                 'The right LOST amountoutstanding is generated' );
339             is( $lost_fee_line->status, undef, 'The LOST status was not set' );
340
341             my $account = $patron->account;
342             my $debts   = $account->outstanding_debits;
343
344             # Write off the debt
345             my $credit = $account->add_credit(
346                 {
347                     amount    => $account->balance,
348                     type      => 'WRITEOFF',
349                     interface => 'test',
350                 }
351             );
352             $credit->apply( { debits => [ $debts->as_list ] } );
353
354             # Simulate item marked as found
355             $item->itemlost(0)->store;
356             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 0, 'No LOST_FOUND account line added' );
357
358             $lost_fee_line->discard_changes;    # reload from DB
359             is( $lost_fee_line->amountoutstanding + 0,
360                 0, 'Lost fee has no outstanding amount' );
361             is( $lost_fee_line->debit_type_code,
362                 'LOST', 'Lost fee now still has account type of LOST' );
363             is( $lost_fee_line->status, 'FOUND',
364                 "Lost fee now has account status of FOUND - No Refund" );
365
366             is( $patron->account->balance,
367                 -0, 'The patron balance is 0, everything was written off' );
368         };
369
370         subtest 'Full payment tests' => sub {
371
372             plan tests => 16;
373
374             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
375
376             my $item = $builder->build_sample_item(
377                 {
378                     biblionumber     => $biblio->biblionumber,
379                     library          => $library->branchcode,
380                     replacementprice => $replacement_amount,
381                     itype            => $item_type->itemtype
382                 }
383             );
384
385             my $issue =
386               C4::Circulation::AddIssue( $patron, $item->barcode );
387
388             # Simulate item marked as lost
389             $item->itemlost(1)->store;
390             C4::Circulation::LostItem( $item->itemnumber, 1 );
391
392             my $processing_fee_lines = Koha::Account::Lines->search(
393                 {
394                     borrowernumber  => $patron->id,
395                     itemnumber      => $item->itemnumber,
396                     debit_type_code => 'PROCESSING'
397                 }
398             );
399             is( $processing_fee_lines->count,
400                 1, 'Only one processing fee produced' );
401             my $processing_fee_line = $processing_fee_lines->next;
402             is( $processing_fee_line->amount + 0,
403                 $processfee_amount,
404                 'The right PROCESSING amount is generated' );
405             is( $processing_fee_line->amountoutstanding + 0,
406                 $processfee_amount,
407                 'The right PROCESSING amountoutstanding is generated' );
408
409             my $lost_fee_lines = Koha::Account::Lines->search(
410                 {
411                     borrowernumber  => $patron->id,
412                     itemnumber      => $item->itemnumber,
413                     debit_type_code => 'LOST'
414                 }
415             );
416             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
417             my $lost_fee_line = $lost_fee_lines->next;
418             is( $lost_fee_line->amount + 0,
419                 $replacement_amount, 'The right LOST amount is generated' );
420             is( $lost_fee_line->amountoutstanding + 0,
421                 $replacement_amount,
422                 'The right LOST amountountstanding is generated' );
423
424             my $account = $patron->account;
425             my $debts   = $account->outstanding_debits;
426
427             # Pay off the debt
428             my $credit = $account->add_credit(
429                 {
430                     amount    => $account->balance,
431                     type      => 'PAYMENT',
432                     interface => 'test',
433                 }
434             );
435             $credit->apply( { debits => [ $debts->as_list ] } );
436
437             # Simulate item marked as found
438             $item->itemlost(0)->store;
439             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
440
441             my $credit_return = Koha::Account::Lines->search(
442                 {
443                     itemnumber       => $item->itemnumber,
444                     credit_type_code => 'LOST_FOUND'
445                 },
446                 { rows => 1 }
447             )->single;
448
449             ok( $credit_return, 'An account line of type LOST_FOUND is added' );
450             is( $credit_return->amount + 0,
451                 -99.00,
452                 'The account line of type LOST_FOUND has an amount of -99' );
453             is(
454                 $credit_return->amountoutstanding + 0,
455                 -99.00,
456 'The account line of type LOST_FOUND has an amountoutstanding of -99'
457             );
458
459             my $processing_return = Koha::Account::Lines->search(
460                 {
461                     itemnumber       => $item->itemnumber,
462                     credit_type_code => 'PROCESSING_FOUND'
463                 },
464                 { rows => 1 }
465             )->single;
466             ok( $processing_return, 'An account line of type PROCESSING_FOUND is added' );
467             is( $processing_return->amount + 0,
468                 -20.00,
469                 'The account line of type PROCESSING_FOUND has an amount of -20' );
470
471             $lost_fee_line->discard_changes;
472             is( $lost_fee_line->amountoutstanding + 0,
473                 0, 'Lost fee has no outstanding amount' );
474             is( $lost_fee_line->debit_type_code,
475                 'LOST', 'Lost fee now still has account type of LOST' );
476             is( $lost_fee_line->status, 'FOUND',
477                 "Lost fee now has account status of FOUND" );
478
479             is( $patron->account->balance, -119,
480 'The patron balance is -119, a credit that equals the lost fee payment and the processing fee'
481             );
482         };
483
484         subtest 'Test without payment or write off' => sub {
485
486             plan tests => 14;
487
488             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
489
490             my $item = $builder->build_sample_item(
491                 {
492                     biblionumber     => $biblio->biblionumber,
493                     library          => $library->branchcode,
494                     replacementprice => 23.00,
495                     replacementprice => $replacement_amount,
496                     itype            => $item_type->itemtype
497                 }
498             );
499
500             my $issue =
501               C4::Circulation::AddIssue( $patron, $item->barcode );
502
503             # Simulate item marked as lost
504             $item->itemlost(3)->store;
505             C4::Circulation::LostItem( $item->itemnumber, 1 );
506
507             my $processing_fee_lines = Koha::Account::Lines->search(
508                 {
509                     borrowernumber  => $patron->id,
510                     itemnumber      => $item->itemnumber,
511                     debit_type_code => 'PROCESSING'
512                 }
513             );
514             is( $processing_fee_lines->count,
515                 1, 'Only one processing fee produced' );
516             my $processing_fee_line = $processing_fee_lines->next;
517             is( $processing_fee_line->amount + 0,
518                 $processfee_amount,
519                 'The right PROCESSING amount is generated' );
520             is( $processing_fee_line->amountoutstanding + 0,
521                 $processfee_amount,
522                 'The right PROCESSING amountoutstanding is generated' );
523
524             my $lost_fee_lines = Koha::Account::Lines->search(
525                 {
526                     borrowernumber  => $patron->id,
527                     itemnumber      => $item->itemnumber,
528                     debit_type_code => 'LOST'
529                 }
530             );
531             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
532             my $lost_fee_line = $lost_fee_lines->next;
533             is( $lost_fee_line->amount + 0,
534                 $replacement_amount, 'The right LOST amount is generated' );
535             is( $lost_fee_line->amountoutstanding + 0,
536                 $replacement_amount,
537                 'The right LOST amountountstanding is generated' );
538
539             # Set processingreturn_policy to '0' so processing fee is retained
540             # these tests are just for lostreturn
541             my $processingreturn_rule = $builder->build(
542                 {
543                     source => 'CirculationRule',
544                     value  => {
545                         branchcode   => undef,
546                         categorycode => undef,
547                         itemtype     => undef,
548                         rule_name    => 'processingreturn',
549                         rule_value   => '0'
550                     }
551                 }
552             );
553
554             # Simulate item marked as found
555             $item->itemlost(0)->store;
556             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
557
558             my $credit_return = Koha::Account::Lines->search(
559                 {
560                     itemnumber       => $item->itemnumber,
561                     credit_type_code => 'LOST_FOUND'
562                 },
563                 { rows => 1 }
564             )->single;
565
566             ok( $credit_return, 'An account line of type LOST_FOUND is added' );
567             is( $credit_return->amount + 0,
568                 -99.00,
569                 'The account line of type LOST_FOUND has an amount of -99' );
570             is(
571                 $credit_return->amountoutstanding + 0,
572                 0,
573 'The account line of type LOST_FOUND has an amountoutstanding of 0'
574             );
575
576             $lost_fee_line->discard_changes;
577             is( $lost_fee_line->amountoutstanding + 0,
578                 0, 'Lost fee has no outstanding amount' );
579             is( $lost_fee_line->debit_type_code,
580                 'LOST', 'Lost fee now still has account type of LOST' );
581             is( $lost_fee_line->status, 'FOUND',
582                 "Lost fee now has account status of FOUND" );
583
584             is( $patron->account->balance,
585                 20, 'The patron balance is 20, still owes the processing fee' );
586         };
587
588         subtest
589           'Test with partial payment and write off, and remaining debt' =>
590           sub {
591
592             plan tests => 19;
593
594             t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 0 );
595
596             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
597             my $item = $builder->build_sample_item(
598                 {
599                     biblionumber     => $biblio->biblionumber,
600                     library          => $library->branchcode,
601                     replacementprice => $replacement_amount,
602                     itype            => $item_type->itemtype
603                 }
604             );
605
606             my $issue =
607               C4::Circulation::AddIssue( $patron, $item->barcode );
608
609             # Simulate item marked as lost
610             $item->itemlost(1)->store;
611             C4::Circulation::LostItem( $item->itemnumber, 1 );
612
613             my $processing_fee_lines = Koha::Account::Lines->search(
614                 {
615                     borrowernumber  => $patron->id,
616                     itemnumber      => $item->itemnumber,
617                     debit_type_code => 'PROCESSING'
618                 }
619             );
620             is( $processing_fee_lines->count,
621                 1, 'Only one processing fee produced' );
622             my $processing_fee_line = $processing_fee_lines->next;
623             is( $processing_fee_line->amount + 0,
624                 $processfee_amount,
625                 'The right PROCESSING amount is generated' );
626             is( $processing_fee_line->amountoutstanding + 0,
627                 $processfee_amount,
628                 'The right PROCESSING amountoutstanding is generated' );
629
630             my $lost_fee_lines = Koha::Account::Lines->search(
631                 {
632                     borrowernumber  => $patron->id,
633                     itemnumber      => $item->itemnumber,
634                     debit_type_code => 'LOST'
635                 }
636             );
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,
642                 $replacement_amount,
643                 'The right LOST amountountstanding is generated' );
644
645             my $account = $patron->account;
646             is(
647                 $account->balance,
648                 $processfee_amount + $replacement_amount,
649                 'Balance is PROCESSING + LOST'
650             );
651
652             # Partially pay fee (99 - 27 = 72)
653             my $payment_amount = 24;
654             my $payment        = $account->add_credit(
655                 {
656                     amount    => $payment_amount,
657                     type      => 'PAYMENT',
658                     interface => 'test',
659                 }
660             );
661
662             $payment->apply( { debits => [$lost_fee_line] } );
663
664             # Partially write off fee (72 - 20 = 52)
665             my $write_off_amount = 20;
666             my $write_off        = $account->add_credit(
667                 {
668                     amount    => $write_off_amount,
669                     type      => 'WRITEOFF',
670                     interface => 'test',
671                 }
672             );
673             $write_off->apply( { debits => [$lost_fee_line] } );
674
675
676             my $payment_amount_2 = 3;
677             my $payment_2        = $account->add_credit(
678                 {
679                     amount    => $payment_amount_2,
680                     type      => 'PAYMENT',
681                     interface => 'test',
682                 }
683             );
684
685             $payment_2->apply(
686                 { debits => [$lost_fee_line] } );
687
688             # Partially write off fee (52 - 5 = 47)
689             my $write_off_amount_2 = 5;
690             my $write_off_2        = $account->add_credit(
691                 {
692                     amount    => $write_off_amount_2,
693                     type      => 'WRITEOFF',
694                     interface => 'test',
695                 }
696             );
697
698             $write_off_2->apply(
699                 { debits => [$lost_fee_line] } );
700
701             is(
702                 $account->balance,
703                 $processfee_amount +
704                   $replacement_amount -
705                   $payment_amount -
706                   $write_off_amount -
707                   $payment_amount_2 -
708                   $write_off_amount_2,
709                 'Balance is PROCESSING + LOST - PAYMENT 1 - WRITEOFF - PAYMENT 2 - WRITEOFF 2'
710             );
711
712             # VOID payment_2 and writeoff_2
713             $payment_2->void({ interface => 'test' });
714             $write_off_2->void({ interface => 'test' });
715
716             is(
717                 $account->balance,
718                 $processfee_amount +
719                   $replacement_amount -
720                   $payment_amount -
721                   $write_off_amount,
722                 'Balance is PROCESSING + LOST - PAYMENT 1 - WRITEOFF (PAYMENT 2 and WRITEOFF 2 VOIDED)'
723             );
724
725             # Store the amountoutstanding value
726             $lost_fee_line->discard_changes;
727             my $outstanding = $lost_fee_line->amountoutstanding;
728             is(
729                 $outstanding + 0,
730                 $replacement_amount - $payment_amount - $write_off_amount,
731                 "Lost Fee Outstanding is LOST - PAYMENT 1 - WRITEOFF"
732             );
733
734             # Simulate item marked as found
735             $item->itemlost(0)->store;
736             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
737
738             my $credit_return = Koha::Account::Lines->search(
739                 {
740                     itemnumber       => $item->itemnumber,
741                     credit_type_code => 'LOST_FOUND'
742                 },
743                 { rows => 1 }
744             )->single;
745
746             ok( $credit_return, 'An account line of type LOST_FOUND is added' );
747
748             is(
749                 $account->balance,
750                 $processfee_amount - $payment_amount,
751                 'Balance is PROCESSING - PAYMENT (LOST_FOUND)'
752             );
753
754             $lost_fee_line->discard_changes;
755             is( $lost_fee_line->amountoutstanding + 0,
756                 0, 'Lost fee has no outstanding amount' );
757             is( $lost_fee_line->debit_type_code,
758                 'LOST', 'Lost fee now still has account type of LOST' );
759             is( $lost_fee_line->status, 'FOUND',
760                 "Lost fee now has account status of FOUND" );
761
762             is(
763                 $credit_return->amount + 0,
764                 ( $payment_amount + $outstanding ) * -1,
765 'The account line of type LOST_FOUND has an amount equal to the payment 1 + outstanding'
766             );
767             is(
768                 $credit_return->amountoutstanding + 0,
769                 $payment_amount * -1,
770 'The account line of type LOST_FOUND has an amountoutstanding equal to the payment'
771             );
772
773             is(
774                 $account->balance,
775                 $processfee_amount - $payment_amount,
776 'The patron balance is the difference between the PROCESSING and the credit'
777             );
778           };
779
780         subtest 'Partial payment, existing debits and AccountAutoReconcile' =>
781           sub {
782
783             plan tests => 10;
784
785             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
786             my $barcode            = 'KD123456793';
787             my $replacement_amount = 100;
788             my $processfee_amount  = 20;
789
790             my $item_type = $builder->build_object(
791                 {
792                     class => 'Koha::ItemTypes',
793                     value => {
794                         notforloan         => 0,
795                         rentalcharge       => 0,
796                         defaultreplacecost => undef,
797                         processfee         => 0,
798                         rentalcharge_daily => 0,
799                     }
800                 }
801             );
802             my $item = Koha::Item->new(
803                 {
804                     biblionumber     => $biblio->biblionumber,
805                     homebranch       => $library->branchcode,
806                     holdingbranch    => $library->branchcode,
807                     barcode          => $barcode,
808                     replacementprice => $replacement_amount,
809                     itype            => $item_type->itemtype
810                 },
811             )->store;
812
813             my $issue =
814               C4::Circulation::AddIssue( $patron, $barcode );
815
816             # Simulate item marked as lost
817             $item->itemlost(1)->store;
818             C4::Circulation::LostItem( $item->itemnumber, 1 );
819
820             my $lost_fee_lines = Koha::Account::Lines->search(
821                 {
822                     borrowernumber  => $patron->id,
823                     itemnumber      => $item->itemnumber,
824                     debit_type_code => 'LOST'
825                 }
826             );
827             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
828             my $lost_fee_line = $lost_fee_lines->next;
829             is( $lost_fee_line->amount + 0,
830                 $replacement_amount, 'The right LOST amount is generated' );
831             is( $lost_fee_line->amountoutstanding + 0,
832                 $replacement_amount,
833                 'The right LOST amountountstanding is generated' );
834
835             my $account = $patron->account;
836             is( $account->balance, $replacement_amount, 'Balance is L' );
837
838             # Partially pay fee
839             my $payment_amount = 27;
840             my $payment        = $account->add_credit(
841                 {
842                     amount    => $payment_amount,
843                     type      => 'PAYMENT',
844                     interface => 'test',
845                 }
846             );
847             $payment->apply( { debits => [$lost_fee_line] } );
848
849             is(
850                 $account->balance,
851                 $replacement_amount - $payment_amount,
852                 'Payment applied'
853             );
854
855             my $manual_debit_amount = 80;
856             $account->add_debit(
857                 {
858                     amount    => $manual_debit_amount,
859                     type      => 'OVERDUE',
860                     interface => 'test'
861                 }
862             );
863
864             is(
865                 $account->balance,
866                 $manual_debit_amount + $replacement_amount - $payment_amount,
867                 'Manual debit applied'
868             );
869
870             t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 1 );
871
872             # Simulate item marked as found
873             $item->itemlost(0)->store;
874             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
875
876             my $credit_return = Koha::Account::Lines->search(
877                 {
878                     itemnumber       => $item->itemnumber,
879                     credit_type_code => 'LOST_FOUND'
880                 },
881                 { rows => 1 }
882             )->single;
883
884             ok( $credit_return, 'An account line of type LOST_FOUND is added' );
885
886             is(
887                 $account->balance,
888                 $manual_debit_amount - $payment_amount,
889                 'Balance is PROCESSING - payment (LOST_FOUND)'
890             );
891
892             my $manual_debit = Koha::Account::Lines->search(
893                 {
894                     borrowernumber  => $patron->id,
895                     debit_type_code => 'OVERDUE',
896                     status          => 'UNRETURNED'
897                 }
898             )->next;
899             is(
900                 $manual_debit->amountoutstanding + 0,
901                 $manual_debit_amount - $payment_amount,
902                 'reconcile_balance was called'
903             );
904           };
905
906         subtest 'Patron deleted' => sub {
907             plan tests => 1;
908
909             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
910             my $barcode            = 'KD123456794';
911             my $replacement_amount = 100;
912             my $processfee_amount  = 20;
913
914             my $item_type = $builder->build_object(
915                 {
916                     class => 'Koha::ItemTypes',
917                     value => {
918                         notforloan         => 0,
919                         rentalcharge       => 0,
920                         defaultreplacecost => undef,
921                         processfee         => 0,
922                         rentalcharge_daily => 0,
923                     }
924                 }
925             );
926             my $item = Koha::Item->new(
927                 {
928                     biblionumber     => $biblio->biblionumber,
929                     homebranch       => $library->branchcode,
930                     holdingbranch    => $library->branchcode,
931                     barcode          => $barcode,
932                     replacementprice => $replacement_amount,
933                     itype            => $item_type->itemtype
934                 },
935             )->store;
936
937             my $issue =
938               C4::Circulation::AddIssue( $patron, $barcode );
939
940             # Simulate item marked as lost
941             $item->itemlost(1)->store;
942             C4::Circulation::LostItem( $item->itemnumber, 1 );
943
944             $issue->delete();
945             $patron->delete();
946
947             # Simulate item marked as found
948             $item->itemlost(0)->store;
949             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 0, 'No refund triggered' );
950
951         };
952
953         subtest 'restore fine | no overdue' => sub {
954
955             plan tests => 8;
956
957             my $manager =
958               $builder->build_object( { class => "Koha::Patrons" } );
959             t::lib::Mocks::mock_userenv(
960                 { patron => $manager, branchcode => $manager->branchcode } );
961
962             # Set lostreturn_policy to 'restore' for tests
963             my $specific_rule_restore = $builder->build(
964                 {
965                     source => 'CirculationRule',
966                     value  => {
967                         branchcode   => $manager->branchcode,
968                         categorycode => undef,
969                         itemtype     => undef,
970                         rule_name    => 'lostreturn',
971                         rule_value   => 'restore'
972                     }
973                 }
974             );
975
976             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
977
978             my $item = $builder->build_sample_item(
979                 {
980                     biblionumber     => $biblio->biblionumber,
981                     library          => $library->branchcode,
982                     replacementprice => $replacement_amount,
983                     itype            => $item_type->itemtype
984                 }
985             );
986
987             my $issue =
988               C4::Circulation::AddIssue( $patron, $item->barcode );
989
990             # Simulate item marked as lost
991             $item->itemlost(1)->store;
992             C4::Circulation::LostItem( $item->itemnumber, 1 );
993
994             my $processing_fee_lines = Koha::Account::Lines->search(
995                 {
996                     borrowernumber  => $patron->id,
997                     itemnumber      => $item->itemnumber,
998                     debit_type_code => 'PROCESSING'
999                 }
1000             );
1001             is( $processing_fee_lines->count,
1002                 1, 'Only one processing fee produced' );
1003             my $processing_fee_line = $processing_fee_lines->next;
1004             is( $processing_fee_line->amount + 0,
1005                 $processfee_amount,
1006                 'The right PROCESSING amount is generated' );
1007             is( $processing_fee_line->amountoutstanding + 0,
1008                 $processfee_amount,
1009                 'The right PROCESSING amountoutstanding is generated' );
1010
1011             my $lost_fee_lines = Koha::Account::Lines->search(
1012                 {
1013                     borrowernumber  => $patron->id,
1014                     itemnumber      => $item->itemnumber,
1015                     debit_type_code => 'LOST'
1016                 }
1017             );
1018             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1019             my $lost_fee_line = $lost_fee_lines->next;
1020             is( $lost_fee_line->amount + 0,
1021                 $replacement_amount, 'The right LOST amount is generated' );
1022             is( $lost_fee_line->amountoutstanding + 0,
1023                 $replacement_amount,
1024                 'The right LOST amountountstanding is generated' );
1025
1026             my $account = $patron->account;
1027             my $debts   = $account->outstanding_debits;
1028
1029             # Pay off the debt
1030             my $credit = $account->add_credit(
1031                 {
1032                     amount    => $account->balance,
1033                     type      => 'PAYMENT',
1034                     interface => 'test',
1035                 }
1036             );
1037             $credit->apply( { debits => [ $debts->as_list ] } );
1038
1039             # Simulate item marked as found
1040             $item->itemlost(0)->store;
1041             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1042             is( scalar ( grep { $_->message eq 'lost_restored' } @{$item->object_messages} ), 0, 'Restore not triggered when there is no overdue fine found' );
1043         };
1044
1045         subtest 'restore fine | unforgiven overdue' => sub {
1046
1047             plan tests => 10;
1048
1049             # Set lostreturn_policy to 'restore' for tests
1050             my $manager =
1051               $builder->build_object( { class => "Koha::Patrons" } );
1052             t::lib::Mocks::mock_userenv(
1053                 { patron => $manager, branchcode => $manager->branchcode } );
1054             my $specific_rule_restore = $builder->build(
1055                 {
1056                     source => 'CirculationRule',
1057                     value  => {
1058                         branchcode   => $manager->branchcode,
1059                         categorycode => undef,
1060                         itemtype     => undef,
1061                         rule_name    => 'lostreturn',
1062                         rule_value   => 'restore'
1063                     }
1064                 }
1065             );
1066
1067             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1068
1069             my $item = $builder->build_sample_item(
1070                 {
1071                     biblionumber     => $biblio->biblionumber,
1072                     library          => $library->branchcode,
1073                     replacementprice => $replacement_amount,
1074                     itype            => $item_type->itemtype
1075                 }
1076             );
1077
1078             my $issue =
1079               C4::Circulation::AddIssue( $patron, $item->barcode );
1080
1081             # Simulate item marked as lost
1082             $item->itemlost(1)->store;
1083             C4::Circulation::LostItem( $item->itemnumber, 1 );
1084
1085             my $processing_fee_lines = Koha::Account::Lines->search(
1086                 {
1087                     borrowernumber  => $patron->id,
1088                     itemnumber      => $item->itemnumber,
1089                     debit_type_code => 'PROCESSING'
1090                 }
1091             );
1092             is( $processing_fee_lines->count,
1093                 1, 'Only one processing fee produced' );
1094             my $processing_fee_line = $processing_fee_lines->next;
1095             is( $processing_fee_line->amount + 0,
1096                 $processfee_amount,
1097                 'The right PROCESSING amount is generated' );
1098             is( $processing_fee_line->amountoutstanding + 0,
1099                 $processfee_amount,
1100                 'The right PROCESSING amountoutstanding is generated' );
1101
1102             my $lost_fee_lines = Koha::Account::Lines->search(
1103                 {
1104                     borrowernumber  => $patron->id,
1105                     itemnumber      => $item->itemnumber,
1106                     debit_type_code => 'LOST'
1107                 }
1108             );
1109             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1110             my $lost_fee_line = $lost_fee_lines->next;
1111             is( $lost_fee_line->amount + 0,
1112                 $replacement_amount, 'The right LOST amount is generated' );
1113             is( $lost_fee_line->amountoutstanding + 0,
1114                 $replacement_amount,
1115                 'The right LOST amountountstanding is generated' );
1116
1117             my $account = $patron->account;
1118             my $debts   = $account->outstanding_debits;
1119
1120             # Pay off the debt
1121             my $credit = $account->add_credit(
1122                 {
1123                     amount    => $account->balance,
1124                     type      => 'PAYMENT',
1125                     interface => 'test',
1126                 }
1127             );
1128             $credit->apply( { debits => [ $debts->as_list ] } );
1129
1130             # Fine not forgiven
1131             my $overdue = $account->add_debit(
1132                 {
1133                     amount     => 30.00,
1134                     user_id    => $manager->borrowernumber,
1135                     library_id => $library->branchcode,
1136                     interface  => 'test',
1137                     item_id    => $item->itemnumber,
1138                     type       => 'OVERDUE',
1139                 }
1140             )->store();
1141             $overdue->status('LOST')->store();
1142             $overdue->discard_changes;
1143             is( $overdue->status, 'LOST',
1144                 'Overdue status set to LOST' );
1145
1146             # Simulate item marked as found
1147             $item->itemlost(0)->store;
1148             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1149             is( scalar ( grep { $_->message eq 'lost_restored' } @{$item->object_messages} ), 0, 'Restore not triggered when overdue was not forgiven' );
1150             $overdue->discard_changes;
1151             is( $overdue->status, 'FOUND',
1152                 'Overdue status updated to FOUND' );
1153         };
1154
1155         subtest 'restore fine | forgiven overdue' => sub {
1156
1157             plan tests => 12;
1158
1159             # Set lostreturn_policy to 'restore' for tests
1160             my $manager =
1161               $builder->build_object( { class => "Koha::Patrons" } );
1162             t::lib::Mocks::mock_userenv(
1163                 { patron => $manager, branchcode => $manager->branchcode } );
1164             my $specific_rule_restore = $builder->build(
1165                 {
1166                     source => 'CirculationRule',
1167                     value  => {
1168                         branchcode   => $manager->branchcode,
1169                         categorycode => undef,
1170                         itemtype     => undef,
1171                         rule_name    => 'lostreturn',
1172                         rule_value   => 'restore'
1173                     }
1174                 }
1175             );
1176
1177             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1178
1179             my $item = $builder->build_sample_item(
1180                 {
1181                     biblionumber     => $biblio->biblionumber,
1182                     library          => $library->branchcode,
1183                     replacementprice => $replacement_amount,
1184                     itype            => $item_type->itemtype
1185                 }
1186             );
1187
1188             my $issue =
1189               C4::Circulation::AddIssue( $patron, $item->barcode );
1190
1191             # Simulate item marked as lost
1192             $item->itemlost(1)->store;
1193             C4::Circulation::LostItem( $item->itemnumber, 1 );
1194
1195             my $processing_fee_lines = Koha::Account::Lines->search(
1196                 {
1197                     borrowernumber  => $patron->id,
1198                     itemnumber      => $item->itemnumber,
1199                     debit_type_code => 'PROCESSING'
1200                 }
1201             );
1202             is( $processing_fee_lines->count,
1203                 1, 'Only one processing fee produced' );
1204             my $processing_fee_line = $processing_fee_lines->next;
1205             is( $processing_fee_line->amount + 0,
1206                 $processfee_amount,
1207                 'The right PROCESSING amount is generated' );
1208             is( $processing_fee_line->amountoutstanding + 0,
1209                 $processfee_amount,
1210                 'The right PROCESSING amountoutstanding is generated' );
1211
1212             my $lost_fee_lines = Koha::Account::Lines->search(
1213                 {
1214                     borrowernumber  => $patron->id,
1215                     itemnumber      => $item->itemnumber,
1216                     debit_type_code => 'LOST'
1217                 }
1218             );
1219             is( $lost_fee_lines->count, 1, 'Only one lost item fee produced' );
1220             my $lost_fee_line = $lost_fee_lines->next;
1221             is( $lost_fee_line->amount + 0,
1222                 $replacement_amount, 'The right LOST amount is generated' );
1223             is( $lost_fee_line->amountoutstanding + 0,
1224                 $replacement_amount,
1225                 'The right LOST amountountstanding is generated' );
1226
1227             my $account = $patron->account;
1228             my $debts   = $account->outstanding_debits;
1229
1230             # Pay off the debt
1231             my $credit = $account->add_credit(
1232                 {
1233                     amount    => $account->balance,
1234                     type      => 'PAYMENT',
1235                     interface => 'test',
1236                 }
1237             );
1238             $credit->apply( { debits => [ $debts->as_list ] } );
1239
1240             # Add overdue
1241             my $overdue = $account->add_debit(
1242                 {
1243                     amount     => 30.00,
1244                     user_id    => $manager->borrowernumber,
1245                     library_id => $library->branchcode,
1246                     interface  => 'test',
1247                     item_id    => $item->itemnumber,
1248                     type       => 'OVERDUE',
1249                 }
1250             )->store();
1251             $overdue->status('LOST')->store();
1252             is( $overdue->status, 'LOST',
1253                 'Overdue status set to LOST' );
1254
1255             t::lib::Mocks::mock_preference( 'AccountAutoReconcile', 0 );
1256
1257             # Forgive fine
1258             $credit = $account->add_credit(
1259                 {
1260                     amount     => 30.00,
1261                     user_id    => $manager->borrowernumber,
1262                     library_id => $library->branchcode,
1263                     interface  => 'test',
1264                     type       => 'FORGIVEN',
1265                     item_id    => $item->itemnumber
1266                 }
1267             );
1268             $credit->apply( { debits => [$overdue] } );
1269
1270             # Simulate item marked as found
1271             $item->itemlost(0)->store;
1272             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1273             is( scalar ( grep { $_->message eq 'lost_restored' } @{$item->object_messages} ), 1, 'Restore triggered when overdue was forgiven' );
1274             $overdue->discard_changes;
1275             is( $overdue->status, 'FOUND', 'Overdue status updated to FOUND' );
1276             is( $overdue->amountoutstanding, $overdue->amount, 'Overdue outstanding has been restored' );
1277             $credit->discard_changes;
1278             is( $credit->status, 'VOID', 'Overdue Forgival has been marked as VOID');
1279         };
1280
1281         subtest 'Continue when userenv is not set' => sub {
1282             plan tests => 1;
1283
1284             my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1285             my $barcode            = 'KD123456795';
1286             my $replacement_amount = 100;
1287             my $processfee_amount  = 20;
1288
1289             my $item_type = $builder->build_object(
1290                 {
1291                     class => 'Koha::ItemTypes',
1292                     value => {
1293                         notforloan         => 0,
1294                         rentalcharge       => 0,
1295                         defaultreplacecost => undef,
1296                         processfee         => 0,
1297                         rentalcharge_daily => 0,
1298                     }
1299                 }
1300             );
1301             my $item = $builder->build_sample_item(
1302                 {
1303                     biblionumber     => $biblio->biblionumber,
1304                     homebranch       => $library->branchcode,
1305                     holdingbranch    => $library->branchcode,
1306                     barcode          => $barcode,
1307                     replacementprice => $replacement_amount,
1308                     itype            => $item_type->itemtype
1309                 }
1310             );
1311
1312             my $issue =
1313               C4::Circulation::AddIssue( $patron, $barcode );
1314
1315             # Simulate item marked as lost
1316             $item->itemlost(1)->store;
1317             C4::Circulation::LostItem( $item->itemnumber, 1 );
1318
1319             # Unset the userenv
1320             C4::Context->_new_userenv(undef);
1321
1322             # Simluate item marked as found
1323             $item->itemlost(0)->store;
1324             is( scalar ( grep { $_->message eq 'lost_refunded' } @{$item->object_messages} ), 1, 'Refund triggered' );
1325
1326         };
1327     };
1328
1329     subtest 'log_action' => sub {
1330         plan tests => 2;
1331         t::lib::Mocks::mock_preference( 'CataloguingLog', 1 );
1332
1333         my $item = Koha::Item->new(
1334             {
1335                 homebranch    => $library->{branchcode},
1336                 holdingbranch => $library->{branchcode},
1337                 biblionumber  => $biblio->biblionumber,
1338                 location      => 'my_loc',
1339             }
1340         )->store;
1341         is(
1342             Koha::ActionLogs->search(
1343                 {
1344                     module => 'CATALOGUING',
1345                     action => 'ADD',
1346                     object => $item->itemnumber,
1347                     info   => 'item'
1348                 }
1349             )->count,
1350             1,
1351             "Item creation logged"
1352         );
1353
1354         $item->location('another_loc')->store;
1355         is(
1356             Koha::ActionLogs->search(
1357                 {
1358                     module => 'CATALOGUING',
1359                     action => 'MODIFY',
1360                     object => $item->itemnumber
1361                 }
1362             )->count,
1363             1,
1364             "Item modification logged"
1365         );
1366     };
1367
1368     subtest 'itemlost / statistics' => sub {    # TODO BZ 34308 (gt zero checks)
1369         plan tests => 5;
1370
1371         my $item = $builder->build_sample_item;
1372         $item->itemlost(-1)->store;             # weird value; >0 test not triggered ?
1373         is( Koha::Statistics->search( { itemnumber => $item->id } )->count, 0, 'No statistics added' );
1374         $item->itemlost(1)->store;
1375         is( Koha::Statistics->search( { itemnumber => $item->id } )->count, 1, 'statistics added' );
1376         $item->itemlost(2)->store;
1377         is( Koha::Statistics->search( { itemnumber => $item->id } )->count, 1, 'No statistics added, already lost' );
1378         $item->itemlost(-1)->store;             # weird value; <=0 test triggered ?
1379         is( Koha::Statistics->search( { itemnumber => $item->id } )->count, 2, 'statistics added' );
1380         $item->itemlost(-2)->store;             # weird value, but no status change
1381         is( Koha::Statistics->search( { itemnumber => $item->id } )->count, 2, 'No statistics added, already *found*' );
1382     };
1383 };
1384
1385 subtest 'serial_item' => sub {
1386
1387     plan tests => 3;
1388
1389     $schema->storage->txn_begin;
1390
1391     my $item = $builder->build_sample_item;
1392     my $serial_item =
1393         $builder->build_object( { class => 'Koha::Serial::Items', value => { itemnumber => $item->itemnumber } } );
1394     is( ref( $item->serial_item ),      'Koha::Serial::Item' );
1395     is( $item->serial_item->itemnumber, $item->itemnumber );
1396
1397     is( ref( $item->serial_item->serial ), 'Koha::Serial', 'Koha::Serial::Item->serial returns a Koha::Serial object' );
1398
1399     $schema->storage->txn_rollback;
1400
1401 };
1402
1403 subtest 'item_group_item' => sub {
1404
1405     plan tests => 2;
1406
1407     $schema->storage->txn_begin;
1408
1409     my $biblio = $builder->build_sample_biblio();
1410     my $item_1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
1411     my $item_2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber } );
1412
1413     my $item_group = Koha::Biblio::ItemGroup->new( { biblio_id => $biblio->id } )->store();
1414     $item_group->add_item( { item_id => $item_1->itemnumber } );
1415
1416     is(
1417         ref( $item_1->item_group_item ), 'Koha::Biblio::ItemGroup::Item',
1418         '->item_group_item should return a Koha::Biblio::ItemGroup::Item object'
1419     );
1420     is( $item_1->item_group_item->item_id, $item_1->itemnumber, '->item_group_item should return the correct item' );
1421
1422     $schema->storage->txn_rollback;
1423
1424 };
1425
1426 subtest 'course_item' => sub {
1427
1428     plan tests => 2;
1429
1430     $schema->storage->txn_begin;
1431
1432     my $item = $builder->build_sample_item;
1433     my $course_item =
1434         $builder->build_object( { class => 'Koha::Course::Items', value => { itemnumber => $item->itemnumber } } );
1435     is( ref( $item->course_item ), 'Koha::Course::Item', '->course_item should return a Koha::Course::Item object' );
1436     is( $item->course_item->ci_id, $course_item->ci_id,  '->course_item should return the correct object' );
1437
1438     $schema->storage->txn_rollback;
1439
1440 };
1441
1442 subtest 'get_transfer|transfer' => sub {
1443     plan tests => 9;
1444
1445     my $transfer = $new_item_1->get_transfer();
1446     is( $transfer, undef, 'Koha::Item->get_transfer should return undef if the item is not in transit' );
1447     is( $new_item_1->transfer, undef );
1448
1449     my $library_to = $builder->build( { source => 'Branch' } );
1450
1451     my $transfer_1 = $builder->build_object(
1452         {
1453             class => 'Koha::Item::Transfers',
1454             value => {
1455                 itemnumber    => $new_item_1->itemnumber,
1456                 frombranch    => $new_item_1->holdingbranch,
1457                 tobranch      => $library_to->{branchcode},
1458                 reason        => 'Manual',
1459                 datesent      => undef,
1460                 datearrived   => undef,
1461                 datecancelled => undef,
1462                 daterequested => \'NOW()'
1463             }
1464         }
1465     );
1466
1467     $transfer = $new_item_1->get_transfer();
1468     is( ref($transfer), 'Koha::Item::Transfer', 'Koha::Item->get_transfer should return a Koha::Item::Transfer object' );
1469     is( ref($new_item_1->transfer), 'Koha::Item::Transfer' );
1470
1471     my $transfer_2 = $builder->build_object(
1472         {
1473             class => 'Koha::Item::Transfers',
1474             value => {
1475                 itemnumber    => $new_item_1->itemnumber,
1476                 frombranch    => $new_item_1->holdingbranch,
1477                 tobranch      => $library_to->{branchcode},
1478                 reason        => 'Manual',
1479                 datesent      => undef,
1480                 datearrived   => undef,
1481                 datecancelled => undef,
1482                 daterequested => \'NOW()'
1483             }
1484         }
1485     );
1486
1487     $transfer = $new_item_1->get_transfer();
1488     is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the oldest transfer request');
1489
1490     $transfer_2->datesent(\'NOW()')->store;
1491     $transfer = $new_item_1->get_transfer();
1492     is( $transfer->branchtransfer_id, $transfer_2->branchtransfer_id, 'Koha::Item->get_transfer returns the in_transit transfer');
1493
1494     my $transfer_3 = $builder->build_object(
1495         {
1496             class => 'Koha::Item::Transfers',
1497             value => {
1498                 itemnumber    => $new_item_1->itemnumber,
1499                 frombranch    => $new_item_1->holdingbranch,
1500                 tobranch      => $library_to->{branchcode},
1501                 reason        => 'Manual',
1502                 datesent      => undef,
1503                 datearrived   => undef,
1504                 datecancelled => undef,
1505                 daterequested => \'NOW()'
1506             }
1507         }
1508     );
1509
1510     $transfer_2->datearrived(\'NOW()')->store;
1511     $transfer = $new_item_1->get_transfer();
1512     is( $transfer->branchtransfer_id, $transfer_1->branchtransfer_id, 'Koha::Item->get_transfer returns the next queued transfer');
1513     is( $transfer->itemnumber, $new_item_1->itemnumber, 'Koha::Item->get_transfer returns the right items transfer' );
1514
1515     $transfer_1->datecancelled(\'NOW()')->store;
1516     $transfer = $new_item_1->get_transfer();
1517     is( $transfer->branchtransfer_id, $transfer_3->branchtransfer_id, 'Koha::Item->get_transfer ignores cancelled transfers');
1518 };
1519
1520 subtest 'holds' => sub {
1521     plan tests => 7;
1522
1523     my $biblio = $builder->build_sample_biblio();
1524     my $item   = $builder->build_sample_item(
1525         {
1526             biblionumber => $biblio->biblionumber,
1527         }
1528     );
1529     is( $item->holds->count, 0,     "Nothing returned if no holds" );
1530     is( $item->first_hold,   undef, 'No hold yet' );
1531     my $yesterday = dt_from_string->subtract( days => 1 )->ymd;
1532     my $hold1     = $builder->build(
1533         {
1534             source => 'Reserve',
1535             value  => {
1536                 itemnumber => $item->itemnumber, found => 'T', reservedate => $yesterday, suspend => 0, priority => 2
1537             }
1538         }
1539     );
1540     my $hold2 = $builder->build(
1541         {
1542             source => 'Reserve',
1543             value  => {
1544                 itemnumber => $item->itemnumber, found => 'W', reservedate => $yesterday, suspend => 0, priority => 1
1545             }
1546         }
1547     );
1548     my $hold3 = $builder->build(
1549         {
1550             source => 'Reserve',
1551             value  => {
1552                 itemnumber => $item->itemnumber, found => 'W', reservedate => $yesterday, suspend => 0, priority => 3
1553             }
1554         }
1555     );
1556
1557     is( $item->holds()->count,                   3, "Three holds found" );
1558     is( $item->holds( { found => 'W' } )->count, 2, "Two waiting holds found" );
1559     is_deeply( $item->holds( { found => 'T' } )->next->unblessed, $hold1, "Found transit holds matches the hold" );
1560     is( $item->holds( { found => undef } )->count, 0,                    "Nothing returned if no matching holds" );
1561     is( $item->first_hold->reserve_id,             $hold2->{reserve_id}, '->first_hold returns the correct hold' );
1562 };
1563
1564 subtest 'biblio' => sub {
1565     plan tests => 2;
1566
1567     my $biblio = $retrieved_item_1->biblio;
1568     is( ref( $biblio ), 'Koha::Biblio', 'Koha::Item->biblio should return a Koha::Biblio' );
1569     is( $biblio->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblio should return the correct biblio' );
1570 };
1571
1572 subtest 'biblioitem' => sub {
1573     plan tests => 2;
1574
1575     my $biblioitem = $retrieved_item_1->biblioitem;
1576     is( ref( $biblioitem ), 'Koha::Biblioitem', 'Koha::Item->biblioitem should return a Koha::Biblioitem' );
1577     is( $biblioitem->biblionumber, $retrieved_item_1->biblionumber, 'Koha::Item->biblioitem should return the correct biblioitem' );
1578 };
1579
1580 # Restore userenv
1581 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
1582 subtest 'checkout' => sub {
1583     plan tests => 5;
1584     my $item = Koha::Items->find( $new_item_1->itemnumber );
1585     # No checkout yet
1586     my $checkout = $item->checkout;
1587     is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no current checkout on this item' );
1588
1589     # Add a checkout
1590     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1591     C4::Circulation::AddIssue( $patron, $item->barcode );
1592     $checkout = $retrieved_item_1->checkout;
1593     is( ref( $checkout ), 'Koha::Checkout', 'Koha::Item->checkout should return a Koha::Checkout' );
1594     is( $checkout->itemnumber, $item->itemnumber, 'Koha::Item->checkout should return the correct checkout' );
1595     is( $checkout->borrowernumber, $patron->borrowernumber, 'Koha::Item->checkout should return the correct checkout' );
1596
1597     # Do the return
1598     C4::Circulation::AddReturn( $item->barcode );
1599
1600     # There is no more checkout on this item, making sure it will not return old checkouts
1601     $checkout = $item->checkout;
1602     is( $checkout, undef, 'Koha::Item->checkout should return undef if there is no *current* checkout on this item' );
1603 };
1604
1605 subtest 'can_be_transferred' => sub {
1606     plan tests => 5;
1607
1608     t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
1609     t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
1610
1611     my $biblio   = $builder->build_sample_biblio();
1612     my $library1 = $builder->build_object( { class => 'Koha::Libraries' } );
1613     my $library2 = $builder->build_object( { class => 'Koha::Libraries' } );
1614     my $item  = $builder->build_sample_item({
1615         biblionumber     => $biblio->biblionumber,
1616         homebranch       => $library1->branchcode,
1617         holdingbranch    => $library1->branchcode,
1618     });
1619
1620     is(Koha::Item::Transfer::Limits->search({
1621         fromBranch => $library1->branchcode,
1622         toBranch => $library2->branchcode,
1623     })->count, 0, 'There are no transfer limits between libraries.');
1624     ok($item->can_be_transferred({ to => $library2 }),
1625        'Item can be transferred between libraries.');
1626
1627     my $limit = Koha::Item::Transfer::Limit->new({
1628         fromBranch => $library1->branchcode,
1629         toBranch => $library2->branchcode,
1630         itemtype => $item->effective_itemtype,
1631     })->store;
1632     is(Koha::Item::Transfer::Limits->search({
1633         fromBranch => $library1->branchcode,
1634         toBranch => $library2->branchcode,
1635     })->count, 1, 'Given we have added a transfer limit,');
1636     is($item->can_be_transferred({ to => $library2 }), 0,
1637        'Item can no longer be transferred between libraries.');
1638     is($item->can_be_transferred({ to => $library2, from => $library1 }), 0,
1639        'We get the same result also if we pass the from-library parameter.');
1640 };
1641
1642 # Reset nb_of_items prior to testing delete
1643 $nb_of_items = Koha::Items->search->count;
1644
1645 # Test delete
1646 $retrieved_item_1->delete;
1647 is( Koha::Items->search->count, $nb_of_items - 1, 'Delete should have deleted the item' );
1648
1649 $schema->storage->txn_rollback;
1650
1651 subtest 'filter_by_visible_in_opac() tests' => sub {
1652
1653     plan tests => 14;
1654
1655     $schema->storage->txn_begin;
1656
1657     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
1658     my $mocked_category = Test::MockModule->new('Koha::Patron::Category');
1659     my $exception = 1;
1660     $mocked_category->mock( 'override_hidden_items', sub {
1661         return $exception;
1662     });
1663
1664     # have a fresh biblio
1665     my $biblio = $builder->build_sample_biblio;
1666     # have two itemtypes
1667     my $itype_1 = $builder->build_object({ class => 'Koha::ItemTypes' });
1668     my $itype_2 = $builder->build_object({ class => 'Koha::ItemTypes' });
1669     # have 5 items on that biblio
1670     my $item_1 = $builder->build_sample_item(
1671         {
1672             biblionumber => $biblio->biblionumber,
1673             itemlost     => -1,
1674             itype        => $itype_1->itemtype,
1675             withdrawn    => 1,
1676             copynumber   => undef
1677         }
1678     );
1679     my $item_2 = $builder->build_sample_item(
1680         {
1681             biblionumber => $biblio->biblionumber,
1682             itemlost     => 0,
1683             itype        => $itype_2->itemtype,
1684             withdrawn    => 2,
1685             copynumber   => undef
1686         }
1687     );
1688     my $item_3 = $builder->build_sample_item(
1689         {
1690             biblionumber => $biblio->biblionumber,
1691             itemlost     => 1,
1692             itype        => $itype_1->itemtype,
1693             withdrawn    => 3,
1694             copynumber   => undef
1695         }
1696     );
1697     my $item_4 = $builder->build_sample_item(
1698         {
1699             biblionumber => $biblio->biblionumber,
1700             itemlost     => 0,
1701             itype        => $itype_2->itemtype,
1702             withdrawn    => 4,
1703             copynumber   => undef
1704         }
1705     );
1706     my $item_5 = $builder->build_sample_item(
1707         {
1708             biblionumber => $biblio->biblionumber,
1709             itemlost     => 0,
1710             itype        => $itype_1->itemtype,
1711             withdrawn    => 5,
1712             copynumber   => undef
1713         }
1714     );
1715     my $item_6 = $builder->build_sample_item(
1716         {
1717             biblionumber => $biblio->biblionumber,
1718             itemlost     => 2,
1719             itype        => $itype_1->itemtype,
1720             withdrawn    => 5,
1721             copynumber   => undef
1722         }
1723     );
1724
1725     my $rules = undef;
1726
1727     my $mocked_context = Test::MockModule->new('C4::Context');
1728     $mocked_context->mock( 'yaml_preference', sub {
1729         return $rules;
1730     });
1731
1732     t::lib::Mocks::mock_preference( 'hidelostitems', 0 );
1733     is( $biblio->items->filter_by_visible_in_opac->count,
1734         6, 'No rules passed, hidelostitems unset' );
1735
1736     is( $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1737         6, 'No rules passed, hidelostitems unset, patron exception changes nothing' );
1738
1739     $rules = { copynumber => [ 2 ] };
1740
1741     t::lib::Mocks::mock_preference( 'hidelostitems', 1 );
1742     is(
1743         $biblio->items->filter_by_visible_in_opac->count,
1744         3,
1745         'No rules passed, hidelostitems set'
1746     );
1747
1748     is(
1749         $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1750         3,
1751         'No rules passed, hidelostitems set, patron exception changes nothing'
1752     );
1753
1754     $rules = { biblionumber => [ $biblio->biblionumber ] };
1755     is(
1756         $biblio->items->filter_by_visible_in_opac->count,
1757         0,
1758         'Biblionumber rule successfully hides all items'
1759     );
1760
1761     my $biblio2 = $builder->build_sample_biblio;
1762     $rules = { biblionumber => [ $biblio2->biblionumber ] };
1763     my $prefetched = $biblio->items->search({},{ prefetch => ['branchtransfers','reserves'] })->filter_by_visible_in_opac;
1764     ok( $prefetched->next, "Can retrieve object when prefetching and hiding on a duplicated column");
1765
1766     $rules = { withdrawn => [ 1, 2 ], copynumber => [ 2 ] };
1767     is(
1768         $biblio->items->filter_by_visible_in_opac->count,
1769         2,
1770         'Rules on withdrawn, hidelostitems set'
1771     );
1772
1773     is(
1774         $biblio->items->filter_by_visible_in_opac({ patron => $patron })->count,
1775         3,
1776         'hidelostitems set, rules on withdrawn but patron override passed'
1777     );
1778
1779     $rules = { itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1780     is(
1781         $biblio->items->filter_by_visible_in_opac->count,
1782         2,
1783         'Rules on itype, hidelostitems set'
1784     );
1785
1786     $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_1->itemtype ], copynumber => [ 2 ] };
1787     is(
1788         $biblio->items->filter_by_visible_in_opac->count,
1789         1,
1790         'Rules on itype and withdrawn, hidelostitems set'
1791     );
1792     is(
1793         $biblio->items->filter_by_visible_in_opac
1794           ->next->itemnumber,
1795         $item_4->itemnumber,
1796         'The right item is returned'
1797     );
1798
1799     $rules = { withdrawn => [ 1, 2 ], itype => [ $itype_2->itemtype ], copynumber => [ 2 ] };
1800     is(
1801         $biblio->items->filter_by_visible_in_opac->count,
1802         1,
1803         'Rules on itype and withdrawn, hidelostitems set'
1804     );
1805     is(
1806         $biblio->items->filter_by_visible_in_opac
1807           ->next->itemnumber,
1808         $item_5->itemnumber,
1809         'The right item is returned'
1810     );
1811
1812     # Make sure the warning on the about page will work
1813     $rules = { itemlost => ['AB'] };
1814     my $c = Koha::Items->filter_by_visible_in_opac->count;
1815     my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
1816     like( $warnings[2], qr/Truncated incorrect (DOUBLE|DECIMAL) value: 'AB'/);
1817
1818     $schema->storage->txn_rollback;
1819 };
1820
1821 subtest 'filter_out_lost() tests' => sub {
1822
1823     plan tests => 2;
1824
1825     $schema->storage->txn_begin;
1826
1827     # have a fresh biblio
1828     my $biblio = $builder->build_sample_biblio;
1829     # have 3 items on that biblio
1830     my $item_1 = $builder->build_sample_item(
1831         {
1832             biblionumber => $biblio->biblionumber,
1833             itemlost     => -1,
1834         }
1835     );
1836     my $item_2 = $builder->build_sample_item(
1837         {
1838             biblionumber => $biblio->biblionumber,
1839             itemlost     => 0,
1840         }
1841     );
1842     my $item_3 = $builder->build_sample_item(
1843         {
1844             biblionumber => $biblio->biblionumber,
1845             itemlost     => 1,
1846         }
1847     );
1848
1849     is( $biblio->items->filter_out_lost->next->itemnumber, $item_2->itemnumber, 'Right item returned' );
1850     is( $biblio->items->filter_out_lost->count, 1, 'Only one item is not lost' );
1851
1852     $schema->storage->txn_rollback;
1853 };
1854
1855 subtest 'move_to_biblio() tests' => sub {
1856
1857     plan tests => 2;
1858
1859     $schema->storage->txn_begin;
1860
1861     my $biblio1 = $builder->build_sample_biblio;
1862     my $biblio2 = $builder->build_sample_biblio;
1863     my $item1 = $builder->build_sample_item({ biblionumber => $biblio1->biblionumber });
1864     my $item2 = $builder->build_sample_item({ biblionumber => $biblio1->biblionumber });
1865
1866     $biblio1->items->move_to_biblio($biblio2);
1867
1868     $item1->discard_changes;
1869     $item2->discard_changes;
1870
1871     is($item1->biblionumber, $biblio2->biblionumber, "Item 1 moved");
1872     is($item2->biblionumber, $biblio2->biblionumber, "Item 2 moved");
1873
1874     $schema->storage->txn_rollback;
1875
1876 };
1877
1878 subtest 'search_ordered' => sub {
1879
1880     plan tests => 8;
1881
1882     $schema->storage->txn_begin;
1883
1884     my $library_a = $builder->build_object(
1885         { class => 'Koha::Libraries', value => { branchname => 'TEST_A' } } );
1886     my $library_z = $builder->build_object(
1887         { class => 'Koha::Libraries', value => { branchname => 'TEST_Z' } } );
1888     my $biblio = $builder->build_sample_biblio( { serial => 0 } );
1889     my $item1 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1890     my $item2 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1891     my $item3 = $builder->build_sample_item({ biblionumber => $biblio->biblionumber });
1892
1893     { # Is not a serial
1894
1895         # order_by homebranch.branchname
1896         $item1->discard_changes->update( { homebranch => $library_z->branchcode } );
1897         $item2->discard_changes->update( { homebranch => $library_a->branchcode } );
1898         $item3->discard_changes->update( { homebranch => $library_z->branchcode } );
1899         is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1900             [ $item2->itemnumber, $item1->itemnumber, $item3->itemnumber ],
1901             "not a serial - order by homebranch" );
1902
1903         # order_by me.enumchron
1904         $biblio->items->update( { homebranch => $library_a->branchcode } );
1905         $item1->discard_changes->update( { enumchron => 'cc' } );
1906         $item2->discard_changes->update( { enumchron => 'bb' } );
1907         $item3->discard_changes->update( { enumchron => 'aa' } );
1908         is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1909             [ $item3->itemnumber, $item2->itemnumber, $item1->itemnumber ],
1910             "not a serial - order by enumchron" );
1911
1912         # order_by -desc => 'me.dateaccessioned'
1913         $biblio->items->update( { enumchron => undef } );
1914         $item1->discard_changes->update( { dateaccessioned => '2022-08-19' } );
1915         $item2->discard_changes->update( { dateaccessioned => '2022-07-19' } );
1916         $item3->discard_changes->update( { dateaccessioned => '2022-09-19' } );
1917         is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1918             [ $item3->itemnumber, $item1->itemnumber, $item2->itemnumber ],
1919             "not a serial - order by date accessioned desc" );
1920     }
1921
1922     {    # Is a serial
1923
1924         my $sub_freq = $builder->build( { source => 'SubscriptionFrequency' } );
1925         my $sub_np =
1926           $builder->build( { source => 'SubscriptionNumberpattern' } );
1927         my $subscription = $builder->build_object(
1928             {
1929                 class => 'Koha::Subscriptions',
1930                 value => {
1931                     biblionumber  => $biblio->biblionumber,
1932                     periodicity   => $sub_freq->{id},
1933                     numberpattern => $sub_np->{id},
1934                     published_on_template => "[% publisheddatetext %] [% biblionumber %]",
1935                 }
1936             }
1937         );
1938         $builder->build_object(
1939             {
1940                 class => 'Koha::Subscription::Histories',
1941                 value => {
1942                     subscriptionid => $subscription->subscriptionid,
1943                     biblionumber   => $biblio->biblionumber
1944                 }
1945             }
1946         );
1947
1948         $biblio->update( { serial => 1 } );
1949         my $serialid1 =
1950           C4::Serials::NewIssue( "serialseq", $subscription->subscriptionid,
1951             $biblio->biblionumber, 1, undef, undef, "publisheddatetext",
1952             "notes", "routingnotes" );
1953         C4::Serials::AddItem2Serial( $serialid1, $item1->itemnumber );
1954         my $serialid2 =
1955           C4::Serials::NewIssue( "serialseq", $subscription->subscriptionid,
1956             $biblio->biblionumber, 1, undef, undef, "publisheddatetext",
1957             "notes", "routingnotes" );
1958         C4::Serials::AddItem2Serial( $serialid2, $item2->itemnumber );
1959         my $serialid3 =
1960           C4::Serials::NewIssue( "serialseq", $subscription->subscriptionid,
1961             $biblio->biblionumber, 1, undef, undef, "publisheddatetext",
1962             "notes", "routingnotes" );
1963         C4::Serials::AddItem2Serial( $serialid3, $item3->itemnumber );
1964         my $serial1 = Koha::Serials->find($serialid1);
1965         my $serial2 = Koha::Serials->find($serialid2);
1966         my $serial3 = Koha::Serials->find($serialid3);
1967
1968         # order_by serial.publisheddate
1969         $serial1->discard_changes->update( { publisheddate => '2022-09-19' } );
1970         $serial2->discard_changes->update( { publisheddate => '2022-07-19' } );
1971         $serial3->discard_changes->update( { publisheddate => '2022-08-19' } );
1972         is_deeply(
1973             [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1974             [ $item2->itemnumber, $item3->itemnumber, $item1->itemnumber ],
1975             "serial - order by publisheddate"
1976         );
1977
1978         # order_by me.enumchron
1979         $serial1->discard_changes->update({ publisheddate => '2022-08-19' });
1980         $serial2->discard_changes->update({ publisheddate => '2022-08-19' });
1981         $serial3->discard_changes->update({ publisheddate => '2022-08-19' });
1982         $item1->discard_changes->update( { enumchron => 'cc' } );
1983         $item2->discard_changes->update( { enumchron => 'bb' } );
1984         $item3->discard_changes->update( { enumchron => 'aa' } );
1985         is_deeply( [ map { $_->itemnumber } $biblio->items->search_ordered->as_list ],
1986             [ $item3->itemnumber, $item2->itemnumber, $item1->itemnumber ],
1987             "serial - order by enumchron" );
1988
1989         is( $serial1->publisheddatetext, "publisheddatetext " . $biblio->biblionumber, "Column publisheddatetext rendered correctly from template for serial1" );
1990         is( $serial2->publisheddatetext, "publisheddatetext " . $biblio->biblionumber, "Column publisheddatetext rendered correctly from template for serial2" );
1991         is( $serial3->publisheddatetext, "publisheddatetext " . $biblio->biblionumber, "Column publisheddatetext rendered correctly from template for serial3" );
1992
1993     }
1994
1995     $schema->storage->txn_rollback;
1996
1997 };
1998
1999 subtest 'filter_by_for_hold' => sub {
2000
2001     plan tests => 13;
2002
2003     $schema->storage->txn_begin;
2004
2005     my $biblio  = $builder->build_sample_biblio;
2006     my $library = $builder->build_object({ class => 'Koha::Libraries' });
2007
2008     t::lib::Mocks::mock_preference('IndependentBranches', 0); # more robust tests
2009
2010     is( $biblio->items->filter_by_for_hold->count, 0, 'no item yet' );
2011     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 1 } );
2012     is( $biblio->items->filter_by_for_hold->count, 0, 'no item for hold' );
2013     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => 0 } );
2014     is( $biblio->items->filter_by_for_hold->count, 1, '1 item for hold' );
2015     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, notforloan => -1 } );
2016     is( $biblio->items->filter_by_for_hold->count, 2, '2 items for hold' );
2017
2018     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 0, library => $library->id } );
2019     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itemlost => 1, library => $library->id } );
2020     is( $biblio->items->filter_by_for_hold->count, 3, '3 items for hold - itemlost' );
2021
2022     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 0, library => $library->id } );
2023     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, withdrawn => 1, library => $library->id } );
2024     is( $biblio->items->filter_by_for_hold->count, 4, '4 items for hold - withdrawn' );
2025
2026     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, damaged => 0 } );
2027     $builder->build_sample_item( { biblionumber => $biblio->biblionumber, damaged => 1 } );
2028     t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0);
2029     is( $biblio->items->filter_by_for_hold->count, 5, '5 items for hold - not damaged if not AllowHoldsOnDamagedItems' );
2030     t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1);
2031     is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - damaged if AllowHoldsOnDamagedItems' );
2032
2033     my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' });
2034     my $not_holdable_itemtype = $itemtype->itemtype;
2035     $builder->build_sample_item(
2036         {
2037             biblionumber => $biblio->biblionumber,
2038             itype        => $not_holdable_itemtype,
2039         }
2040     );
2041     Koha::CirculationRules->set_rule(
2042         {
2043             branchcode   => undef,
2044             itemtype     => $not_holdable_itemtype,
2045             rule_name    => 'holdallowed',
2046             rule_value   => 'not_allowed',
2047         }
2048     );
2049     is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - holdallowed=not_allowed' );
2050
2051     # Remove rule, test notforloan on itemtype
2052     Koha::CirculationRules->set_rule(
2053         {
2054             branchcode   => undef,
2055             itemtype     => $not_holdable_itemtype,
2056             rule_name    => 'holdallowed',
2057             rule_value   => undef,
2058         }
2059     );
2060     is( $biblio->items->filter_by_for_hold->count, 7, '7 items for hold - rule deleted' );
2061     $itemtype->notforloan(1)->store;
2062     is( $biblio->items->filter_by_for_hold->count, 6, '6 items for hold - notforloan' );
2063
2064     {
2065         my $mock_context = Test::MockModule->new('C4::Context');
2066         $mock_context->mock( 'only_my_library', 1 );
2067         $mock_context->mock( 'mybranch',        $library->id );
2068         is( $biblio->items->filter_by_for_hold->count, 2, '2 items for hold, filtered by IndependentBranches' );
2069     }
2070
2071     t::lib::Mocks::mock_preference('item-level_itypes', 0);
2072     $biblio->biblioitem->itemtype($not_holdable_itemtype)->store;
2073     is( $biblio->items->filter_by_for_hold->count, 0, '0 item-level_itypes=0' );
2074
2075     t::lib::Mocks::mock_preference('item-level_itypes', 1);
2076
2077     $schema->storage->txn_rollback;
2078 };
2079
2080 subtest 'filter_by_bookable' => sub {
2081     plan tests => 2;
2082
2083     $schema->storage->txn_begin;
2084
2085     my $biblio = $builder->build_sample_biblio;
2086
2087     # bookable items
2088     my $bookable_item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } );
2089
2090     # not bookable items
2091     my $non_bookable_item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 0 } );
2092     my $non_bookable_item2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 0 } );
2093
2094     is( $biblio->items->filter_by_bookable->count, 1, "filter_by_bookable returns the correct number of items" );
2095     is(
2096         $biblio->items->filter_by_bookable->next->itemnumber, $bookable_item1->itemnumber,
2097         "the correct item is returned from filter_by_bookable"
2098     );
2099
2100     $schema->storage->txn_rollback;
2101 };