3 # Copyright 2000-2002 Katipo Communications
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
24 use Date::Calc qw( Add_Delta_Days );
28 use Module::Load::Conditional qw(can_load);
31 use C4::Members::Attributes qw(GetBorrowerAttributes);
36 use Koha::SMS::Providers;
39 use Koha::Notice::Messages;
40 use Koha::DateUtils qw( format_sqldatetime dt_from_string );
42 use Koha::Subscriptions;
44 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
50 &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &SendAlerts &GetPrintMessages &GetMessageTransportTypes
56 C4::Letters - Give functions for Letters management
64 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
65 late issues, as well as other tasks like sending a mail to users that have subscribed to a "serial issue alert" (= being warned every time a new issue has arrived at the library)
67 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
69 =head2 GetLetters([$module])
71 $letters = &GetLetters($module);
72 returns informations about letters.
73 if needed, $module filters for letters given module
75 DEPRECATED - You must use Koha::Notice::Templates instead
76 The group by clause is confusing and can lead to issues
82 my $module = $filters->{module};
83 my $code = $filters->{code};
84 my $branchcode = $filters->{branchcode};
85 my $dbh = C4::Context->dbh;
86 my $letters = $dbh->selectall_arrayref(
88 SELECT code, module, name
92 . ( $module ? q| AND module = ?| : q|| )
93 . ( $code ? q| AND code = ?| : q|| )
94 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
95 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
96 , ( $module ? $module : () )
97 , ( $code ? $code : () )
98 , ( defined $branchcode ? $branchcode : () )
104 =head2 GetLetterTemplates
106 my $letter_templates = GetLetterTemplates(
108 module => 'circulation',
110 branchcode => 'CPL', # '' for default,
114 Return a hashref of letter templates.
118 sub GetLetterTemplates {
121 my $module = $params->{module};
122 my $code = $params->{code};
123 my $branchcode = $params->{branchcode} // '';
124 my $dbh = C4::Context->dbh;
125 my $letters = $dbh->selectall_arrayref(
127 SELECT module, code, branchcode, name, is_html, title, content, message_transport_type, lang
134 , $module, $code, $branchcode
140 =head2 GetLettersAvailableForALibrary
142 my $letters = GetLettersAvailableForALibrary(
144 branchcode => 'CPL', # '' for default
145 module => 'circulation',
149 Return an arrayref of letters, sorted by name.
150 If a specific letter exist for the given branchcode, it will be retrieve.
151 Otherwise the default letter will be.
155 sub GetLettersAvailableForALibrary {
157 my $branchcode = $filters->{branchcode};
158 my $module = $filters->{module};
160 croak "module should be provided" unless $module;
162 my $dbh = C4::Context->dbh;
163 my $default_letters = $dbh->selectall_arrayref(
165 SELECT module, code, branchcode, name
169 . q| AND branchcode = ''|
170 . ( $module ? q| AND module = ?| : q|| )
171 . q| ORDER BY name|, { Slice => {} }
172 , ( $module ? $module : () )
175 my $specific_letters;
177 $specific_letters = $dbh->selectall_arrayref(
179 SELECT module, code, branchcode, name
183 . q| AND branchcode = ?|
184 . ( $module ? q| AND module = ?| : q|| )
185 . q| ORDER BY name|, { Slice => {} }
187 , ( $module ? $module : () )
192 for my $l (@$default_letters) {
193 $letters{ $l->{code} } = $l;
195 for my $l (@$specific_letters) {
196 # Overwrite the default letter with the specific one.
197 $letters{ $l->{code} } = $l;
200 return [ map { $letters{$_} }
201 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
207 my ( $module, $code, $branchcode, $message_transport_type, $lang) = @_;
208 $message_transport_type //= '%';
209 $lang = 'default' unless( $lang && C4::Context->preference('TranslateNotices') );
212 my $only_my_library = C4::Context->only_my_library;
213 if ( $only_my_library and $branchcode ) {
214 $branchcode = C4::Context::mybranch();
218 my $dbh = C4::Context->dbh;
219 my $sth = $dbh->prepare(q{
222 WHERE module=? AND code=? AND (branchcode = ? OR branchcode = '')
223 AND message_transport_type LIKE ?
225 ORDER BY branchcode DESC LIMIT 1
227 $sth->execute( $module, $code, $branchcode, $message_transport_type, $lang );
228 my $line = $sth->fetchrow_hashref
230 $line->{'content-type'} = 'text/html; charset="UTF-8"' if $line->{is_html};
240 module => 'circulation',
246 Delete the letter. The mtt parameter is facultative.
247 If not given, all templates mathing the other parameters will be removed.
253 my $branchcode = $params->{branchcode};
254 my $module = $params->{module};
255 my $code = $params->{code};
256 my $mtt = $params->{mtt};
257 my $lang = $params->{lang};
258 my $dbh = C4::Context->dbh;
265 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
266 . ( $lang? q| AND lang = ?| : q|| )
267 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
272 my $err = &SendAlerts($type, $externalid, $letter_code);
275 - $type : the type of alert
276 - $externalid : the id of the "object" to query
277 - $letter_code : the notice template to use
279 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
281 Currently it supports ($type):
282 - claim serial issues (claimissues)
283 - claim acquisition orders (claimacquisition)
284 - send acquisition orders to the vendor (orderacquisition)
285 - notify patrons about newly received serial issues (issue)
286 - notify patrons when their account is created (members)
288 Returns undef or { error => 'message } on failure.
289 Returns true on success.
294 my ( $type, $externalid, $letter_code ) = @_;
295 my $dbh = C4::Context->dbh;
296 if ( $type eq 'issue' ) {
298 # prepare the letter...
299 # search the subscriptionid
302 "SELECT subscriptionid FROM serial WHERE serialid=?");
303 $sth->execute($externalid);
304 my ($subscriptionid) = $sth->fetchrow
305 or warn( "No subscription for '$externalid'" ),
308 # search the biblionumber
311 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
312 $sth->execute($subscriptionid);
313 my ($biblionumber) = $sth->fetchrow
314 or warn( "No biblionumber for '$subscriptionid'" ),
318 # find the list of subscribers to notify
319 my $subscription = Koha::Subscriptions->find( $subscriptionid );
320 my $subscribers = $subscription->subscribers;
321 while ( my $patron = $subscribers->next ) {
322 my $email = $patron->email or next;
324 # warn "sending issues...";
325 my $userenv = C4::Context->userenv;
326 my $library = $patron->library;
327 my $letter = GetPreparedLetter (
329 letter_code => $letter_code,
330 branchcode => $userenv->{branch},
332 'branches' => $library->branchcode,
333 'biblio' => $biblionumber,
334 'biblioitems' => $biblionumber,
335 'borrowers' => $patron->unblessed,
336 'subscription' => $subscriptionid,
337 'serial' => $externalid,
343 my $message = Koha::Email->new();
344 my %mail = $message->create_message_headers(
347 from => $library->branchemail,
348 replyto => $library->branchreplyto,
349 sender => $library->branchreturnpath,
350 subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
351 message => $letter->{'is_html'}
352 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
353 Encode::encode( "UTF-8", "" . $letter->{'title'} ))
354 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
355 contenttype => $letter->{'is_html'}
356 ? 'text/html; charset="utf-8"'
357 : 'text/plain; charset="utf-8"',
360 unless( Mail::Sendmail::sendmail(%mail) ) {
361 carp $Mail::Sendmail::error;
362 return { error => $Mail::Sendmail::error };
366 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
368 # prepare the letter...
373 if ( $type eq 'claimacquisition') {
375 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
377 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
378 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
379 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
380 WHERE aqorders.ordernumber IN (
384 carp "No order selected";
385 return { error => "no_order_selected" };
387 $strsth .= join( ",", ('?') x @$externalid ) . ")";
388 $action = "ACQUISITION CLAIM";
389 $sthorders = $dbh->prepare($strsth);
390 $sthorders->execute( @$externalid );
391 $dataorders = $sthorders->fetchall_arrayref( {} );
394 if ($type eq 'claimissues') {
396 SELECT serial.*,subscription.*, biblio.*, biblioitems.*, aqbooksellers.*,
397 aqbooksellers.id AS booksellerid
399 LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
400 LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
401 LEFT JOIN biblioitems ON serial.biblionumber = biblioitems.biblionumber
402 LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
403 WHERE serial.serialid IN (
407 carp "No issues selected";
408 return { error => "no_issues_selected" };
411 $strsth .= join( ",", ('?') x @$externalid ) . ")";
412 $action = "SERIAL CLAIM";
413 $sthorders = $dbh->prepare($strsth);
414 $sthorders->execute( @$externalid );
415 $dataorders = $sthorders->fetchall_arrayref( {} );
418 if ( $type eq 'orderacquisition') {
420 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
422 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
423 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
424 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
425 WHERE aqbasket.basketno = ?
426 AND orderstatus IN ('new','ordered')
430 carp "No basketnumber given";
431 return { error => "no_basketno" };
433 $action = "ACQUISITION ORDER";
434 $sthorders = $dbh->prepare($strsth);
435 $sthorders->execute($externalid);
436 $dataorders = $sthorders->fetchall_arrayref( {} );
440 $dbh->prepare("select * from aqbooksellers where id=?");
441 $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
442 my $databookseller = $sthbookseller->fetchrow_hashref;
444 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
447 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
448 $sthcontact->execute( $dataorders->[0]->{booksellerid} );
449 my $datacontact = $sthcontact->fetchrow_hashref;
453 push @email, $databookseller->{bookselleremail} if $databookseller->{bookselleremail};
454 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
456 warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
457 return { error => "no_email" };
460 while ($addlcontact = $sthcontact->fetchrow_hashref) {
461 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
464 my $userenv = C4::Context->userenv;
465 my $letter = GetPreparedLetter (
467 letter_code => $letter_code,
468 branchcode => $userenv->{branch},
470 'branches' => $userenv->{branch},
471 'aqbooksellers' => $databookseller,
472 'aqcontacts' => $datacontact,
474 repeat => $dataorders,
476 ) or return { error => "no_letter" };
478 # Remove the order tag
479 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
482 my $library = Koha::Libraries->find( $userenv->{branch} );
484 To => join( ',', @email),
485 Cc => join( ',', @cc),
486 From => $library->branchemail || C4::Context->preference('KohaAdminEmailAddress'),
487 Subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
488 Message => $letter->{'is_html'}
489 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
490 Encode::encode( "UTF-8", "" . $letter->{'title'} ))
491 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
492 'Content-Type' => $letter->{'is_html'}
493 ? 'text/html; charset="utf-8"'
494 : 'text/plain; charset="utf-8"',
497 if ($type eq 'claimacquisition' || $type eq 'claimissues' ) {
498 $mail{'Reply-to'} = C4::Context->preference('ReplytoDefault')
499 if C4::Context->preference('ReplytoDefault');
500 $mail{'Sender'} = C4::Context->preference('ReturnpathDefault')
501 if C4::Context->preference('ReturnpathDefault');
502 $mail{'Bcc'} = $userenv->{emailaddress}
503 if C4::Context->preference("ClaimsBccCopy");
506 unless ( Mail::Sendmail::sendmail(%mail) ) {
507 carp $Mail::Sendmail::error;
508 return { error => $Mail::Sendmail::error };
516 . join( ',', @email )
521 ) if C4::Context->preference("LetterLog");
523 # send an "account details" notice to a newly created user
524 elsif ( $type eq 'members' ) {
525 my $library = Koha::Libraries->find( $externalid->{branchcode} )->unblessed;
526 my $letter = GetPreparedLetter (
528 letter_code => $letter_code,
529 branchcode => $externalid->{'branchcode'},
530 lang => $externalid->{lang} || 'default',
532 'branches' => $library,
533 'borrowers' => $externalid->{'borrowernumber'},
535 substitute => { 'borrowers.password' => $externalid->{'password'} },
538 return { error => "no_email" } unless $externalid->{'emailaddr'};
539 my $email = Koha::Email->new();
540 my %mail = $email->create_message_headers(
542 to => $externalid->{'emailaddr'},
543 from => $library->{branchemail},
544 replyto => $library->{branchreplyto},
545 sender => $library->{branchreturnpath},
546 subject => Encode::encode( "UTF-8", "" . $letter->{'title'} ),
547 message => $letter->{'is_html'}
548 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
549 Encode::encode( "UTF-8", "" . $letter->{'title'} ) )
550 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
551 contenttype => $letter->{'is_html'}
552 ? 'text/html; charset="utf-8"'
553 : 'text/plain; charset="utf-8"',
556 unless( Mail::Sendmail::sendmail(%mail) ) {
557 carp $Mail::Sendmail::error;
558 return { error => $Mail::Sendmail::error };
562 # If we come here, return an OK status
566 =head2 GetPreparedLetter( %params )
569 module => letter module, mandatory
570 letter_code => letter code, mandatory
571 branchcode => for letter selection, if missing default system letter taken
572 tables => a hashref with table names as keys. Values are either:
573 - a scalar - primary key value
574 - an arrayref - primary key values
575 - a hashref - full record
576 substitute => custom substitution key/value pairs
577 repeat => records to be substituted on consecutive lines:
578 - an arrayref - tries to guess what needs substituting by
579 taking remaining << >> tokensr; not recommended
580 - a hashref token => @tables - replaces <token> << >> << >> </token>
581 subtemplate for each @tables row; table is a hashref as above
582 want_librarian => boolean, if set to true triggers librarian details
583 substitution from the userenv
585 letter fields hashref (title & content useful)
589 sub GetPreparedLetter {
592 my $letter = $params{letter};
595 my $module = $params{module} or croak "No module";
596 my $letter_code = $params{letter_code} or croak "No letter_code";
597 my $branchcode = $params{branchcode} || '';
598 my $mtt = $params{message_transport_type} || 'email';
599 my $lang = $params{lang} || 'default';
601 $letter = getletter( $module, $letter_code, $branchcode, $mtt, $lang );
604 $letter = getletter( $module, $letter_code, $branchcode, $mtt, 'default' )
605 or warn( "No $module $letter_code letter transported by " . $mtt ),
610 my $tables = $params{tables} || {};
611 my $substitute = $params{substitute} || {};
612 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
613 my $repeat = $params{repeat};
614 %$tables || %$substitute || $repeat || %$loops
615 or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
617 my $want_librarian = $params{want_librarian};
620 while ( my ($token, $val) = each %$substitute ) {
621 if ( $token eq 'items.content' ) {
622 $val =~ s|\n|<br/>|g if $letter->{is_html};
625 $letter->{title} =~ s/<<$token>>/$val/g;
626 $letter->{content} =~ s/<<$token>>/$val/g;
630 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
631 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
633 if ($want_librarian) {
634 # parsing librarian name
635 my $userenv = C4::Context->userenv;
636 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
637 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
638 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
641 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
644 if (ref ($repeat) eq 'ARRAY' ) {
645 $repeat_no_enclosing_tags = $repeat;
647 $repeat_enclosing_tags = $repeat;
651 if ($repeat_enclosing_tags) {
652 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
653 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
656 my %subletter = ( title => '', content => $subcontent );
657 _substitute_tables( \%subletter, $_ );
660 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
666 _substitute_tables( $letter, $tables );
669 if ($repeat_no_enclosing_tags) {
670 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
675 $c =~ s/<<count>>/$i/go;
676 foreach my $field ( keys %{$_} ) {
677 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
681 } @$repeat_no_enclosing_tags;
683 my $replaceby = join( "\n", @lines );
684 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
688 $letter->{content} = _process_tt(
690 content => $letter->{content},
693 substitute => $substitute,
697 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
702 sub _substitute_tables {
703 my ( $letter, $tables ) = @_;
704 while ( my ($table, $param) = each %$tables ) {
707 my $ref = ref $param;
710 if ($ref && $ref eq 'HASH') {
714 my $sth = _parseletter_sth($table);
716 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
719 $sth->execute( $ref ? @$param : $param );
721 $values = $sth->fetchrow_hashref;
725 _parseletter ( $letter, $table, $values );
729 sub _parseletter_sth {
733 carp "ERROR: _parseletter_sth() called without argument (table)";
736 # NOTE: we used to check whether we had a statement handle cached in
737 # a %handles module-level variable. This was a dumb move and
738 # broke things for the rest of us. prepare_cached is a better
739 # way to cache statement handles anyway.
741 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
742 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
743 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
744 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
745 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE itemnumber = ? ORDER BY timestamp DESC LIMIT 1" :
746 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
747 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
748 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
749 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
750 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
751 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
752 ($table eq 'opac_news' ) ? "SELECT * FROM $table WHERE idnew = ?" :
753 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
754 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
755 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
756 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
759 warn "ERROR: No _parseletter_sth query for table '$table'";
760 return; # nothing to get
762 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
763 warn "ERROR: Failed to prepare query: '$query'";
766 return $sth; # now cache is populated for that $table
769 =head2 _parseletter($letter, $table, $values)
772 - $letter : a hash to letter fields (title & content useful)
773 - $table : the Koha table to parse.
774 - $values_in : table record hashref
775 parse all fields from a table, and replace values in title & content with the appropriate value
776 (not exported sub, used only internally)
781 my ( $letter, $table, $values_in ) = @_;
783 # Work on a local copy of $values_in (passed by reference) to avoid side effects
784 # in callers ( by changing / formatting values )
785 my $values = $values_in ? { %$values_in } : {};
787 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
788 $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
791 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
792 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
795 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
796 my $todaysdate = output_pref( DateTime->now() );
797 $letter->{content} =~ s/<<today>>/$todaysdate/go;
800 while ( my ($field, $val) = each %$values ) {
801 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
802 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
803 #Therefore adding the test on biblio. This includes biblioitems,
804 #but excludes items. Removed unneeded global and lookahead.
806 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
807 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
808 $val = $av->count ? $av->next->lib : '';
812 my $replacedby = defined ($val) ? $val : '';
814 and not $replacedby =~ m|0000-00-00|
815 and not $replacedby =~ m|9999-12-31|
816 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
818 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
819 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
820 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
822 for my $letter_field ( qw( title content ) ) {
823 my $filter_string_used = q{};
824 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
825 # We overwrite $dateonly if the filter exists and we have a time in the datetime
826 $filter_string_used = $1 || q{};
827 $dateonly = $1 unless $dateonly;
829 my $replacedby_date = eval {
830 output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
833 if ( $letter->{ $letter_field } ) {
834 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
835 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
839 # Other fields replacement
841 for my $letter_field ( qw( title content ) ) {
842 if ( $letter->{ $letter_field } ) {
843 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
844 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
850 if ($table eq 'borrowers' && $letter->{content}) {
851 if ( my $attributes = GetBorrowerAttributes($values->{borrowernumber}) ) {
853 foreach (@$attributes) {
854 my $code = $_->{code};
855 my $val = $_->{value_description} || $_->{value};
856 $val =~ s/\p{P}(?=$)//g if $val;
857 next unless $val gt '';
859 push @{ $attr{$code} }, $val;
861 while ( my ($code, $val_ar) = each %attr ) {
862 my $replacefield = "<<borrower-attribute:$code>>";
863 my $replacedby = join ',', @$val_ar;
864 $letter->{content} =~ s/$replacefield/$replacedby/g;
873 my $success = EnqueueLetter( { letter => $letter,
874 borrowernumber => '12', message_transport_type => 'email' } )
876 places a letter in the message_queue database table, which will
877 eventually get processed (sent) by the process_message_queue.pl
878 cronjob when it calls SendQueuedMessages.
880 return message_id on success
885 my $params = shift or return;
887 return unless exists $params->{'letter'};
888 # return unless exists $params->{'borrowernumber'};
889 return unless exists $params->{'message_transport_type'};
891 my $content = $params->{letter}->{content};
892 $content =~ s/\s+//g if(defined $content);
893 if ( not defined $content or $content eq '' ) {
894 warn "Trying to add an empty message to the message queue" if $debug;
898 # If we have any attachments we should encode then into the body.
899 if ( $params->{'attachments'} ) {
900 $params->{'letter'} = _add_attachments(
901 { letter => $params->{'letter'},
902 attachments => $params->{'attachments'},
903 message => MIME::Lite->new( Type => 'multipart/mixed' ),
908 my $dbh = C4::Context->dbh();
909 my $statement = << 'ENDSQL';
910 INSERT INTO message_queue
911 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type )
913 ( ?, ?, ?, ?, ?, ?, ?, NOW(), ?, ?, ? )
916 my $sth = $dbh->prepare($statement);
917 my $result = $sth->execute(
918 $params->{'borrowernumber'}, # borrowernumber
919 $params->{'letter'}->{'title'}, # subject
920 $params->{'letter'}->{'content'}, # content
921 $params->{'letter'}->{'metadata'} || '', # metadata
922 $params->{'letter'}->{'code'} || '', # letter_code
923 $params->{'message_transport_type'}, # message_transport_type
925 $params->{'to_address'}, # to_address
926 $params->{'from_address'}, # from_address
927 $params->{'letter'}->{'content-type'}, # content_type
929 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
932 =head2 SendQueuedMessages ([$hashref])
934 my $sent = SendQueuedMessages({
935 letter_code => $letter_code,
936 borrowernumber => $who_letter_is_for,
942 Sends all of the 'pending' items in the message queue, unless
943 parameters are passed.
945 The letter_code, borrowernumber and limit parameters are used
946 to build a parameter set for _get_unsent_messages, thus limiting
947 which pending messages will be processed. They are all optional.
949 The verbose parameter can be used to generate debugging output.
952 Returns number of messages sent.
956 sub SendQueuedMessages {
959 my $which_unsent_messages = {
960 'limit' => $params->{'limit'} // 0,
961 'borrowernumber' => $params->{'borrowernumber'} // q{},
962 'letter_code' => $params->{'letter_code'} // q{},
963 'type' => $params->{'type'} // q{},
965 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
966 MESSAGE: foreach my $message ( @$unsent_messages ) {
967 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
968 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
969 $message_object->make_column_dirty('status');
970 return unless $message_object->store;
972 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
973 warn sprintf( 'sending %s message to patron: %s',
974 $message->{'message_transport_type'},
975 $message->{'borrowernumber'} || 'Admin' )
976 if $params->{'verbose'} or $debug;
977 # This is just begging for subclassing
978 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
979 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
980 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
982 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
983 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
984 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
985 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
986 unless ( $sms_provider ) {
987 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
988 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
991 unless ( $patron->smsalertnumber ) {
992 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
993 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
996 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
997 $message->{to_address} .= '@' . $sms_provider->domain();
998 _update_message_to_address($message->{'message_id'},$message->{to_address});
999 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1001 _send_message_by_sms( $message );
1005 return scalar( @$unsent_messages );
1008 =head2 GetRSSMessages
1010 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1012 returns a listref of all queued RSS messages for a particular person.
1016 sub GetRSSMessages {
1019 return unless $params;
1020 return unless ref $params;
1021 return unless $params->{'borrowernumber'};
1023 return _get_unsent_messages( { message_transport_type => 'rss',
1024 limit => $params->{'limit'},
1025 borrowernumber => $params->{'borrowernumber'}, } );
1028 =head2 GetPrintMessages
1030 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1032 Returns a arrayref of all queued print messages (optionally, for a particular
1037 sub GetPrintMessages {
1038 my $params = shift || {};
1040 return _get_unsent_messages( { message_transport_type => 'print',
1041 borrowernumber => $params->{'borrowernumber'},
1045 =head2 GetQueuedMessages ([$hashref])
1047 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1049 fetches messages out of the message queue.
1052 list of hashes, each has represents a message in the message queue.
1056 sub GetQueuedMessages {
1059 my $dbh = C4::Context->dbh();
1060 my $statement = << 'ENDSQL';
1061 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued
1067 if ( exists $params->{'borrowernumber'} ) {
1068 push @whereclauses, ' borrowernumber = ? ';
1069 push @query_params, $params->{'borrowernumber'};
1072 if ( @whereclauses ) {
1073 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1076 if ( defined $params->{'limit'} ) {
1077 $statement .= ' LIMIT ? ';
1078 push @query_params, $params->{'limit'};
1081 my $sth = $dbh->prepare( $statement );
1082 my $result = $sth->execute( @query_params );
1083 return $sth->fetchall_arrayref({});
1086 =head2 GetMessageTransportTypes
1088 my @mtt = GetMessageTransportTypes();
1090 returns an arrayref of transport types
1094 sub GetMessageTransportTypes {
1095 my $dbh = C4::Context->dbh();
1096 my $mtts = $dbh->selectcol_arrayref("
1097 SELECT message_transport_type
1098 FROM message_transport_types
1099 ORDER BY message_transport_type
1106 my $message = C4::Letters::Message($message_id);
1111 my ( $message_id ) = @_;
1112 return unless $message_id;
1113 my $dbh = C4::Context->dbh;
1114 return $dbh->selectrow_hashref(q|
1115 SELECT message_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type
1117 WHERE message_id = ?
1118 |, {}, $message_id );
1121 =head2 ResendMessage
1123 Attempt to resend a message which has failed previously.
1125 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1127 Updates the message to 'pending' status so that
1128 it will be resent later on.
1130 returns 1 on success, 0 on failure, undef if no message was found
1135 my $message_id = shift;
1136 return unless $message_id;
1138 my $message = GetMessage( $message_id );
1139 return unless $message;
1141 if ( $message->{status} ne 'pending' ) {
1142 $rv = C4::Letters::_set_message_status({
1143 message_id => $message_id,
1144 status => 'pending',
1146 $rv = $rv > 0? 1: 0;
1147 # Clear destination email address to force address update
1148 _update_message_to_address( $message_id, undef ) if $rv &&
1149 $message->{message_transport_type} eq 'email';
1154 =head2 _add_attachements
1157 letter - the standard letter hashref
1158 attachments - listref of attachments. each attachment is a hashref of:
1159 type - the mime type, like 'text/plain'
1160 content - the actual attachment
1161 filename - the name of the attachment.
1162 message - a MIME::Lite object to attach these to.
1164 returns your letter object, with the content updated.
1168 sub _add_attachments {
1171 my $letter = $params->{'letter'};
1172 my $attachments = $params->{'attachments'};
1173 return $letter unless @$attachments;
1174 my $message = $params->{'message'};
1176 # First, we have to put the body in as the first attachment
1178 Type => $letter->{'content-type'} || 'TEXT',
1179 Data => $letter->{'is_html'}
1180 ? _wrap_html($letter->{'content'}, $letter->{'title'})
1181 : $letter->{'content'},
1184 foreach my $attachment ( @$attachments ) {
1186 Type => $attachment->{'type'},
1187 Data => $attachment->{'content'},
1188 Filename => $attachment->{'filename'},
1191 # we're forcing list context here to get the header, not the count back from grep.
1192 ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1193 $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1194 $letter->{'content'} = $message->body_as_string;
1200 =head2 _get_unsent_messages
1202 This function's parameter hash reference takes the following
1203 optional named parameters:
1204 message_transport_type: method of message sending (e.g. email, sms, etc.)
1205 borrowernumber : who the message is to be sent
1206 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1207 limit : maximum number of messages to send
1209 This function returns an array of matching hash referenced rows from
1210 message_queue with some borrower information added.
1214 sub _get_unsent_messages {
1217 my $dbh = C4::Context->dbh();
1219 SELECT mq.message_id, mq.borrowernumber, mq.subject, mq.content, mq.message_transport_type, mq.status, mq.time_queued, mq.from_address, mq.to_address, mq.content_type, b.branchcode, mq.letter_code
1220 FROM message_queue mq
1221 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1225 my @query_params = ('pending');
1226 if ( ref $params ) {
1227 if ( $params->{'message_transport_type'} ) {
1228 $statement .= ' AND mq.message_transport_type = ? ';
1229 push @query_params, $params->{'message_transport_type'};
1231 if ( $params->{'borrowernumber'} ) {
1232 $statement .= ' AND mq.borrowernumber = ? ';
1233 push @query_params, $params->{'borrowernumber'};
1235 if ( $params->{'letter_code'} ) {
1236 $statement .= ' AND mq.letter_code = ? ';
1237 push @query_params, $params->{'letter_code'};
1239 if ( $params->{'type'} ) {
1240 $statement .= ' AND message_transport_type = ? ';
1241 push @query_params, $params->{'type'};
1243 if ( $params->{'limit'} ) {
1244 $statement .= ' limit ? ';
1245 push @query_params, $params->{'limit'};
1249 $debug and warn "_get_unsent_messages SQL: $statement";
1250 $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1251 my $sth = $dbh->prepare( $statement );
1252 my $result = $sth->execute( @query_params );
1253 return $sth->fetchall_arrayref({});
1256 sub _send_message_by_email {
1257 my $message = shift or return;
1258 my ($username, $password, $method) = @_;
1260 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1261 my $to_address = $message->{'to_address'};
1262 unless ($to_address) {
1264 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1265 _set_message_status( { message_id => $message->{'message_id'},
1266 status => 'failed' } );
1269 $to_address = $patron->notice_email_address;
1270 unless ($to_address) {
1271 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1272 # warning too verbose for this more common case?
1273 _set_message_status( { message_id => $message->{'message_id'},
1274 status => 'failed' } );
1279 my $utf8 = decode('MIME-Header', $message->{'subject'} );
1280 $message->{subject}= encode('MIME-Header', $utf8);
1281 my $subject = encode('UTF-8', $message->{'subject'});
1282 my $content = encode('UTF-8', $message->{'content'});
1283 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1284 my $is_html = $content_type =~ m/html/io;
1285 my $branch_email = undef;
1286 my $branch_replyto = undef;
1287 my $branch_returnpath = undef;
1289 my $library = $patron->library;
1290 $branch_email = $library->branchemail;
1291 $branch_replyto = $library->branchreplyto;
1292 $branch_returnpath = $library->branchreturnpath;
1294 my $email = Koha::Email->new();
1295 my %sendmail_params = $email->create_message_headers(
1298 from => $message->{'from_address'} || $branch_email,
1299 replyto => $branch_replyto,
1300 sender => $branch_returnpath,
1301 subject => $subject,
1302 message => $is_html ? _wrap_html( $content, $subject ) : $content,
1303 contenttype => $content_type
1307 $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
1308 if ( my $bcc = C4::Context->preference('NoticeBcc') ) {
1309 $sendmail_params{ Bcc } = $bcc;
1312 _update_message_to_address($message->{'message_id'},$to_address) unless $message->{to_address}; #if initial message address was empty, coming here means that a to address was found and queue should be updated
1314 if ( Mail::Sendmail::sendmail( %sendmail_params ) ) {
1315 _set_message_status( { message_id => $message->{'message_id'},
1316 status => 'sent' } );
1319 _set_message_status( { message_id => $message->{'message_id'},
1320 status => 'failed' } );
1321 carp $Mail::Sendmail::error;
1327 my ($content, $title) = @_;
1329 my $css = C4::Context->preference("NoticeCSS") || '';
1330 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1332 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1333 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1334 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1336 <title>$title</title>
1337 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1348 my ( $message ) = @_;
1349 my $dbh = C4::Context->dbh;
1350 my $count = $dbh->selectrow_array(q|
1353 WHERE message_transport_type = ?
1354 AND borrowernumber = ?
1356 AND CAST(time_queued AS date) = CAST(NOW() AS date)
1359 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1363 sub _send_message_by_sms {
1364 my $message = shift or return;
1365 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1367 unless ( $patron and $patron->smsalertnumber ) {
1368 _set_message_status( { message_id => $message->{'message_id'},
1369 status => 'failed' } );
1373 if ( _is_duplicate( $message ) ) {
1374 _set_message_status( { message_id => $message->{'message_id'},
1375 status => 'failed' } );
1379 my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
1380 message => $message->{'content'},
1382 _set_message_status( { message_id => $message->{'message_id'},
1383 status => ($success ? 'sent' : 'failed') } );
1387 sub _update_message_to_address {
1389 my $dbh = C4::Context->dbh();
1390 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1393 sub _set_message_status {
1394 my $params = shift or return;
1396 foreach my $required_parameter ( qw( message_id status ) ) {
1397 return unless exists $params->{ $required_parameter };
1400 my $dbh = C4::Context->dbh();
1401 my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1402 my $sth = $dbh->prepare( $statement );
1403 my $result = $sth->execute( $params->{'status'},
1404 $params->{'message_id'} );
1409 my ( $params ) = @_;
1411 my $content = $params->{content};
1412 my $tables = $params->{tables};
1413 my $loops = $params->{loops};
1414 my $substitute = $params->{substitute} || {};
1416 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1417 my $template = Template->new(
1421 PLUGIN_BASE => 'Koha::Template::Plugin',
1422 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1423 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1425 ENCODING => 'UTF-8',
1427 ) or die Template->error();
1429 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1431 $content = add_tt_filters( $content );
1432 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1435 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1440 sub _get_tt_params {
1441 my ($tables, $is_a_loop) = @_;
1447 article_requests => {
1448 module => 'Koha::ArticleRequests',
1449 singular => 'article_request',
1450 plural => 'article_requests',
1454 module => 'Koha::Biblios',
1455 singular => 'biblio',
1456 plural => 'biblios',
1457 pk => 'biblionumber',
1460 module => 'Koha::Biblioitems',
1461 singular => 'biblioitem',
1462 plural => 'biblioitems',
1463 pk => 'biblioitemnumber',
1466 module => 'Koha::Patrons',
1467 singular => 'borrower',
1468 plural => 'borrowers',
1469 pk => 'borrowernumber',
1472 module => 'Koha::Libraries',
1473 singular => 'branch',
1474 plural => 'branches',
1478 module => 'Koha::Items',
1484 module => 'Koha::News',
1490 module => 'Koha::Acquisition::Orders',
1491 singular => 'order',
1493 pk => 'ordernumber',
1496 module => 'Koha::Holds',
1499 fk => [ 'borrowernumber', 'biblionumber' ],
1502 module => 'Koha::Serials',
1503 singular => 'serial',
1504 plural => 'serials',
1508 module => 'Koha::Subscriptions',
1509 singular => 'subscription',
1510 plural => 'subscriptions',
1511 pk => 'subscriptionid',
1514 module => 'Koha::Suggestions',
1515 singular => 'suggestion',
1516 plural => 'suggestions',
1517 pk => 'suggestionid',
1520 module => 'Koha::Checkouts',
1521 singular => 'checkout',
1522 plural => 'checkouts',
1526 module => 'Koha::Old::Checkouts',
1527 singular => 'old_checkout',
1528 plural => 'old_checkouts',
1532 module => 'Koha::Checkouts',
1533 singular => 'overdue',
1534 plural => 'overdues',
1537 borrower_modifications => {
1538 module => 'Koha::Patron::Modifications',
1539 singular => 'patron_modification',
1540 plural => 'patron_modifications',
1541 fk => 'verification_token',
1545 foreach my $table ( keys %$tables ) {
1546 next unless $config->{$table};
1548 my $ref = ref( $tables->{$table} ) || q{};
1549 my $module = $config->{$table}->{module};
1551 if ( can_load( modules => { $module => undef } ) ) {
1552 my $pk = $config->{$table}->{pk};
1553 my $fk = $config->{$table}->{fk};
1556 my $values = $tables->{$table} || [];
1557 unless ( ref( $values ) eq 'ARRAY' ) {
1558 croak "ERROR processing table $table. Wrong API call.";
1560 my $key = $pk ? $pk : $fk;
1561 # $key does not come from user input
1562 my $objects = $module->search(
1563 { $key => $values },
1565 # We want to retrieve the data in the same order
1567 # field is a MySQLism, but they are no other way to do it
1568 # To be generic we could do it in perl, but we will need to fetch
1569 # all the data then order them
1570 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1573 $params->{ $config->{$table}->{plural} } = $objects;
1575 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1576 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1578 if ( $fk ) { # Using a foreign key for lookup
1579 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1581 foreach my $key ( @$fk ) {
1582 $search->{$key} = $id->{$key};
1584 $object = $module->search( $search )->last();
1585 } else { # Foreign key is single column
1586 $object = $module->search( { $fk => $id } )->last();
1588 } else { # using the table's primary key for lookup
1589 $object = $module->find($id);
1591 $params->{ $config->{$table}->{singular} } = $object;
1593 else { # $ref eq 'ARRAY'
1595 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1596 $object = $module->search( { $pk => $tables->{$table} } )->last();
1598 else { # Params are mutliple foreign keys
1599 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1601 $params->{ $config->{$table}->{singular} } = $object;
1605 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1609 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1614 =head3 add_tt_filters
1616 $content = add_tt_filters( $content );
1618 Add TT filters to some specific fields if needed.
1620 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1624 sub add_tt_filters {
1625 my ( $content ) = @_;
1626 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1627 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1631 =head2 get_item_content
1633 my $item = Koha::Items->find(...)->unblessed;
1634 my @item_content_fields = qw( date_due title barcode author itemnumber );
1635 my $item_content = C4::Letters::get_item_content({
1637 item_content_fields => \@item_content_fields
1640 This function generates a tab-separated list of values for the passed item. Dates
1641 are formatted following the current setup.
1645 sub get_item_content {
1646 my ( $params ) = @_;
1647 my $item = $params->{item};
1648 my $dateonly = $params->{dateonly} || 0;
1649 my $item_content_fields = $params->{item_content_fields} || [];
1651 return unless $item;
1653 my @item_info = map {
1657 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1661 } @$item_content_fields;
1662 return join( "\t", @item_info ) . "\n";