3 # This file is part of Koha.
5 # Copyright (C) 2016 ByWater Solutions
6 # Copyright (C) 2017 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 => 30;
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 $patron = $builder->build( { source => 'Borrower' } );
315 my $item1 = $builder->build_sample_item(
317 barcode => 'a_t_barcode',
318 library => $library->{branchcode},
320 itemcallnumber => 'itemcallnumber1',
323 my $biblio1 = $item1->biblio->unblessed;
324 $item1 = $item1->unblessed;
325 my $item2 = $builder->build_sample_item(
327 barcode => 'another_t_barcode',
328 library => $library->{branchcode},
330 itemcallnumber => 'itemcallnumber2',
333 my $biblio2 = $item2->biblio->unblessed;
334 $item2 = $item2->unblessed;
335 my $item3 = $builder->build_sample_item(
337 barcode => 'another_t_barcode_3',
338 library => $library->{branchcode},
340 itemcallnumber => 'itemcallnumber3',
343 my $biblio3 = $item3->biblio->unblessed;
344 $item3 = $item3->unblessed;
346 t::lib::Mocks::mock_userenv({ branchcode => $library->{branchcode} });
348 subtest 'ACQ_NOTIF_ON_RECEIV ' => sub {
350 my $code = 'ACQ_NOTIF_ON_RECEIV';
351 my $branchcode = $library->{branchcode};
352 my $order = $builder->build({ source => 'Aqorder' });
355 Dear <<borrowers.firstname>> <<borrowers.surname>>,
356 The order <<aqorders.ordernumber>> (<<biblio.title>>) has been received.
359 my $params = { code => $code, branchcode => $branchcode, tables => { branches => $library, borrowers => $patron, biblio => $biblio1, aqorders => $order } };
360 my $letter = process_letter( { template => $template, %$params });
362 Dear [% borrower.firstname %] [% borrower.surname %],
363 The order [% order.ordernumber %] ([% biblio.title %]) has been received.
366 my $tt_letter = process_letter( { template => $tt_template, %$params });
368 is( $tt_letter->{content}, $letter->{content}, 'Verified letter content' );
371 subtest 'AR_*' => sub {
373 my $code = 'AR_CANCELED';
374 my $branchcode = $library->{branchcode};
377 <<borrowers.firstname>> <<borrowers.surname>> (<<borrowers.cardnumber>>)
379 Your request for an article from <<biblio.title>> (<<items.barcode>>) has been canceled for the following reason:
381 <<article_requests.notes>>
384 Title: <<article_requests.title>>
385 Author: <<article_requests.author>>
386 Volume: <<article_requests.volume>>
387 Issue: <<article_requests.issue>>
388 Date: <<article_requests.date>>
389 Pages: <<article_requests.pages>>
390 Chapters: <<article_requests.chapters>>
391 Notes: <<article_requests.patron_notes>>
393 reset_template( { template => $template, code => $code, module => 'circulation' } );
394 my $article_request = $builder->build_object(
396 class => 'Koha::ArticleRequests',
397 value => { debit_id => undef }
400 $article_request->cancel;
401 my $letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
404 [% borrower.firstname %] [% borrower.surname %] ([% borrower.cardnumber %])
406 Your request for an article from [% biblio.title %] ([% item.barcode %]) has been canceled for the following reason:
408 [% article_request.notes %]
411 Title: [% article_request.title %]
412 Author: [% article_request.author %]
413 Volume: [% article_request.volume %]
414 Issue: [% article_request.issue %]
415 Date: [% article_request.date %]
416 Pages: [% article_request.pages %]
417 Chapters: [% article_request.chapters %]
418 Notes: [% article_request.patron_notes %]
420 reset_template( { template => $tt_template, code => $code, module => 'circulation' } );
421 $article_request->cancel;
422 my $tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
423 is( $tt_letter->content, $letter->content, 'Compare AR_* notices' );
424 isnt( $tt_letter->message_id, $letter->message_id, 'Comparing AR_* notices should compare 2 different messages' );
427 subtest 'CHECKOUT+CHECKIN' => sub {
430 my $checkout_code = 'CHECKOUT';
431 my $checkin_code = 'CHECKIN';
433 my $dbh = C4::Context->dbh;
434 # Enable notification for CHECKOUT - Things are hardcoded here but should work with default data
435 $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ? )|, undef, $patron->{borrowernumber}, 6 );
436 my $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
437 $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
438 # Enable notification for CHECKIN - Things are hardcoded here but should work with default data
439 $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ? )|, undef, $patron->{borrowernumber}, 5 );
440 $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
441 $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
444 my $checkout_template = q|
445 The following items have been checked out:
449 Thank you for visiting <<branches.branchname>>.
451 reset_template( { template => $checkout_template, code => $checkout_code, module => 'circulation' } );
452 my $checkin_template = q[
453 The following items have been checked out:
455 <<biblio.title>> was due on <<old_issues.date_due | dateonly>>
457 Thank you for visiting <<branches.branchname>>.
459 reset_template( { template => $checkin_template, code => $checkin_code, module => 'circulation' } );
461 C4::Circulation::AddIssue( $patron, $item1->{barcode} );
462 my $first_checkout_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
463 C4::Circulation::AddIssue( $patron, $item2->{barcode} );
464 my $second_checkout_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
466 AddReturn( $item1->{barcode} );
467 my $first_checkin_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
468 AddReturn( $item2->{barcode} );
469 my $second_checkin_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
471 Koha::Notice::Messages->delete;
474 $checkout_template = q|
475 The following items have been checked out:
479 Thank you for visiting [% branch.branchname %].
481 reset_template( { template => $checkout_template, code => $checkout_code, module => 'circulation' } );
482 $checkin_template = q[
483 The following items have been checked out:
485 [% biblio.title %] was due on [% old_checkout.date_due | $KohaDates %]
487 Thank you for visiting [% branch.branchname %].
489 reset_template( { template => $checkin_template, code => $checkin_code, module => 'circulation' } );
491 C4::Circulation::AddIssue( $patron, $item1->{barcode} );
492 my $first_checkout_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
493 C4::Circulation::AddIssue( $patron, $item2->{barcode} );
494 my $second_checkout_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
496 AddReturn( $item1->{barcode} );
497 my $first_checkin_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
498 AddReturn( $item2->{barcode} );
499 my $second_checkin_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
501 is( $first_checkout_tt_letter->content, $first_checkout_letter->content, 'Verify first checkout letter' );
502 is( $second_checkout_tt_letter->content, $second_checkout_letter->content, 'Verify second checkout letter' );
503 is( $first_checkin_tt_letter->content, $first_checkin_letter->content, 'Verify first checkin letter' );
504 is( $second_checkin_tt_letter->content, $second_checkin_letter->content, 'Verify second checkin letter' );
508 subtest 'DUEDGST|count' => sub {
511 my $code = 'DUEDGST';
513 my $dbh = C4::Context->dbh;
514 # Enable notification for DUEDGST - Things are hardcoded here but should work with default data
515 $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ? )|, undef, $patron->{borrowernumber}, 1 );
516 my $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
517 $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
521 substitute => { count => 42 },
525 You have <<count>> items due
527 my $letter = process_letter( { template => $template, %$params });
530 You have [% count %] items due
532 my $tt_letter = process_letter( { template => $tt_template, %$params });
533 is( $tt_letter->{content}, $letter->{content}, );
536 subtest 'HOLD_SLIP|dates|today' => sub {
539 my $code = 'HOLD_SLIP';
541 my $reserve_id1 = C4::Reserves::AddReserve(
543 branchcode => $library->{branchcode},
544 borrowernumber => $patron->{borrowernumber},
545 biblionumber => $biblio1->{biblionumber},
547 itemnumber => $item1->{itemnumber},
550 my $reserve_id2 = C4::Reserves::AddReserve(
552 branchcode => $library->{branchcode},
553 borrowernumber => $patron->{borrowernumber},
554 biblionumber => $biblio1->{biblionumber},
556 itemnumber => $item1->{itemnumber},
559 my $reserve_id3 = C4::Reserves::AddReserve(
561 branchcode => $library->{branchcode},
562 borrowernumber => $patron->{borrowernumber},
563 biblionumber => $biblio2->{biblionumber},
564 notes => "another note",
565 itemnumber => $item2->{itemnumber},
569 my $template = <<EOF;
570 <h5>Date: <<today>></h5>
572 <h3> Transfer to/Hold in <<branches.branchname>></h3>
574 <h3><<borrowers.surname>>, <<borrowers.firstname>></h3>
577 <li><<borrowers.cardnumber>></li>
578 <li><<borrowers.phone>></li>
579 <li> <<borrowers.address>><br />
580 <<borrowers.address2>><br />
581 <<borrowers.city>> <<borrowers.zipcode>>
583 <li><<borrowers.email>></li>
586 <h3>ITEM ON HOLD</h3>
587 <h4><<biblio.title>></h4>
588 <h5><<biblio.author>></h5>
590 <li><<items.barcode>></li>
591 <li><<items.itemcallnumber>></li>
592 <li><<reserves.waitingdate>></li>
595 <pre><<reserves.reserve_id>>=<<reserves.reservenotes>></pre>
599 reset_template( { template => $template, code => $code, module => 'circulation' } );
600 my $letter_for_item1 = C4::Reserves::ReserveSlip( { branchcode => $library->{branchcode}, reserve_id => $reserve_id1 } );
601 my $letter_for_item2 = C4::Reserves::ReserveSlip( { branchcode => $library->{branchcode}, reserve_id => $reserve_id3 } );
603 my $tt_template = <<EOF;
604 <h5>Date: [% today | \$KohaDates with_hours => 1 %]</h5>
606 <h3> Transfer to/Hold in [% branch.branchname %]</h3>
608 <h3>[% borrower.surname %], [% borrower.firstname %]</h3>
611 <li>[% borrower.cardnumber %]</li>
612 <li>[% borrower.phone %]</li>
613 <li> [% borrower.address %]<br />
614 [% borrower.address2 %]<br />
615 [% borrower.city %] [% borrower.zipcode %]
617 <li>[% borrower.email %]</li>
620 <h3>ITEM ON HOLD</h3>
621 <h4>[% biblio.title %]</h4>
622 <h5>[% biblio.author %]</h5>
624 <li>[% item.barcode %]</li>
625 <li>[% item.itemcallnumber %]</li>
626 <li>[% hold.waitingdate | \$KohaDates %]</li>
629 <pre>[% hold.reserve_id %]=[% hold.reservenotes %]</pre>
633 reset_template( { template => $tt_template, code => $code, module => 'circulation' } );
634 my $tt_letter_for_item1 = C4::Reserves::ReserveSlip( { branchcode => $library->{branchcode}, reserve_id => $reserve_id1 } );
635 my $tt_letter_for_item2 = C4::Reserves::ReserveSlip( { branchcode => $library->{branchcode}, reserve_id => $reserve_id3 } );
637 is( $tt_letter_for_item1->{content}, $letter_for_item1->{content}, );
638 is( $tt_letter_for_item2->{content}, $letter_for_item2->{content}, );
641 subtest 'ISSUESLIP|checkedout|repeat|news' => sub {
644 my $code = 'ISSUESLIP';
645 my $now = dt_from_string;
646 my $one_minute_ago = dt_from_string->subtract( minutes => 1 );
648 my $branchcode = $library->{branchcode};
650 Koha::AdditionalContents->delete;
651 my $news_item = $builder->build_object(
653 class => 'Koha::AdditionalContents',
657 branchcode => $branchcode,
659 title => "A wonderful news",
660 content => "This is the wonderful news.",
661 expiration_date => undef,
662 published_on => $one_minute_ago
668 my $template = <<EOF;
669 <h3><<branches.branchname>></h3>
670 Checked out to <<borrowers.title>> <<borrowers.firstname>> <<borrowers.initials>> <<borrowers.surname>> <br />
671 (<<borrowers.cardnumber>>) <br />
678 <<biblio.title>> <br />
679 Barcode: <<items.barcode>><br />
680 Date due: <<issues.date_due | dateonly>><br />
687 <<biblio.title>> <br />
688 Barcode: <<items.barcode>><br />
689 Date due: <<issues.date_due | dateonly>><br />
695 <h4 style="text-align: center; font-style:italic;">News</h4>
697 <div class="newsitem">
698 <h5 style="margin-bottom: 1px; margin-top: 1px"><b><<additional_contents.title>></b></h5>
699 <p style="margin-bottom: 1px; margin-top: 1px"><<additional_contents.content>></p>
700 <p class="newsfooter" style="font-size: 8pt; font-style:italic; margin-bottom: 1px; margin-top: 1px">Posted on <<additional_contents.published_on>></p>
706 reset_template( { template => $template, code => $code, module => 'circulation' } );
708 my $checkout = C4::Circulation::AddIssue( $patron, $item1->{barcode} ); # Add a first checkout
709 $checkout->set( { timestamp => $now, issuedate => $one_minute_ago } )->store;
710 my $first_slip = C4::Members::IssueSlip( $branchcode, $patron->{borrowernumber} );
712 $checkout = C4::Circulation::AddIssue( $patron, $item2->{barcode} ); # Add a second checkout
713 $checkout->set( { timestamp => $now, issuedate => $now } )->store;
714 my $yesterday = dt_from_string->subtract( days => 1 );
715 C4::Circulation::AddIssue( $patron, $item3->{barcode}, $yesterday ); # Add an overdue
716 my $second_slip = C4::Members::IssueSlip( $branchcode, $patron->{borrowernumber} );
719 AddReturn( $item1->{barcode} );
720 AddReturn( $item2->{barcode} );
721 AddReturn( $item3->{barcode} );
724 my $tt_template = <<EOF;
725 <h3>[% branch.branchname %]</h3>
726 Checked out to [% borrower.title %] [% borrower.firstname %] [% borrower.initials %] [% borrower.surname %] <br />
727 ([% borrower.cardnumber %]) <br />
729 [% today | \$KohaDates with_hours => 1 %]<br />
732 [% FOREACH checkout IN checkouts %]
733 [%~ SET item = checkout.item %]
734 [%~ SET biblio = checkout.item.biblio %]
736 [% biblio.title %] <br />
737 Barcode: [% item.barcode %]<br />
738 Date due: [% checkout.date_due | \$KohaDates %]<br />
743 [% FOREACH overdue IN overdues %]
744 [%~ SET item = overdue.item %]
745 [%~ SET biblio = overdue.item.biblio %]
747 [% biblio.title %] <br />
748 Barcode: [% item.barcode %]<br />
749 Date due: [% overdue.date_due | \$KohaDates %]<br />
755 <h4 style="text-align: center; font-style:italic;">News</h4>
756 [% FOREACH n IN additional_contents %]
757 <div class="newsitem">
758 <h5 style="margin-bottom: 1px; margin-top: 1px"><b>[% n.title %]</b></h5>
759 <p style="margin-bottom: 1px; margin-top: 1px">[% n.content %]</p>
760 <p class="newsfooter" style="font-size: 8pt; font-style:italic; margin-bottom: 1px; margin-top: 1px">Posted on [% n.published_on | \$KohaDates %]</p>
766 reset_template( { template => $tt_template, code => $code, module => 'circulation' } );
768 $checkout = C4::Circulation::AddIssue( $patron, $item1->{barcode} ); # Add a first checkout
769 $checkout->set( { timestamp => $now, issuedate => $one_minute_ago } )->store;
770 my $first_tt_slip = C4::Members::IssueSlip( $branchcode, $patron->{borrowernumber} );
772 $checkout = C4::Circulation::AddIssue( $patron, $item2->{barcode} ); # Add a second checkout
773 $checkout->set( { timestamp => $now, issuedate => $now } )->store;
774 C4::Circulation::AddIssue( $patron, $item3->{barcode}, $yesterday ); # Add an overdue
775 my $second_tt_slip = C4::Members::IssueSlip( $branchcode, $patron->{borrowernumber} );
777 # There is too many line breaks generated by the historic syntax
778 $second_slip->{content} =~ s|</p>\n\n\n<p>|</p>\n\n<p>|s;
780 my $news_item_title = $news_item->title;
781 like( $first_slip->{content}, qr{$news_item_title} );
782 is( $first_tt_slip->{content}, $first_slip->{content}, );
783 is( $second_tt_slip->{content}, $second_slip->{content}, );
786 AddReturn( $item1->{barcode} );
787 AddReturn( $item2->{barcode} );
788 AddReturn( $item3->{barcode} );
791 subtest 'ODUE|items.content|item' => sub {
796 my $branchcode = $library->{branchcode};
799 # FIXME items.fine does not work with TT notices
801 # <item> should contain Fine: <<items.fine>></item>
802 my $template = <<EOF;
803 Dear <<borrowers.firstname>> <<borrowers.surname>>,
805 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.
807 <<branches.branchname>>
808 <<branches.branchaddress1>>
809 <<branches.branchaddress2>> <<branches.branchaddress3>>
810 Phone: <<branches.branchphone>>
811 Fax: <<branches.branchfax>>
812 Email: <<branches.branchemail>>
814 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.
816 The following item(s) is/are currently overdue:
818 <item>"<<biblio.title>>" by <<biblio.author>>, <<items.itemcallnumber>>, Barcode: <<items.barcode>></item>
822 Thank-you for your prompt attention to this matter.
824 <<branches.branchname>> Staff
827 reset_template( { template => $template, code => $code, module => 'circulation' } );
829 my $yesterday = dt_from_string->subtract( days => 1 );
830 my $two_days_ago = dt_from_string->subtract( days => 2 );
831 my $issue1 = C4::Circulation::AddIssue( $patron, $item1->{barcode} ); # Add a first checkout
832 my $issue2 = C4::Circulation::AddIssue( $patron, $item2->{barcode}, $yesterday ); # Add an first overdue
833 my $issue3 = C4::Circulation::AddIssue( $patron, $item3->{barcode}, $two_days_ago ); # Add an second overdue
834 $issue1 = $issue1->unblessed;
835 $issue2 = $issue2->unblessed;
836 $issue3 = $issue3->unblessed;
839 my @item_fields = qw( date_due title barcode author itemnumber );
840 my $items_content = C4::Letters::get_item_content( { item => { %$item1, %$biblio1, %$issue1 }, item_content_fields => \@item_fields, dateonly => 1 } );
841 $items_content .= C4::Letters::get_item_content( { item => { %$item2, %$biblio2, %$issue2 }, item_content_fields => \@item_fields, dateonly => 1 } );
842 $items_content .= C4::Letters::get_item_content( { item => { %$item3, %$biblio3, %$issue3 }, item_content_fields => \@item_fields, dateonly => 1 } );
844 my @items = ( $item1, $item2, $item3 );
845 my $letter = C4::Overdues::parse_overdues_letter(
847 letter_code => $code,
848 borrowernumber => $patron->{borrowernumber},
849 branchcode => $library->{branchcode},
852 bib => $library->{branchname},
853 'items.content' => $items_content,
854 count => scalar( @items ),
855 message_transport_type => 'email',
861 AddReturn( $item1->{barcode} );
862 AddReturn( $item2->{barcode} );
863 AddReturn( $item3->{barcode} );
867 my $tt_template = <<EOF;
868 Dear [% borrower.firstname %] [% borrower.surname %],
870 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.
872 [% branch.branchname %]
873 [% branch.branchaddress1 %]
874 [% branch.branchaddress2 %] [% branch.branchaddress3 %]
875 Phone: [% branch.branchphone %]
876 Fax: [% branch.branchfax %]
877 Email: [% branch.branchemail %]
879 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.
881 The following item(s) is/are currently overdue:
883 [% FOREACH overdue IN overdues %]
884 [%~ SET item = overdue.item ~%]
885 "[% item.biblio.title %]" by [% item.biblio.author %], [% item.itemcallnumber %], Barcode: [% item.barcode %]
887 [% FOREACH overdue IN overdues %]
888 [%~ SET item = overdue.item ~%]
889 [% overdue.date_due | \$KohaDates %]\t[% item.biblio.title %]\t[% item.barcode %]\t[% item.biblio.author %]\t[% item.itemnumber %]
892 Thank-you for your prompt attention to this matter.
894 [% branch.branchname %] Staff
897 reset_template( { template => $tt_template, code => $code, module => 'circulation' } );
899 C4::Circulation::AddIssue( $patron, $item1->{barcode} ); # Add a first checkout
900 C4::Circulation::AddIssue( $patron, $item2->{barcode}, $yesterday ); # Add an first overdue
901 C4::Circulation::AddIssue( $patron, $item3->{barcode}, $two_days_ago ); # Add an second overdue
903 my $tt_letter = C4::Overdues::parse_overdues_letter(
905 letter_code => $code,
906 borrowernumber => $patron->{borrowernumber},
907 branchcode => $library->{branchcode},
910 bib => $library->{branchname},
911 'items.content' => $items_content,
912 count => scalar( @items ),
913 message_transport_type => 'email',
918 is( $tt_letter->{content}, $letter->{content}, );
921 subtest 'Bug 19743 - Header and Footer should be updated on each item for checkin / checkout / renewal notices' => sub {
924 my $checkout_code = 'CHECKOUT';
925 my $checkin_code = 'CHECKIN';
927 my $dbh = C4::Context->dbh;
928 $dbh->do("DELETE FROM letter");
929 $dbh->do("DELETE FROM issues");
930 $dbh->do("DELETE FROM message_queue");
932 # Enable notification for CHECKOUT - Things are hardcoded here but should work with default data
933 $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ? )|, undef, $patron->{borrowernumber}, 6 );
934 my $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
935 $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
936 # Enable notification for CHECKIN - Things are hardcoded here but should work with default data
937 $dbh->do(q|INSERT INTO borrower_message_preferences( borrowernumber, message_attribute_id ) VALUES ( ?, ? )|, undef, $patron->{borrowernumber}, 5 );
938 $borrower_message_preference_id = $dbh->last_insert_id(undef, undef, "borrower_message_preferences", undef);
939 $dbh->do(q|INSERT INTO borrower_message_transport_preferences( borrower_message_preference_id, message_transport_type) VALUES ( ?, ? )|, undef, $borrower_message_preference_id, 'email' );
941 my $checkout_template = q|
942 <<branches.branchname>>
946 reset_template( { template => $checkout_template, code => $checkout_code, module => 'circulation' } );
947 my $checkin_template = q[
948 <<branches.branchname>>
952 reset_template( { template => $checkin_template, code => $checkin_code, module => 'circulation' } );
954 my $issue = C4::Circulation::AddIssue( $patron, $item1->{barcode} );
955 my $first_checkout_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
957 my $library_object = Koha::Libraries->find( $issue->branchcode );
958 my $old_branchname = $library_object->branchname;
959 my $new_branchname = "Kyle M Hall Memorial Library";
961 # Change branch name for second checkout notice
962 $library_object->branchname($new_branchname);
963 $library_object->store();
965 C4::Circulation::AddIssue( $patron, $item2->{barcode} );
966 my $second_checkout_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
968 # Restore old name for first checkin notice
969 $library_object->branchname( $old_branchname );
970 $library_object->store();
972 AddReturn( $item1->{barcode} );
973 my $first_checkin_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
975 # Change branch name for second checkin notice
976 $library_object->branchname($new_branchname);
977 $library_object->store();
979 AddReturn( $item2->{barcode} );
980 my $second_checkin_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
982 # Restore old name for first TT checkout notice
983 $library_object->branchname( $old_branchname );
984 $library_object->store();
986 Koha::Notice::Messages->delete;
989 $checkout_template = q|
990 [% branch.branchname %]
994 reset_template( { template => $checkout_template, code => $checkout_code, module => 'circulation' } );
995 $checkin_template = q[
996 [% branch.branchname %]
1000 reset_template( { template => $checkin_template, code => $checkin_code, module => 'circulation' } );
1002 C4::Circulation::AddIssue( $patron, $item1->{barcode} );
1003 my $first_checkout_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
1005 # Change branch name for second checkout notice
1006 $library_object->branchname($new_branchname);
1007 $library_object->store();
1009 C4::Circulation::AddIssue( $patron, $item2->{barcode} );
1010 my $second_checkout_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
1012 # Restore old name for first checkin notice
1013 $library_object->branchname( $old_branchname );
1014 $library_object->store();
1016 AddReturn( $item1->{barcode} );
1017 my $first_checkin_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
1019 # Change branch name for second checkin notice
1020 $library_object->branchname($new_branchname);
1021 $library_object->store();
1023 AddReturn( $item2->{barcode} );
1024 my $second_checkin_tt_letter = Koha::Notice::Messages->search( {}, { order_by => { -desc => 'message_id' } } )->next;
1026 my $first_letter = qq[
1029 my $second_letter = qq[
1034 is( $first_checkout_letter->content, $first_letter, 'Verify first checkout letter' );
1035 is( $second_checkout_letter->content, $second_letter, 'Verify second checkout letter' );
1036 is( $first_checkin_letter->content, $first_letter, 'Verify first checkin letter' );
1037 is( $second_checkin_letter->content, $second_letter, 'Verify second checkin letter' );
1039 is( $first_checkout_tt_letter->content, $first_letter, 'Verify TT first checkout letter' );
1040 is( $second_checkout_tt_letter->content, $second_letter, 'Verify TT second checkout letter' );
1041 is( $first_checkin_tt_letter->content, $first_letter, 'Verify TT first checkin letter' );
1042 is( $second_checkin_tt_letter->content, $second_letter, 'Verify TT second checkin letter' );
1047 subtest 'loops' => sub {
1050 my $module = "TEST";
1052 subtest 'primary key is AI' => sub {
1054 my $patron_1 = $builder->build({ source => 'Borrower' });
1055 my $patron_2 = $builder->build({ source => 'Borrower' });
1057 my $template = q|[% FOREACH patron IN borrowers %][% patron.surname %][% END %]|;
1058 reset_template( { template => $template, code => $code, module => $module } );
1059 my $letter = GetPreparedLetter( module => $module, letter_code => $code, loops => { borrowers => [ $patron_1->{borrowernumber}, $patron_2->{borrowernumber} ] } );
1060 my $expected_letter = join '', ( $patron_1->{surname}, $patron_2->{surname} );
1061 is( $letter->{content}, $expected_letter, );
1064 subtest 'foreign key is used' => sub {
1066 my $patron_1 = $builder->build({ source => 'Borrower' });
1067 my $patron_2 = $builder->build({ source => 'Borrower' });
1068 my $checkout_1 = $builder->build({ source => 'Issue', value => { borrowernumber => $patron_1->{borrowernumber} } } );
1069 my $checkout_2 = $builder->build({ source => 'Issue', value => { borrowernumber => $patron_1->{borrowernumber} } } );
1071 my $template = q|[% FOREACH checkout IN checkouts %][% checkout.issue_id %][% END %]|;
1072 reset_template( { template => $template, code => $code, module => $module } );
1073 my $letter = GetPreparedLetter( module => $module, letter_code => $code, loops => { issues => [ $checkout_1->{itemnumber}, $checkout_2->{itemnumber} ] } );
1074 my $expected_letter = join '', ( $checkout_1->{issue_id}, $checkout_2->{issue_id} );
1075 is( $letter->{content}, $expected_letter, );
1079 subtest 'add_tt_filters' => sub {
1082 my $module = "TEST";
1084 my $patron = $builder->build_object(
1086 class => 'Koha::Patrons',
1087 value => { surname => "with_punctuation_" }
1090 my $biblio = $builder->build_object(
1091 { class => 'Koha::Biblios', value => { title => "with_punctuation_" } }
1093 my $biblioitem = $builder->build_object(
1095 class => 'Koha::Biblioitems',
1097 biblionumber => $biblio->biblionumber,
1098 isbn => "with_punctuation_"
1103 my $template = q|patron=[% borrower.surname %];biblio=[% biblio.title %];biblioitems=[% biblioitem.isbn %]|;
1104 reset_template( { template => $template, code => $code, module => $module } );
1105 my $letter = GetPreparedLetter(
1107 letter_code => $code,
1109 borrowers => $patron->borrowernumber,
1110 biblio => $biblio->biblionumber,
1111 biblioitems => $biblioitem->biblioitemnumber
1114 my $expected_letter = q|patron=with_punctuation_;biblio=with_punctuation;biblioitems=with_punctuation|;
1115 is( $letter->{content}, $expected_letter, "Pre-processing should call TT plugin to remove punctuation if table is biblio or biblioitems");
1118 subtest 'Handle includes' => sub {
1120 my $cgi = CGI->new();
1121 my $code = 'TEST_INCLUDE';
1123 $builder->build_object( { class => 'Koha::Account::Lines', value => { credit_type_code => 'PAYMENT', status => 'CANCELLED' } } );
1124 my $template = <<EOF;
1126 [%- PROCESS 'accounts.inc' -%]
1127 [%- PROCESS account_type_description account=credit -%]
1129 reset_template({ template => $template, code => $code, module => 'test' });
1130 my $letter = GetPreparedLetter(
1132 letter_code => $code,
1134 credits => $account->accountlines_id
1137 is($letter->{content},'<span>Payment</span><span> (Cancelled)</span>', "Include used in notice");
1140 subtest 'Dates formatting' => sub {
1142 my $code = 'TEST_DATE';
1143 t::lib::Mocks::mock_preference('dateformat', 'metric'); # MM/DD/YYYY
1144 my $biblio = $builder->build_object(
1146 class => 'Koha::Biblios',
1148 timestamp => '2018-12-13 20:21:22',
1149 datecreated => '2018-12-13'
1153 my $template = <<EOF;
1154 [%- USE KohaDates -%]
1155 [% biblio.timestamp %]
1156 [% biblio.timestamp | \$KohaDates %]
1157 [% biblio.timestamp | \$KohaDates with_hours => 1 %]
1159 [% biblio.datecreated %]
1160 [% biblio.datecreated | \$KohaDates %]
1161 [% biblio.datecreated | \$KohaDates with_hours => 1 %]
1163 [% biblio.timestamp | \$KohaDates dateformat => 'iso' %]
1164 [% KohaDates.output_preference( str => biblio.timestamp, dateformat => 'iso' ) %]
1165 [% KohaDates.output_preference( str => biblio.timestamp, dateformat => 'iso', dateonly => 1 ) %]
1167 reset_template({ template => $template, code => $code, module => 'test' });
1168 my $letter = GetPreparedLetter(
1170 letter_code => $code,
1172 biblio => $biblio->biblionumber,
1175 my $expected_content = sprintf("%s\n%s\n%s\n\n%s\n%s\n%s\n\n%s\n%s\n%s\n",
1176 '2018-12-13 20:21:22',
1188 is( $letter->{content}, $expected_content );
1191 subtest 'Execute TT process in a DB transaction' => sub {
1193 my $code = 'TEST_TXN';
1194 my $library = $builder->build_object( { class => 'Koha::Libraries' } );
1195 my $template = <<EOF;
1196 =[% branch.branchcode %]=
1197 [%~ branch.delete ~%]
1199 reset_template({ template => $template, code => $code, module => 'test' });
1200 my $letter = GetPreparedLetter(
1202 letter_code => $code,
1204 branches => $library->branchcode,
1207 my $branchcode = $library->branchcode;
1208 like($letter->{content}, qr{=$branchcode=}, 'content generated with the library');
1209 is( ref($library->get_from_storage), 'Koha::Library', 'calling ->delete on the object has not been comitted');
1212 sub reset_template {
1213 my ( $params ) = @_;
1214 my $template = $params->{template};
1215 my $code = $params->{code};
1216 my $module = $params->{module} || 'test_module';
1218 Koha::Notice::Templates->search( { code => $code } )->delete;
1219 Koha::Notice::Template->new(
1226 message_transport_type => 'email',
1227 content => $template
1232 sub process_letter {
1234 my $template = $params->{template};
1235 my $tables = $params->{tables};
1236 my $substitute = $params->{substitute};
1237 my $code = $params->{code};
1238 my $module = $params->{module} || 'test_module';
1239 my $branchcode = $params->{branchcode};
1241 reset_template( $params );
1243 my $letter = C4::Letters::GetPreparedLetter(
1245 letter_code => $code,
1248 substitute => $substitute,
1253 $schema->storage->txn_rollback;