Bug 17969: (QA followup) Add POD
[koha.git] / C4 / Letters.pm
index cdf0860..4c25330 100644 (file)
@@ -17,8 +17,7 @@ package C4::Letters;
 # You should have received a copy of the GNU General Public License
 # along with Koha; if not, see <http://www.gnu.org/licenses>.
 
-use strict;
-use warnings;
+use Modern::Perl;
 
 use MIME::Lite;
 use Mail::Sendmail;
@@ -28,10 +27,8 @@ use Carp;
 use Template;
 use Module::Load::Conditional qw(can_load);
 
-use C4::Koha qw(GetAuthorisedValueByCode);
 use C4::Members;
 use C4::Members::Attributes qw(GetBorrowerAttributes);
-use C4::Branch;
 use C4::Log;
 use C4::SMS;
 use C4::Debug;
@@ -40,6 +37,7 @@ use Koha::SMS::Providers;
 
 use Koha::Email;
 use Koha::DateUtils qw( format_sqldatetime dt_from_string );
+use Koha::Patrons;
 
 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
 
@@ -109,7 +107,6 @@ sub GetLetters {
     );
 
     Return a hashref of letter templates.
-    The key will be the message transport type.
 
 =cut
 
@@ -120,16 +117,15 @@ sub GetLetterTemplates {
     my $code      = $params->{code};
     my $branchcode = $params->{branchcode} // '';
     my $dbh       = C4::Context->dbh;
-    my $letters   = $dbh->selectall_hashref(
+    my $letters   = $dbh->selectall_arrayref(
         q|
-            SELECT module, code, branchcode, name, is_html, title, content, message_transport_type
+            SELECT module, code, branchcode, name, is_html, title, content, message_transport_type, lang
             FROM letter
             WHERE module = ?
             AND code = ?
             and branchcode = ?
         |
-        , 'message_transport_type'
-        , undef
+        , { Slice => {} }
         , $module, $code, $branchcode
     );
 
@@ -203,14 +199,14 @@ sub GetLettersAvailableForALibrary {
 }
 
 sub getletter {
-    my ( $module, $code, $branchcode, $message_transport_type ) = @_;
+    my ( $module, $code, $branchcode, $message_transport_type, $lang) = @_;
     $message_transport_type //= '%';
+    $lang = 'default' unless( $lang && C4::Context->preference('TranslateNotices') );
 
-    if ( C4::Context->preference('IndependentBranches')
-            and $branchcode
-            and C4::Context->userenv ) {
 
-        $branchcode = C4::Context->userenv->{'branch'};
+    my $only_my_library = C4::Context->only_my_library;
+    if ( $only_my_library and $branchcode ) {
+        $branchcode = C4::Context::mybranch();
     }
     $branchcode //= '';
 
@@ -220,9 +216,10 @@ sub getletter {
         FROM letter
         WHERE module=? AND code=? AND (branchcode = ? OR branchcode = '')
         AND message_transport_type LIKE ?
+        AND lang =?
         ORDER BY branchcode DESC LIMIT 1
     });
-    $sth->execute( $module, $code, $branchcode, $message_transport_type );
+    $sth->execute( $module, $code, $branchcode, $message_transport_type, $lang );
     my $line = $sth->fetchrow_hashref
       or return;
     $line->{'content-type'} = 'text/html; charset="UTF-8"' if $line->{is_html};
@@ -252,14 +249,17 @@ sub DelLetter {
     my $module     = $params->{module};
     my $code       = $params->{code};
     my $mtt        = $params->{mtt};
+    my $lang       = $params->{lang};
     my $dbh        = C4::Context->dbh;
     $dbh->do(q|
         DELETE FROM letter
         WHERE branchcode = ?
           AND module = ?
           AND code = ?
-    | . ( $mtt ? q| AND message_transport_type = ?| : q|| )
-    , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ) );
+    |
+    . ( $mtt ? q| AND message_transport_type = ?| : q|| )
+    . ( $lang? q| AND lang = ?| : q|| )
+    , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
 }
 
 =head2 addalert ($borrowernumber, $type, $externalid)
@@ -366,12 +366,24 @@ sub findrelatedto {
 
 =head2 SendAlerts
 
-    parameters :
-    - $type : the type of alert
-    - $externalid : the id of the "object" to query
-    - $letter_code : the letter to send.
+    my $err = &SendAlerts($type, $externalid, $letter_code);
+
+    Parameters:
+      - $type : the type of alert
+      - $externalid : the id of the "object" to query
+      - $letter_code : the notice template to use
+
+    C<&SendAlerts> sends an email notice directly to a patron or a vendor.
+
+    Currently it supports ($type):
+      - claim serial issues (claimissues)
+      - claim acquisition orders (claimacquisition)
+      - send acquisition orders to the vendor (orderacquisition)
+      - notify patrons about newly received serial issues (issue)
+      - notify patrons when their account is created (members)
 
-    send an alert to all borrowers having put an alert on a given subject.
+    Returns undef or { error => 'message } on failure.
+    Returns true on success.
 
 =cut
 
@@ -403,8 +415,9 @@ sub SendAlerts {
         # find the list of borrowers to alert
         my $alerts = getalert( '', 'issue', $subscriptionid );
         foreach (@$alerts) {
-            my $borinfo = C4::Members::GetMember('borrowernumber' => $_->{'borrowernumber'});
-            my $email = $borinfo->{email} or next;
+            my $patron = Koha::Patrons->find( $_->{borrowernumber} );
+            next unless $patron; # Just in case
+            my $email = $patron->email or next;
 
 #                    warn "sending issues...";
             my $userenv = C4::Context->userenv;
@@ -417,7 +430,7 @@ sub SendAlerts {
                     'branches'    => $_->{branchcode},
                     'biblio'      => $biblionumber,
                     'biblioitems' => $biblionumber,
-                    'borrowers'   => $borinfo,
+                    'borrowers'   => $patron->unblessed,
                     'subscription' => $subscriptionid,
                     'serial' => $externalid,
                 },
@@ -442,23 +455,42 @@ sub SendAlerts {
                                     : 'text/plain; charset="utf-8"',
                 }
             );
-            sendmail(%mail) or carp $Mail::Sendmail::error;
+            unless( sendmail(%mail) ) {
+                carp $Mail::Sendmail::error;
+                return { error => $Mail::Sendmail::error };
+            }
         }
     }
-    elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' ) {
+    elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
 
         # prepare the letter...
-        # search the biblionumber
-        my $strsth =  $type eq 'claimacquisition'
-            ? qq{
+        my $strsth;
+        my $sthorders;
+        my $dataorders;
+        my $action;
+        if ( $type eq 'claimacquisition') {
+            $strsth = qq{
             SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
             FROM aqorders
             LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
             LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
             LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
             WHERE aqorders.ordernumber IN (
+            };
+
+            if (!@$externalid){
+                carp "No order selected";
+                return { error => "no_order_selected" };
             }
-            : qq{
+            $strsth .= join( ",", ('?') x @$externalid ) . ")";
+            $action = "ACQUISITION CLAIM";
+            $sthorders = $dbh->prepare($strsth);
+            $sthorders->execute( @$externalid );
+            $dataorders = $sthorders->fetchall_arrayref( {} );
+        }
+
+        if ($type eq 'claimissues') {
+            $strsth = qq{
             SELECT serial.*,subscription.*, biblio.*, aqbooksellers.*,
             aqbooksellers.id AS booksellerid
             FROM serial
@@ -468,21 +500,46 @@ sub SendAlerts {
             WHERE serial.serialid IN (
             };
 
-        if (!@$externalid){
-            carp "No Order seleted";
-            return { error => "no_order_seleted" };
+            if (!@$externalid){
+                carp "No Order selected";
+                return { error => "no_order_selected" };
+            }
+
+            $strsth .= join( ",", ('?') x @$externalid ) . ")";
+            $action = "CLAIM ISSUE";
+            $sthorders = $dbh->prepare($strsth);
+            $sthorders->execute( @$externalid );
+            $dataorders = $sthorders->fetchall_arrayref( {} );
         }
 
-        $strsth .= join( ",", @$externalid ) . ")";
-        my $sthorders = $dbh->prepare($strsth);
-        $sthorders->execute;
-        my $dataorders = $sthorders->fetchall_arrayref( {} );
+        if ( $type eq 'orderacquisition') {
+            $strsth = qq{
+            SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
+            FROM aqorders
+            LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
+            LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
+            LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
+            WHERE aqbasket.basketno = ?
+            AND orderstatus IN ('new','ordered')
+            };
+
+            if (!$externalid){
+                carp "No basketnumber given";
+                return { error => "no_basketno" };
+            }
+            $action = "ACQUISITION ORDER";
+            $sthorders = $dbh->prepare($strsth);
+            $sthorders->execute($externalid);
+            $dataorders = $sthorders->fetchall_arrayref( {} );
+        }
 
         my $sthbookseller =
           $dbh->prepare("select * from aqbooksellers where id=?");
         $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
         my $databookseller = $sthbookseller->fetchrow_hashref;
-        my $addressee =  $type eq 'claimacquisition' ? 'acqprimary' : 'serialsprimary';
+
+        my $addressee =  $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
+
         my $sthcontact =
           $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
         $sthcontact->execute( $dataorders->[0]->{booksellerid} );
@@ -513,16 +570,17 @@ sub SendAlerts {
             },
             repeat => $dataorders,
             want_librarian => 1,
-        ) or return;
+        ) or return { error => "no_letter" };
 
         # Remove the order tag
         $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
 
         # ... then send mail
+        my $library = Koha::Libraries->find( $userenv->{branch} );
         my %mail = (
             To => join( ',', @email),
             Cc             => join( ',', @cc),
-            From           => $userenv->{emailaddress},
+            From           => $library->branchemail || C4::Context->preference('KohaAdminEmailAddress'),
             Subject        => Encode::encode( "UTF-8", "" . $letter->{title} ),
             Message => $letter->{'is_html'}
                             ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
@@ -533,12 +591,14 @@ sub SendAlerts {
                                 : 'text/plain; charset="utf-8"',
         );
 
-        $mail{'Reply-to'} = C4::Context->preference('ReplytoDefault')
-          if C4::Context->preference('ReplytoDefault');
-        $mail{'Sender'} = C4::Context->preference('ReturnpathDefault')
-          if C4::Context->preference('ReturnpathDefault');
-        $mail{'Bcc'} = $userenv->{emailaddress}
-          if C4::Context->preference("ClaimsBccCopy");
+        if ($type eq 'claimacquisition' || $type eq 'claimissues' ) {
+            $mail{'Reply-to'} = C4::Context->preference('ReplytoDefault')
+              if C4::Context->preference('ReplytoDefault');
+            $mail{'Sender'} = C4::Context->preference('ReturnpathDefault')
+              if C4::Context->preference('ReturnpathDefault');
+            $mail{'Bcc'} = $userenv->{emailaddress}
+              if C4::Context->preference("ClaimsBccCopy");
+        }
 
         unless ( sendmail(%mail) ) {
             carp $Mail::Sendmail::error;
@@ -547,7 +607,7 @@ sub SendAlerts {
 
         logaction(
             "ACQUISITION",
-            $type eq 'claimissues' ? "CLAIM ISSUE" : "ACQUISITION CLAIM",
+            $action,
             undef,
             "To="
                 . join( ',', @email )
@@ -589,8 +649,14 @@ sub SendAlerts {
                                 : 'text/plain; charset="utf-8"',
             }
         );
-        sendmail(%mail) or carp $Mail::Sendmail::error;
+        unless( sendmail(%mail) ) {
+            carp $Mail::Sendmail::error;
+            return { error => $Mail::Sendmail::error };
+        }
     }
+
+    # If we come here, return an OK status
+    return 1;
 }
 
 =head2 GetPreparedLetter( %params )
@@ -623,20 +689,26 @@ sub GetPreparedLetter {
     my $letter_code = $params{letter_code} or croak "No letter_code";
     my $branchcode  = $params{branchcode} || '';
     my $mtt         = $params{message_transport_type} || 'email';
+    my $lang        = $params{lang} || 'default';
 
-    my $letter = getletter( $module, $letter_code, $branchcode, $mtt )
-        or warn( "No $module $letter_code letter transported by " . $mtt ),
-            return;
+    my $letter = getletter( $module, $letter_code, $branchcode, $mtt, $lang );
+
+    unless ( $letter ) {
+        $letter = getletter( $module, $letter_code, $branchcode, $mtt, 'default' )
+            or warn( "No $module $letter_code letter transported by " . $mtt ),
+               return;
+    }
 
-    my $tables = $params{tables};
-    my $substitute = $params{substitute};
+    my $tables = $params{tables} || {};
+    my $substitute = $params{substitute} || {};
+    my $loops  = $params{loops} || {}; # loops is not supported for historical notices syntax
     my $repeat = $params{repeat};
-    $tables || $substitute || $repeat
-      or carp( "ERROR: nothing to substitute - both 'tables' and 'substitute' are empty" ),
+    %$tables || %$substitute || $repeat || %$loops
+      or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
          return;
     my $want_librarian = $params{want_librarian};
 
-    if ($substitute) {
+    if (%$substitute) {
         while ( my ($token, $val) = each %$substitute ) {
             if ( $token eq 'items.content' ) {
                 $val =~ s|\n|<br/>|g if $letter->{is_html};
@@ -682,7 +754,7 @@ sub GetPreparedLetter {
         }
     }
 
-    if ($tables) {
+    if (%$tables) {
         _substitute_tables( $letter, $tables );
     }
 
@@ -709,6 +781,8 @@ sub GetPreparedLetter {
         {
             content => $letter->{content},
             tables  => $tables,
+            loops  => $loops,
+            substitute => $substitute,
         }
     );
 
@@ -756,18 +830,19 @@ sub _parseletter_sth {
     #       broke things for the rest of us. prepare_cached is a better
     #       way to cache statement handles anyway.
     my $query = 
-    ($table eq 'biblio'       ) ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
-    ($table eq 'biblioitems'  ) ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
-    ($table eq 'items'        ) ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
-    ($table eq 'issues'       ) ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
-    ($table eq 'old_issues'   ) ? "SELECT * FROM $table WHERE     itemnumber = ? ORDER BY timestamp DESC LIMIT 1"  :
-    ($table eq 'reserves'     ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?"             :
-    ($table eq 'borrowers'    ) ? "SELECT * FROM $table WHERE borrowernumber = ?"                                  :
-    ($table eq 'branches'     ) ? "SELECT * FROM $table WHERE     branchcode = ?"                                  :
-    ($table eq 'suggestions'  ) ? "SELECT * FROM $table WHERE   suggestionid = ?"                                  :
-    ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE             id = ?"                                  :
-    ($table eq 'aqorders'     ) ? "SELECT * FROM $table WHERE    ordernumber = ?"                                  :
-    ($table eq 'opac_news'    ) ? "SELECT * FROM $table WHERE          idnew = ?"                                  :
+    ($table eq 'biblio'       )    ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
+    ($table eq 'biblioitems'  )    ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
+    ($table eq 'items'        )    ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
+    ($table eq 'issues'       )    ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
+    ($table eq 'old_issues'   )    ? "SELECT * FROM $table WHERE     itemnumber = ? ORDER BY timestamp DESC LIMIT 1"  :
+    ($table eq 'reserves'     )    ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?"             :
+    ($table eq 'borrowers'    )    ? "SELECT * FROM $table WHERE borrowernumber = ?"                                  :
+    ($table eq 'branches'     )    ? "SELECT * FROM $table WHERE     branchcode = ?"                                  :
+    ($table eq 'suggestions'  )    ? "SELECT * FROM $table WHERE   suggestionid = ?"                                  :
+    ($table eq 'aqbooksellers')    ? "SELECT * FROM $table WHERE             id = ?"                                  :
+    ($table eq 'aqorders'     )    ? "SELECT * FROM $table WHERE    ordernumber = ?"                                  :
+    ($table eq 'opac_news'    )    ? "SELECT * FROM $table WHERE          idnew = ?"                                  :
+    ($table eq 'article_requests') ? "SELECT * FROM $table WHERE             id = ?"                                  :
     ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
     ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
     ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
@@ -806,17 +881,7 @@ sub _parseletter {
     }
 
     if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
-        my @waitingdate = split /-/, $values->{'waitingdate'};
-
-        $values->{'expirationdate'} = '';
-        if ( C4::Context->preference('ReservesMaxPickUpDelay') ) {
-            my $dt = dt_from_string();
-            $dt->add( days => C4::Context->preference('ReservesMaxPickUpDelay') );
-            $values->{'expirationdate'} = output_pref( { dt => $dt, dateonly => 1 } );
-        }
-
         $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
-
     }
 
     if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
@@ -830,7 +895,10 @@ sub _parseletter {
             #Therefore adding the test on biblio. This includes biblioitems,
             #but excludes items. Removed unneeded global and lookahead.
 
-        $val = GetAuthorisedValueByCode ('ROADTYPE', $val, 0) if $table=~/^borrowers$/ && $field=~/^streettype$/;
+        if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
+            my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
+            $val = $av->count ? $av->next->lib : '';
+        }
 
         # Dates replacement
         my $replacedby   = defined ($val) ? $val : '';
@@ -980,9 +1048,21 @@ sub SendQueuedMessages {
         }
         elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
             if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
-                my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
-                my $sms_provider = Koha::SMS::Providers->find( $member->{'sms_provider_id'} );
+                my $patron = Koha::Patrons->find( $message->{borrowernumber} );
+                my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
+                unless ( $sms_provider ) {
+                    warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
+                    _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
+                    next MESSAGE;
+                }
+                $message->{to_address} ||= $patron->smsalertnumber;
+                unless ( $message->{to_address} && $patron->smsalertnumber ) {
+                    _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
+                    warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
+                    next MESSAGE;
+                }
                 $message->{to_address} .= '@' . $sms_provider->domain();
+                _update_message_to_address($message->{'message_id'},$message->{to_address});
                 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
             } else {
                 _send_message_by_sms( $message );
@@ -1222,10 +1302,10 @@ sub _send_message_by_email {
     my $message = shift or return;
     my ($username, $password, $method) = @_;
 
-    my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
+    my $patron = Koha::Patrons->find( $message->{borrowernumber} );
     my $to_address = $message->{'to_address'};
     unless ($to_address) {
-        unless ($member) {
+        unless ($patron) {
             warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
             _set_message_status( { message_id => $message->{'message_id'},
                                    status     => 'failed' } );
@@ -1250,8 +1330,8 @@ sub _send_message_by_email {
     my $branch_email = undef;
     my $branch_replyto = undef;
     my $branch_returnpath = undef;
-    if ($member) {
-        my $library = Koha::Libraries->find( $member->{branchcode} );
+    if ($patron) {
+        my $library = $patron->library;
         $branch_email      = $library->branchemail;
         $branch_replyto    = $library->branchreplyto;
         $branch_returnpath = $library->branchreturnpath;
@@ -1270,7 +1350,7 @@ sub _send_message_by_email {
     );
 
     $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
-    if ( my $bcc = C4::Context->preference('OverdueNoticeBcc') ) {
+    if ( my $bcc = C4::Context->preference('NoticeBcc') ) {
        $sendmail_params{ Bcc } = $bcc;
     }
 
@@ -1327,9 +1407,9 @@ sub _is_duplicate {
 
 sub _send_message_by_sms {
     my $message = shift or return;
-    my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
+    my $patron = Koha::Patrons->find( $message->{borrowernumber} );
 
-    unless ( $member->{smsalertnumber} ) {
+    unless ( $patron and $patron->smsalertnumber ) {
         _set_message_status( { message_id => $message->{'message_id'},
                                status     => 'failed' } );
         return;
@@ -1341,7 +1421,7 @@ sub _send_message_by_sms {
         return;
     }
 
-    my $success = C4::SMS->send_sms( { destination => $member->{'smsalertnumber'},
+    my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
                                        message     => $message->{'content'},
                                      } );
     _set_message_status( { message_id => $message->{'message_id'},
@@ -1375,6 +1455,8 @@ sub _process_tt {
 
     my $content = $params->{content};
     my $tables = $params->{tables};
+    my $loops = $params->{loops};
+    my $substitute = $params->{substitute} || {};
 
     my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
     my $template           = Template->new(
@@ -1389,7 +1471,9 @@ sub _process_tt {
         }
     ) or die Template->error();
 
-    my $tt_params = _get_tt_params( $tables );
+    my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
+
+    $content = qq|[% USE KohaDates %]$content|;
 
     my $output;
     $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
@@ -1398,11 +1482,18 @@ sub _process_tt {
 }
 
 sub _get_tt_params {
-    my ($tables) = @_;
+    my ($tables, $is_a_loop) = @_;
 
     my $params;
+    $is_a_loop ||= 0;
 
     my $config = {
+        article_requests => {
+            module   => 'Koha::ArticleRequests',
+            singular => 'article_request',
+            plural   => 'article_requests',
+            pk       => 'id',
+          },
         biblio => {
             module   => 'Koha::Biblios',
             singular => 'biblio',
@@ -1433,6 +1524,12 @@ sub _get_tt_params {
             plural   => 'news',
             pk       => 'idnew',
         },
+        aqorders => {
+            module   => 'Koha::Tmp::Orders', # Should Koha::Acquisition::Orders when will be based on Koha::Objects
+            singular => 'order',
+            plural   => 'orders',
+            pk       => 'ordernumber',
+        },
         reserves => {
             module   => 'Koha::Holds',
             singular => 'hold',
@@ -1463,6 +1560,12 @@ sub _get_tt_params {
             plural   => 'checkouts',
             fk       => 'itemnumber',
         },
+        old_issues => {
+            module   => 'Koha::Old::Checkouts',
+            singular => 'old_checkout',
+            plural   => 'old_checkouts',
+            fk       => 'itemnumber',
+        },
         borrower_modifications => {
             module   => 'Koha::Patron::Modifications',
             singular => 'patron_modification',
@@ -1481,7 +1584,16 @@ sub _get_tt_params {
             my $pk = $config->{$table}->{pk};
             my $fk = $config->{$table}->{fk};
 
-            if ( $ref eq q{} || $ref eq 'HASH' ) {
+            if ( $is_a_loop ) {
+                my $values = $tables->{$table} || [];
+                unless ( ref( $values ) eq 'ARRAY' ) {
+                    croak "ERROR processing table $table. Wrong API call.";
+                }
+                my $key = $pk ? $pk : $fk;
+                my $objects = $module->search( { $key => { -in => $values } } );
+                $params->{ $config->{$table}->{plural} } = $objects;
+            }
+            elsif ( $ref eq q{} || $ref eq 'HASH' ) {
                 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
                 my $object;
                 if ( $fk ) { # Using a foreign key for lookup
@@ -1490,9 +1602,9 @@ sub _get_tt_params {
                         foreach my $key ( @$fk ) {
                             $search->{$key} = $id->{$key};
                         }
-                        $object = $module->search( $search )->next();
+                        $object = $module->search( $search )->last();
                     } else { # Foreign key is single column
-                        $object = $module->search( { $fk => $id } )->next();
+                        $object = $module->search( { $fk => $id } )->last();
                     }
                 } else { # using the table's primary key for lookup
                     $object = $module->find($id);
@@ -1502,13 +1614,10 @@ sub _get_tt_params {
             else {    # $ref eq 'ARRAY'
                 my $object;
                 if ( @{ $tables->{$table} } == 1 ) {    # Param is a single key
-                    $object = $module->search( { $pk => $tables->{$table} } )->next();
+                    $object = $module->search( { $pk => $tables->{$table} } )->last();
                 }
                 else {                                  # Params are mutliple foreign keys
-                    my @values = @{ $tables->{$table} };
-                    my @keys   = @{ $config->{$table}->{fk} };
-                    my %params = map { $_ => shift(@values) } @keys;
-                    $object = $module->search( \%params )->next();
+                    croak "Multiple foreign keys (table $table) should be passed using an hashref";
                 }
                 $params->{ $config->{$table}->{singular} } = $object;
             }
@@ -1518,11 +1627,44 @@ sub _get_tt_params {
         }
     }
 
-    $params->{today} = dt_from_string();
+    $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
 
     return $params;
 }
 
+=head2 get_item_content
+
+    my $item = Koha::Items->find(...)->unblessed;
+    my @item_content_fields = qw( date_due title barcode author itemnumber );
+    my $item_content = C4::Letters::get_item_content({
+                             item => $item,
+                             item_content_fields => \@item_content_fields
+                       });
+
+This function generates a tab-separated list of values for the passed item. Dates
+are formatted following the current setup.
+
+=cut
+
+sub get_item_content {
+    my ( $params ) = @_;
+    my $item = $params->{item};
+    my $dateonly = $params->{dateonly} || 0;
+    my $item_content_fields = $params->{item_content_fields} || [];
+
+    return unless $item;
+
+    my @item_info = map {
+        $_ =~ /^date|date$/
+          ? eval {
+            output_pref(
+                { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
+          }
+          : $item->{$_}
+          || ''
+    } @$item_content_fields;
+    return join( "\t", @item_info ) . "\n";
+}
 
 1;
 __END__