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