3 # This file is part of Koha.
5 # Copyright (C) 2016 ByWater Solutions
6 # Copyright (C) 2022 Koha Development Team
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use Test::More tests => 31;
28 use t::lib::TestBuilder;
31 use C4::Circulation qw( AddIssue AddReturn );
32 use C4::Letters qw( GetPreparedLetter );
33 use C4::Members qw( IssueSlip );
36 use Koha::DateUtils qw( dt_from_string output_pref );
37 use Koha::ArticleRequests;
42 use Koha::AdditionalContents;
44 use Koha::Subscription;
48 use Koha::Notice::Messages;
49 use Koha::Notice::Templates;
50 use Koha::Patron::Modification;
52 my $schema = Koha::Database->schema;
53 $schema->storage->txn_begin();
55 my $builder = t::lib::TestBuilder->new();
57 my $dbh = C4::Context->dbh;
59 $dbh->do(q|DELETE FROM letter|);
61 my $now_value = dt_from_string();
62 my $mocked_datetime = Test::MockModule->new('DateTime');
63 $mocked_datetime->mock( 'now', sub { return $now_value->clone; } );
65 my $library = $builder->build( { source => 'Branch' } );
66 my $patron = $builder->build( { source => 'Borrower' } );
67 my $patron2 = $builder->build( { source => 'Borrower' } );
69 my $item = $builder->build_sample_item();
70 my $hold = $builder->build_object(
72 class => 'Koha::Holds',
74 borrowernumber => $patron->{borrowernumber},
75 biblionumber => $item->biblionumber
80 my $news = $builder->build_object(
82 class => 'Koha::AdditionalContents',
83 value => { title => 'a news title', content => 'a news content' }
86 my $serial = $builder->build_object( { class => 'Koha::Serials' } );
87 my $subscription = $builder->build_object( { class => 'Koha::Subscriptions' } );
88 my $suggestion = $builder->build_object( { class => 'Koha::Suggestions' } );
89 my $checkout = $builder->build_object(
90 { class => 'Koha::Checkouts', value => { itemnumber => $item->id } } );
91 my $modification = $builder->build_object(
93 class => 'Koha::Patron::Modifications',
95 verification_token => "TEST",
96 changed_fields => 'firstname,surname'
104 $dbh->prepare(q{INSERT INTO letter (module, code, name, title, content) VALUES ('test',?,'Test',?,?)});
106 $sth->execute( "TEST_PATRON", "[% borrower.firstname %]", "[% borrower.id %]" );
107 $prepared_letter = GetPreparedLetter(
110 letter_code => 'TEST_PATRON',
112 borrowers => $patron->{borrowernumber},
116 is( $prepared_letter->{content}, $patron->{borrowernumber}, 'Patron object used correctly with scalar for content' );
117 is( $prepared_letter->{title}, $patron->{firstname}, 'Patron object used correctly with scalar for title' );
119 $prepared_letter = GetPreparedLetter(
122 letter_code => 'TEST_PATRON',
124 borrowers => $patron,
128 is( $prepared_letter->{content}, $patron->{borrowernumber}, 'Patron object used correctly with hashref for content' );
129 is( $prepared_letter->{title}, $patron->{firstname}, 'Patron object used correctly with hashref for title' );
131 $prepared_letter = GetPreparedLetter(
134 letter_code => 'TEST_PATRON',
136 borrowers => [ $patron->{borrowernumber} ],
140 is( $prepared_letter->{content}, $patron->{borrowernumber}, 'Patron object used correctly with arrayref for content' );
141 is( $prepared_letter->{title}, $patron->{firstname}, 'Patron object used correctly with arrayref for title' );
143 $prepared_letter = GetPreparedLetter(
146 letter_code => 'TEST_PATRON',
148 borrower => scalar Koha::Patrons->find( $patron->{borrowernumber} ),
152 is( $prepared_letter->{content}, $patron->{borrowernumber}, 'Patron object used correctly as object' );
154 $sth->execute( "TEST_BIBLIO", "[% biblio.title %]", "[% biblio.id %]" );
155 $prepared_letter = GetPreparedLetter(
158 letter_code => 'TEST_BIBLIO',
160 biblio => $item->biblionumber,
164 is( $prepared_letter->{content}, $item->biblionumber, 'Biblio object used correctly for content' );
165 is( $prepared_letter->{title}, $item->biblio->title, 'Biblio object used correctly for title' );
167 $sth->execute( "TEST_LIBRARY", "[% branch.branchcode %]", "[% branch.id %]" );
168 $prepared_letter = GetPreparedLetter(
171 letter_code => 'TEST_LIBRARY',
173 branches => $library->{branchcode}
177 is( $prepared_letter->{content}, $library->{branchcode}, 'Library object used correctly for content' );
178 is( $prepared_letter->{title}, $library->{branchcode}, 'Library object used correctly for title' );
180 $sth->execute( "TEST_ITEM", "[% item.barcode %]", "[% item.id %]" );
181 $prepared_letter = GetPreparedLetter(
184 letter_code => 'TEST_ITEM',
190 is( $prepared_letter->{content}, $item->id(), 'Item object used correctly for content' );
191 is( $prepared_letter->{title}, $item->barcode, 'Item object used correctly for title' );
193 $sth->execute( "TEST_NEWS", "[% additional_content.id %]", "[% additional_content.id %]" );
194 $prepared_letter = GetPreparedLetter(
197 letter_code => 'TEST_NEWS',
199 additional_contents => $news->id()
203 is( $prepared_letter->{content}, $news->id(), 'News object used correctly for content' );
204 is( $prepared_letter->{title}, $news->id(), 'News object used correctly for title' );
206 $sth->execute( "TEST_HOLD", "[% hold.borrowernumber %]", "[% hold.id %]" );
207 $prepared_letter = GetPreparedLetter(
210 letter_code => 'TEST_HOLD',
212 reserves => { borrowernumber => $patron->{borrowernumber}, biblionumber => $item->biblionumber },
216 is( $prepared_letter->{content}, $hold->id(), 'Hold object used correctly for content' );
217 is( $prepared_letter->{title}, $hold->borrowernumber, 'Hold object used correctly for title' );
220 $prepared_letter = GetPreparedLetter(
223 letter_code => 'TEST_HOLD',
225 reserves => [ $patron->{borrowernumber}, $item->biblionumber ],
231 like( $croak, qr{^Multiple foreign keys \(table reserves\) should be passed using an hashref.*}, "GetPreparedLetter should not be called with arrayref for multiple FK" );
234 $prepared_letter = GetPreparedLetter(
237 letter_code => 'TEST_HOLD',
239 'branches' => $library,
240 'borrowers' => $patron,
241 'biblio' => $item->biblionumber,
242 'biblioitems' => $item->biblioitemnumber,
243 'reserves' => $hold->unblessed,
244 'items' => $hold->itemnumber,
248 is( $prepared_letter->{content}, $hold->id(), 'Hold object used correctly' );
250 $sth->execute( "TEST_SERIAL", "[% serial.id %]", "[% serial.id %]" );
251 $prepared_letter = GetPreparedLetter(
254 letter_code => 'TEST_SERIAL',
256 serial => $serial->id()
260 is( $prepared_letter->{content}, $serial->id(), 'Serial object used correctly' );
262 $sth->execute( "TEST_SUBSCRIPTION", "[% subscription.id %]", "[% subscription.id %]" );
263 $prepared_letter = GetPreparedLetter(
266 letter_code => 'TEST_SUBSCRIPTION',
268 subscription => $subscription->id()
272 is( $prepared_letter->{content}, $subscription->id(), 'Subscription object used correctly' );
274 $sth->execute( "TEST_SUGGESTION", "[% suggestion.id %]", "[% suggestion.id %]" );
275 $prepared_letter = GetPreparedLetter(
278 letter_code => 'TEST_SUGGESTION',
280 suggestions => $suggestion->id()
284 is( $prepared_letter->{content}, $suggestion->id(), 'Suggestion object used correctly' );
286 $sth->execute( "TEST_ISSUE", "[% checkout.id %]", "[% checkout.id %]" );
287 $prepared_letter = GetPreparedLetter(
290 letter_code => 'TEST_ISSUE',
292 issues => $item->id()
296 is( $prepared_letter->{content}, $checkout->id(), 'Checkout object used correctly' );
298 $sth->execute( "TEST_MODIFICATION", "[% patron_modification.id %]", "[% patron_modification.id %]" );
299 $prepared_letter = GetPreparedLetter(
302 letter_code => 'TEST_MODIFICATION',
304 borrower_modifications => $modification->verification_token,
308 is( $prepared_letter->{content}, $modification->id(), 'Patron modification object used correctly' );
310 subtest 'regression tests' => sub {
313 my $library = $builder->build( { source => 'Branch' } );
314 my $itemtype = $builder->build_object({ class => 'Koha::ItemTypes' })->store->itemtype;
316 my $item1 = $builder->build_sample_item(
318 barcode => 'a_t_barcode',
319 library => $library->{branchcode},
321 itemcallnumber => 'itemcallnumber1',
324 my $biblio1 = $item1->biblio->unblessed;
325 $item1 = $item1->unblessed;
326 my $item2 = $builder->build_sample_item(
328 barcode => 'another_t_barcode',
329 library => $library->{branchcode},
331 itemcallnumber => 'itemcallnumber2',
334 my $biblio2 = $item2->biblio->unblessed;
335 $item2 = $item2->unblessed;
336 my $item3 = $builder->build_sample_item(
338 barcode => 'another_t_barcode_3',
339 library => $library->{branchcode},
341 itemcallnumber => 'itemcallnumber3',
344 my $biblio3 = $item3->biblio->unblessed;
345 $item3 = $item3->unblessed;
347 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
349 subtest 'ACQ_NOTIF_ON_RECEIV ' => sub {
351 my $code = 'ACQ_NOTIF_ON_RECEIV';
352 my $branchcode = $library->{branchcode};
353 my $order = $builder->build({ source => 'Aqorder' });
356 Dear <<borrowers.firstname>> <<borrowers.surname>>,
357 The order <<aqorders.ordernumber>> (<<biblio.title>>) has been received.
360 my $params = { code => $code, branchcode => $branchcode, tables => { branches => $library, borrowers => $patron, biblio => $biblio1, aqorders => $order } };
361 my $letter = process_letter( { template => $template, %$params });
363 Dear [% borrower.firstname %] [% borrower.surname %],
364 The order [% order.ordernumber %] ([% biblio.title %]) has been received.
367 my $tt_letter = process_letter( { template => $tt_template, %$params });
369 is( $tt_letter->{content}, $letter->{content}, 'Verified letter content' );
372 subtest 'AR_*' => sub {
374 my $code = 'AR_CANCELED';
375 my $branchcode = $library->{branchcode};
378 <<borrowers.firstname>> <<borrowers.surname>> (<<borrowers.cardnumber>>)
380 Your request for an article from <<biblio.title>> (<<items.barcode>>) has been canceled for the following reason:
382 <<article_requests.notes>>
385 Title: <<article_requests.title>>
386 Author: <<article_requests.author>>
387 Volume: <<article_requests.volume>>
388 Issue: <<article_requests.issue>>
389 Date: <<article_requests.date>>
390 Pages: <<article_requests.pages>>
391 Chapters: <<article_requests.chapters>>
392 Notes: <<article_requests.patron_notes>>
394 reset_template( { template => $template, code => $code, module => 'circulation' } );
395 my $article_request = $builder->build_object(
397 class => 'Koha::ArticleRequests',
398 value => { debit_id => undef }
401 $article_request->cancel;
402 my $letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
405 [% borrower.firstname %] [% borrower.surname %] ([% borrower.cardnumber %])
407 Your request for an article from [% biblio.title %] ([% item.barcode %]) has been canceled for the following reason:
409 [% article_request.notes %]
412 Title: [% article_request.title %]
413 Author: [% article_request.author %]
414 Volume: [% article_request.volume %]
415 Issue: [% article_request.issue %]
416 Date: [% article_request.date %]
417 Pages: [% article_request.pages %]
418 Chapters: [% article_request.chapters %]
419 Notes: [% article_request.patron_notes %]
421 reset_template( { template => $tt_template, code => $code, module => 'circulation' } );
422 $article_request->cancel;
423 my $tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
424 is( $tt_letter->content, $letter->content, 'Compare AR_* notices' );
425 isnt( $tt_letter->message_id, $letter->message_id, 'Comparing AR_* notices should compare 2 different messages' );
428 subtest 'CHECKOUT+CHECKIN' => sub {
431 my $checkout_code = 'CHECKOUT';
432 my $checkin_code = 'CHECKIN';
434 my $dbh = C4::Context->dbh;
435 # Enable notification for CHECKOUT - Things are hardcoded here but should work with default data
436 $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ? )|, undef, $patron->{borrowernumber}, 6 );
437 my $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
438 $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
439 # Enable notification for CHECKIN - Things are hardcoded here but should work with default data
440 $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ? )|, undef, $patron->{borrowernumber}, 5 );
441 $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
442 $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
445 my $checkout_template = q|
446 The following items have been checked out:
450 Thank you for visiting <<branches.branchname>>.
452 reset_template( { template => $checkout_template, code => $checkout_code, module => 'circulation' } );
453 my $checkin_template = q[
454 The following items have been checked out:
456 <<biblio.title>> was due on <<old_issues.date_due | dateonly>>
458 Thank you for visiting <<branches.branchname>>.
460 reset_template( { template => $checkin_template, code => $checkin_code, module => 'circulation' } );
462 C4::Circulation::AddIssue( $patron, $item1->{barcode} );
463 my $first_checkout_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
464 C4::Circulation::AddIssue( $patron, $item2->{barcode} );
465 my $second_checkout_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
467 AddReturn( $item1->{barcode} );
468 my $first_checkin_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
469 AddReturn( $item2->{barcode} );
470 my $second_checkin_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
472 Koha::Notice::Messages->delete;
475 $checkout_template = q|
476 The following items have been checked out:
480 Thank you for visiting [% branch.branchname %].
482 reset_template( { template => $checkout_template, code => $checkout_code, module => 'circulation' } );
483 $checkin_template = q[
484 The following items have been checked out:
486 [% biblio.title %] was due on [% old_checkout.date_due | $KohaDates %]
488 Thank you for visiting [% branch.branchname %].
490 reset_template( { template => $checkin_template, code => $checkin_code, module => 'circulation' } );
492 C4::Circulation::AddIssue( $patron, $item1->{barcode} );
493 my $first_checkout_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
494 C4::Circulation::AddIssue( $patron, $item2->{barcode} );
495 my $second_checkout_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
497 AddReturn( $item1->{barcode} );
498 my $first_checkin_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
499 AddReturn( $item2->{barcode} );
500 my $second_checkin_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
502 is( $first_checkout_tt_letter->content, $first_checkout_letter->content, 'Verify first checkout letter' );
503 is( $second_checkout_tt_letter->content, $second_checkout_letter->content, 'Verify second checkout letter' );
504 is( $first_checkin_tt_letter->content, $first_checkin_letter->content, 'Verify first checkin letter' );
505 is( $second_checkin_tt_letter->content, $second_checkin_letter->content, 'Verify second checkin letter' );
509 subtest 'DUEDGST|count' => sub {
512 my $code = 'DUEDGST';
514 my $dbh = C4::Context->dbh;
515 # Enable notification for DUEDGST - Things are hardcoded here but should work with default data
516 $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ? )|, undef, $patron->{borrowernumber}, 1 );
517 my $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
518 $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
522 substitute => { count => 42 },
526 You have <<count>> items due
528 my $letter = process_letter( { template => $template, %$params });
531 You have [% count %] items due
533 my $tt_letter = process_letter( { template => $tt_template, %$params });
534 is( $tt_letter->{content}, $letter->{content}, );
537 subtest 'HOLD_SLIP|dates|today' => sub {
540 my $code = 'HOLD_SLIP';
542 my $reserve_id1 = C4::Reserves::AddReserve(
544 branchcode => $library->{branchcode},
545 borrowernumber => $patron->{borrowernumber},
546 biblionumber => $biblio1->{biblionumber},
548 itemnumber => $item1->{itemnumber},
551 my $reserve_id2 = C4::Reserves::AddReserve(
553 branchcode => $library->{branchcode},
554 borrowernumber => $patron->{borrowernumber},
555 biblionumber => $biblio1->{biblionumber},
557 itemnumber => $item1->{itemnumber},
560 my $reserve_id3 = C4::Reserves::AddReserve(
562 branchcode => $library->{branchcode},
563 borrowernumber => $patron->{borrowernumber},
564 biblionumber => $biblio2->{biblionumber},
565 notes => "another note",
566 itemnumber => $item2->{itemnumber},
570 my $template = <<EOF;
571 <h5>Date: <<today>></h5>
573 <h3> Transfer to/Hold in <<branches.branchname>></h3>
575 <h3><<borrowers.surname>>, <<borrowers.firstname>></h3>
578 <li><<borrowers.cardnumber>></li>
579 <li><<borrowers.phone>></li>
580 <li> <<borrowers.address>><br />
581 <<borrowers.address2>><br />
582 <<borrowers.city>> <<borrowers.zipcode>>
584 <li><<borrowers.email>></li>
587 <h3>ITEM ON HOLD</h3>
588 <h4><<biblio.title>></h4>
589 <h5><<biblio.author>></h5>
591 <li><<items.barcode>></li>
592 <li><<items.itemcallnumber>></li>
593 <li><<reserves.waitingdate>></li>
596 <pre><<reserves.reserve_id>>=<<reserves.reservenotes>></pre>
600 reset_template( { template => $template, code => $code, module => 'circulation' } );
601 my $letter_for_item1 = C4::Reserves::ReserveSlip( { branchcode => $library->{branchcode}, reserve_id => $reserve_id1 } );
602 my $letter_for_item2 = C4::Reserves::ReserveSlip( { branchcode => $library->{branchcode}, reserve_id => $reserve_id3 } );
604 my $tt_template = <<EOF;
605 <h5>Date: [% today | \$KohaDates with_hours => 1 %]</h5>
607 <h3> Transfer to/Hold in [% branch.branchname %]</h3>
609 <h3>[% borrower.surname %], [% borrower.firstname %]</h3>
612 <li>[% borrower.cardnumber %]</li>
613 <li>[% borrower.phone %]</li>
614 <li> [% borrower.address %]<br />
615 [% borrower.address2 %]<br />
616 [% borrower.city %] [% borrower.zipcode %]
618 <li>[% borrower.email %]</li>
621 <h3>ITEM ON HOLD</h3>
622 <h4>[% biblio.title %]</h4>
623 <h5>[% biblio.author %]</h5>
625 <li>[% item.barcode %]</li>
626 <li>[% item.itemcallnumber %]</li>
627 <li>[% hold.waitingdate | \$KohaDates %]</li>
630 <pre>[% hold.reserve_id %]=[% hold.reservenotes %]</pre>
634 reset_template( { template => $tt_template, code => $code, module => 'circulation' } );
635 my $tt_letter_for_item1 = C4::Reserves::ReserveSlip( { branchcode => $library->{branchcode}, reserve_id => $reserve_id1 } );
636 my $tt_letter_for_item2 = C4::Reserves::ReserveSlip( { branchcode => $library->{branchcode}, reserve_id => $reserve_id3 } );
638 is( $tt_letter_for_item1->{content}, $letter_for_item1->{content}, );
639 is( $tt_letter_for_item2->{content}, $letter_for_item2->{content}, );
642 subtest 'ISSUESLIP|checkedout|repeat|news' => sub {
645 my $code = 'ISSUESLIP';
646 my $now = dt_from_string;
647 my $one_minute_ago = dt_from_string->subtract( minutes => 1 );
649 my $branchcode = $library->{branchcode};
651 Koha::AdditionalContents->delete;
652 my $news_item = $builder->build_object(
654 class => 'Koha::AdditionalContents',
658 branchcode => $branchcode,
660 title => "A wonderful news",
661 content => "This is the wonderful news.",
662 expirationdate => undef,
663 published_on => $one_minute_ago
669 my $template = <<EOF;
670 <h3><<branches.branchname>></h3>
671 Checked out to <<borrowers.title>> <<borrowers.firstname>> <<borrowers.initials>> <<borrowers.surname>> <br />
672 (<<borrowers.cardnumber>>) <br />
679 <<biblio.title>> <br />
680 Barcode: <<items.barcode>><br />
681 Date due: <<issues.date_due | dateonly>><br />
688 <<biblio.title>> <br />
689 Barcode: <<items.barcode>><br />
690 Date due: <<issues.date_due | dateonly>><br />
696 <h4 style="text-align: center; font-style:italic;">News</h4>
698 <div class="newsitem">
699 <h5 style="margin-bottom: 1px; margin-top: 1px"><b><<additional_contents.title>></b></h5>
700 <p style="margin-bottom: 1px; margin-top: 1px"><<additional_contents.content>></p>
701 <p class="newsfooter" style="font-size: 8pt; font-style:italic; margin-bottom: 1px; margin-top: 1px">Posted on <<additional_contents.published_on>></p>
707 reset_template( { template => $template, code => $code, module => 'circulation' } );
709 my $checkout = C4::Circulation::AddIssue( $patron, $item1->{barcode} ); # Add a first checkout
710 $checkout->set( { timestamp => $now, issuedate => $one_minute_ago } )->store;
711 my $first_slip = C4::Members::IssueSlip( $branchcode, $patron->{borrowernumber} );
713 $checkout = C4::Circulation::AddIssue( $patron, $item2->{barcode} ); # Add a second checkout
714 $checkout->set( { timestamp => $now, issuedate => $now } )->store;
715 my $yesterday = dt_from_string->subtract( days => 1 );
716 C4::Circulation::AddIssue( $patron, $item3->{barcode}, $yesterday ); # Add an overdue
717 my $second_slip = C4::Members::IssueSlip( $branchcode, $patron->{borrowernumber} );
720 AddReturn( $item1->{barcode} );
721 AddReturn( $item2->{barcode} );
722 AddReturn( $item3->{barcode} );
725 my $tt_template = <<EOF;
726 <h3>[% branch.branchname %]</h3>
727 Checked out to [% borrower.title %] [% borrower.firstname %] [% borrower.initials %] [% borrower.surname %] <br />
728 ([% borrower.cardnumber %]) <br />
730 [% today | \$KohaDates with_hours => 1 %]<br />
733 [% FOREACH checkout IN checkouts %]
734 [%~ SET item = checkout.item %]
735 [%~ SET biblio = checkout.item.biblio %]
737 [% biblio.title %] <br />
738 Barcode: [% item.barcode %]<br />
739 Date due: [% checkout.date_due | \$KohaDates %]<br />
744 [% FOREACH overdue IN overdues %]
745 [%~ SET item = overdue.item %]
746 [%~ SET biblio = overdue.item.biblio %]
748 [% biblio.title %] <br />
749 Barcode: [% item.barcode %]<br />
750 Date due: [% overdue.date_due | \$KohaDates %]<br />
756 <h4 style="text-align: center; font-style:italic;">News</h4>
757 [% FOREACH n IN additional_contents %]
758 <div class="newsitem">
759 <h5 style="margin-bottom: 1px; margin-top: 1px"><b>[% n.title %]</b></h5>
760 <p style="margin-bottom: 1px; margin-top: 1px">[% n.content %]</p>
761 <p class="newsfooter" style="font-size: 8pt; font-style:italic; margin-bottom: 1px; margin-top: 1px">Posted on [% n.published_on | \$KohaDates %]</p>
767 reset_template( { template => $tt_template, code => $code, module => 'circulation' } );
769 $checkout = C4::Circulation::AddIssue( $patron, $item1->{barcode} ); # Add a first checkout
770 $checkout->set( { timestamp => $now, issuedate => $one_minute_ago } )->store;
771 my $first_tt_slip = C4::Members::IssueSlip( $branchcode, $patron->{borrowernumber} );
773 $checkout = C4::Circulation::AddIssue( $patron, $item2->{barcode} ); # Add a second checkout
774 $checkout->set( { timestamp => $now, issuedate => $now } )->store;
775 C4::Circulation::AddIssue( $patron, $item3->{barcode}, $yesterday ); # Add an overdue
776 my $second_tt_slip = C4::Members::IssueSlip( $branchcode, $patron->{borrowernumber} );
778 # There is too many line breaks generated by the historic syntax
779 $second_slip->{content} =~ s|</p>\n\n\n<p>|</p>\n\n<p>|s;
781 my $news_item_title = $news_item->title;
782 like( $first_slip->{content}, qr{$news_item_title} );
783 is( $first_tt_slip->{content}, $first_slip->{content}, );
784 is( $second_tt_slip->{content}, $second_slip->{content}, );
787 AddReturn( $item1->{barcode} );
788 AddReturn( $item2->{barcode} );
789 AddReturn( $item3->{barcode} );
792 subtest 'ODUE|items.content|item' => sub {
797 my $branchcode = $library->{branchcode};
800 # FIXME items.fine does not work with TT notices
802 # <item> should contain Fine: <<items.fine>></item>
803 my $template = <<EOF;
804 Dear <<borrowers.firstname>> <<borrowers.surname>>,
806 According to our current records, you have items that are overdue.Your library does not charge late fines, but please return or renew them at the branch below as soon as possible.
808 <<branches.branchname>>
809 <<branches.branchaddress1>>
810 <<branches.branchaddress2>> <<branches.branchaddress3>>
811 Phone: <<branches.branchphone>>
812 Fax: <<branches.branchfax>>
813 Email: <<branches.branchemail>>
815 If you have registered a password with the library, and you have a renewal available, you may renew online. If an item becomes more than 30 days overdue, you will be unable to use your library card until the item is returned.
817 The following item(s) is/are currently overdue:
819 <item>"<<biblio.title>>" by <<biblio.author>>, <<items.itemcallnumber>>, Barcode: <<items.barcode>></item>
823 Thank-you for your prompt attention to this matter.
825 <<branches.branchname>> Staff
828 reset_template( { template => $template, code => $code, module => 'circulation' } );
830 my $yesterday = dt_from_string->subtract( days => 1 );
831 my $two_days_ago = dt_from_string->subtract( days => 2 );
832 my $issue1 = C4::Circulation::AddIssue( $patron, $item1->{barcode} ); # Add a first checkout
833 my $issue2 = C4::Circulation::AddIssue( $patron, $item2->{barcode}, $yesterday ); # Add an first overdue
834 my $issue3 = C4::Circulation::AddIssue( $patron, $item3->{barcode}, $two_days_ago ); # Add an second overdue
835 $issue1 = $issue1->unblessed;
836 $issue2 = $issue2->unblessed;
837 $issue3 = $issue3->unblessed;
840 my @item_fields = qw( date_due title barcode author itemnumber );
841 my $items_content = C4::Letters::get_item_content( { item => { %$item1, %$biblio1, %$issue1 }, item_content_fields => \@item_fields, dateonly => 1 } );
842 $items_content .= C4::Letters::get_item_content( { item => { %$item2, %$biblio2, %$issue2 }, item_content_fields => \@item_fields, dateonly => 1 } );
843 $items_content .= C4::Letters::get_item_content( { item => { %$item3, %$biblio3, %$issue3 }, item_content_fields => \@item_fields, dateonly => 1 } );
845 my @items = ( $item1, $item2, $item3 );
846 my $letter = C4::Overdues::parse_overdues_letter(
848 letter_code => $code,
849 borrowernumber => $patron->{borrowernumber},
850 branchcode => $library->{branchcode},
853 bib => $library->{branchname},
854 'items.content' => $items_content,
855 count => scalar( @items ),
856 message_transport_type => 'email',
862 AddReturn( $item1->{barcode} );
863 AddReturn( $item2->{barcode} );
864 AddReturn( $item3->{barcode} );
868 my $tt_template = <<EOF;
869 Dear [% borrower.firstname %] [% borrower.surname %],
871 According to our current records, you have items that are overdue.Your library does not charge late fines, but please return or renew them at the branch below as soon as possible.
873 [% branch.branchname %]
874 [% branch.branchaddress1 %]
875 [% branch.branchaddress2 %] [% branch.branchaddress3 %]
876 Phone: [% branch.branchphone %]
877 Fax: [% branch.branchfax %]
878 Email: [% branch.branchemail %]
880 If you have registered a password with the library, and you have a renewal available, you may renew online. If an item becomes more than 30 days overdue, you will be unable to use your library card until the item is returned.
882 The following item(s) is/are currently overdue:
884 [% FOREACH overdue IN overdues %]
885 [%~ SET item = overdue.item ~%]
886 "[% item.biblio.title %]" by [% item.biblio.author %], [% item.itemcallnumber %], Barcode: [% item.barcode %]
888 [% FOREACH overdue IN overdues %]
889 [%~ SET item = overdue.item ~%]
890 [% overdue.date_due | \$KohaDates %]\t[% item.biblio.title %]\t[% item.barcode %]\t[% item.biblio.author %]\t[% item.itemnumber %]
893 Thank-you for your prompt attention to this matter.
895 [% branch.branchname %] Staff
898 reset_template( { template => $tt_template, code => $code, module => 'circulation' } );
900 C4::Circulation::AddIssue( $patron, $item1->{barcode} ); # Add a first checkout
901 C4::Circulation::AddIssue( $patron, $item2->{barcode}, $yesterday ); # Add an first overdue
902 C4::Circulation::AddIssue( $patron, $item3->{barcode}, $two_days_ago ); # Add an second overdue
904 my $tt_letter = C4::Overdues::parse_overdues_letter(
906 letter_code => $code,
907 borrowernumber => $patron->{borrowernumber},
908 branchcode => $library->{branchcode},
911 bib => $library->{branchname},
912 'items.content' => $items_content,
913 count => scalar( @items ),
914 message_transport_type => 'email',
919 is( $tt_letter->{content}, $letter->{content}, );
922 subtest 'Bug 19743 - Header and Footer should be updated on each item for checkin / checkout / renewal notices' => sub {
925 my $checkout_code = 'CHECKOUT';
926 my $checkin_code = 'CHECKIN';
928 my $dbh = C4::Context->dbh;
929 $dbh->do("DELETE FROM letter");
930 $dbh->do("DELETE FROM issues");
931 $dbh->do("DELETE FROM message_queue");
933 # Enable notification for CHECKOUT - Things are hardcoded here but should work with default data
934 $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ? )|, undef, $patron->{borrowernumber}, 6 );
935 my $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
936 $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
937 # Enable notification for CHECKIN - Things are hardcoded here but should work with default data
938 $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ? )|, undef, $patron->{borrowernumber}, 5 );
939 $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
940 $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
942 my $checkout_template = q|
943 <<branches.branchname>>
947 reset_template( { template => $checkout_template, code => $checkout_code, module => 'circulation' } );
948 my $checkin_template = q[
949 <<branches.branchname>>
953 reset_template( { template => $checkin_template, code => $checkin_code, module => 'circulation' } );
955 my $issue = C4::Circulation::AddIssue( $patron, $item1->{barcode} );
956 my $first_checkout_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
958 my $library_object = Koha::Libraries->find( $issue->branchcode );
959 my $old_branchname = $library_object->branchname;
960 my $new_branchname = "Kyle M Hall Memorial Library";
962 # Change branch name for second checkout notice
963 $library_object->branchname($new_branchname);
964 $library_object->store();
966 C4::Circulation::AddIssue( $patron, $item2->{barcode} );
967 my $second_checkout_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
969 # Restore old name for first checkin notice
970 $library_object->branchname( $old_branchname );
971 $library_object->store();
973 AddReturn( $item1->{barcode} );
974 my $first_checkin_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
976 # Change branch name for second checkin notice
977 $library_object->branchname($new_branchname);
978 $library_object->store();
980 AddReturn( $item2->{barcode} );
981 my $second_checkin_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
983 # Restore old name for first TT checkout notice
984 $library_object->branchname( $old_branchname );
985 $library_object->store();
987 Koha::Notice::Messages->delete;
990 $checkout_template = q|
991 [% branch.branchname %]
995 reset_template( { template => $checkout_template, code => $checkout_code, module => 'circulation' } );
996 $checkin_template = q[
997 [% branch.branchname %]
1001 reset_template( { template => $checkin_template, code => $checkin_code, module => 'circulation' } );
1003 C4::Circulation::AddIssue( $patron, $item1->{barcode} );
1004 my $first_checkout_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
1006 # Change branch name for second checkout notice
1007 $library_object->branchname($new_branchname);
1008 $library_object->store();
1010 C4::Circulation::AddIssue( $patron, $item2->{barcode} );
1011 my $second_checkout_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
1013 # Restore old name for first checkin notice
1014 $library_object->branchname( $old_branchname );
1015 $library_object->store();
1017 AddReturn( $item1->{barcode} );
1018 my $first_checkin_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
1020 # Change branch name for second checkin notice
1021 $library_object->branchname($new_branchname);
1022 $library_object->store();
1024 AddReturn( $item2->{barcode} );
1025 my $second_checkin_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
1027 my $first_letter = qq[
1030 my $second_letter = qq[
1035 is( $first_checkout_letter->content, $first_letter, 'Verify first checkout letter' );
1036 is( $second_checkout_letter->content, $second_letter, 'Verify second checkout letter' );
1037 is( $first_checkin_letter->content, $first_letter, 'Verify first checkin letter' );
1038 is( $second_checkin_letter->content, $second_letter, 'Verify second checkin letter' );
1040 is( $first_checkout_tt_letter->content, $first_letter, 'Verify TT first checkout letter' );
1041 is( $second_checkout_tt_letter->content, $second_letter, 'Verify TT second checkout letter' );
1042 is( $first_checkin_tt_letter->content, $first_letter, 'Verify TT first checkin letter' );
1043 is( $second_checkin_tt_letter->content, $second_letter, 'Verify TT second checkin letter' );
1048 subtest 'loops' => sub {
1051 my $module = "TEST";
1053 subtest 'primary key is AI' => sub {
1055 my $patron_1 = $builder->build({ source => 'Borrower' });
1056 my $patron_2 = $builder->build({ source => 'Borrower' });
1058 my $template = q|[% FOREACH patron IN borrowers %][% patron.surname %][% END %]|;
1059 reset_template( { template => $template, code => $code, module => $module } );
1060 my $letter = GetPreparedLetter( module => $module, letter_code => $code, loops => { borrowers => [ $patron_1->{borrowernumber}, $patron_2->{borrowernumber} ] } );
1061 my $expected_letter = join '', ( $patron_1->{surname}, $patron_2->{surname} );
1062 is( $letter->{content}, $expected_letter, );
1065 subtest 'foreign key is used' => sub {
1067 my $patron_1 = $builder->build({ source => 'Borrower' });
1068 my $patron_2 = $builder->build({ source => 'Borrower' });
1069 my $checkout_1 = $builder->build({ source => 'Issue', value => { borrowernumber => $patron_1->{borrowernumber} } } );
1070 my $checkout_2 = $builder->build({ source => 'Issue', value => { borrowernumber => $patron_1->{borrowernumber} } } );
1072 my $template = q|[% FOREACH checkout IN checkouts %][% checkout.issue_id %][% END %]|;
1073 reset_template( { template => $template, code => $code, module => $module } );
1074 my $letter = GetPreparedLetter( module => $module, letter_code => $code, loops => { issues => [ $checkout_1->{itemnumber}, $checkout_2->{itemnumber} ] } );
1075 my $expected_letter = join '', ( $checkout_1->{issue_id}, $checkout_2->{issue_id} );
1076 is( $letter->{content}, $expected_letter, );
1080 subtest 'add_tt_filters' => sub {
1083 my $module = "TEST";
1085 my $patron = $builder->build_object(
1087 class => 'Koha::Patrons',
1088 value => { surname => "with_punctuation_" }
1091 my $biblio = $builder->build_object(
1092 { class => 'Koha::Biblios', value => { title => "with_punctuation_" } }
1094 my $biblioitem = $builder->build_object(
1096 class => 'Koha::Biblioitems',
1098 biblionumber => $biblio->biblionumber,
1099 isbn => "with_punctuation_"
1104 my $template = q|patron=[% borrower.surname %];biblio=[% biblio.title %];biblioitems=[% biblioitem.isbn %]|;
1105 reset_template( { template => $template, code => $code, module => $module } );
1106 my $letter = GetPreparedLetter(
1108 letter_code => $code,
1110 borrowers => $patron->borrowernumber,
1111 biblio => $biblio->biblionumber,
1112 biblioitems => $biblioitem->biblioitemnumber
1115 my $expected_letter = q|patron=with_punctuation_;biblio=with_punctuation;biblioitems=with_punctuation|;
1116 is( $letter->{content}, $expected_letter, "Pre-processing should call TT plugin to remove punctuation if table is biblio or biblioitems");
1119 subtest 'Handle includes' => sub {
1121 my $cgi = CGI->new();
1122 my $code = 'TEST_INCLUDE';
1124 $builder->build_object( { class => 'Koha::Account::Lines', value => { credit_type_code => 'PAYMENT', status => 'CANCELLED' } } );
1125 my $template = <<EOF;
1127 [%- PROCESS 'accounts.inc' -%]
1128 [%- PROCESS account_type_description account=credit -%]
1130 reset_template({ template => $template, code => $code, module => 'test' });
1131 my $letter = GetPreparedLetter(
1133 letter_code => $code,
1135 credits => $account->accountlines_id
1138 is($letter->{content},'<span>Payment</span><span> (Cancelled)</span>', "Include used in notice");
1141 subtest 'Dates formatting' => sub {
1143 my $code = 'TEST_DATE';
1144 t::lib::Mocks::mock_preference('dateformat', 'metric'); # MM/DD/YYYY
1145 my $biblio = $builder->build_object(
1147 class => 'Koha::Biblios',
1149 timestamp => '2018-12-13 20:21:22',
1150 datecreated => '2018-12-13'
1154 my $template = <<EOF;
1155 [%- USE KohaDates -%]
1156 [% biblio.timestamp %]
1157 [% biblio.timestamp | \$KohaDates %]
1158 [% biblio.timestamp | \$KohaDates with_hours => 1 %]
1160 [% biblio.datecreated %]
1161 [% biblio.datecreated | \$KohaDates %]
1162 [% biblio.datecreated | \$KohaDates with_hours => 1 %]
1164 [% biblio.timestamp | \$KohaDates dateformat => 'iso' %]
1165 [% KohaDates.output_preference( str => biblio.timestamp, dateformat => 'iso' ) %]
1166 [% KohaDates.output_preference( str => biblio.timestamp, dateformat => 'iso', dateonly => 1 ) %]
1168 reset_template({ template => $template, code => $code, module => 'test' });
1169 my $letter = GetPreparedLetter(
1171 letter_code => $code,
1173 biblio => $biblio->biblionumber,
1176 my $expected_content = sprintf("%s\n%s\n%s\n\n%s\n%s\n%s\n\n%s\n%s\n%s\n",
1177 '2018-12-13 20:21:22',
1189 is( $letter->{content}, $expected_content );
1192 subtest 'Execute TT process in a DB transaction' => sub {
1194 my $code = 'TEST_TXN';
1195 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1196 my $template = <<EOF;
1197 =[% branch.branchcode %]=
1198 [%~ branch.delete ~%]
1200 reset_template({ template => $template, code => $code, module => 'test' });
1201 my $letter = GetPreparedLetter(
1203 letter_code => $code,
1205 branches => $library->branchcode,
1208 my $branchcode = $library->branchcode;
1209 like($letter->{content}, qr{=$branchcode=}, 'content generated with the library');
1210 is( ref($library->get_from_storage), 'Koha::Library', 'calling ->delete on the object has not been comitted');
1214 subtest '_process_letter croaks on parsing error' => sub {
1219 code => 'TEST_CROAK',
1220 substitute => { count => 42 },
1223 my $tt_template = q|[% IF %]|;
1224 eval { process_letter( { template => $tt_template, %$params } ) };
1225 like($@, qr{^ERROR PROCESSING TEMPLATE: });
1228 sub reset_template {
1229 my ( $params ) = @_;
1230 my $template = $params->{template};
1231 my $code = $params->{code};
1232 my $module = $params->{module} || 'test_module';
1234 Koha::Notice::Templates->search( { code => $code } )->delete;
1235 Koha::Notice::Template->new(
1242 message_transport_type => 'email',
1243 content => $template
1248 sub process_letter {
1250 my $template = $params->{template};
1251 my $tables = $params->{tables};
1252 my $substitute = $params->{substitute};
1253 my $code = $params->{code};
1254 my $module = $params->{module} || 'test_module';
1255 my $branchcode = $params->{branchcode};
1257 reset_template( $params );
1259 my $letter = C4::Letters::GetPreparedLetter(
1261 letter_code => $code,
1264 substitute => $substitute,
1269 $schema->storage->txn_rollback;