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 );
43 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
49 &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &addalert &getalert &delalert &SendAlerts &GetPrintMessages &GetMessageTransportTypes
55 C4::Letters - Give functions for Letters management
63 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
64 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)
66 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
68 =head2 GetLetters([$module])
70 $letters = &GetLetters($module);
71 returns informations about letters.
72 if needed, $module filters for letters given module
74 DEPRECATED - You must use Koha::Notice::Templates instead
75 The group by clause is confusing and can lead to issues
81 my $module = $filters->{module};
82 my $code = $filters->{code};
83 my $branchcode = $filters->{branchcode};
84 my $dbh = C4::Context->dbh;
85 my $letters = $dbh->selectall_arrayref(
87 SELECT code, module, name
91 . ( $module ? q| AND module = ?| : q|| )
92 . ( $code ? q| AND code = ?| : q|| )
93 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
94 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
95 , ( $module ? $module : () )
96 , ( $code ? $code : () )
97 , ( defined $branchcode ? $branchcode : () )
103 =head2 GetLetterTemplates
105 my $letter_templates = GetLetterTemplates(
107 module => 'circulation',
109 branchcode => 'CPL', # '' for default,
113 Return a hashref of letter templates.
117 sub GetLetterTemplates {
120 my $module = $params->{module};
121 my $code = $params->{code};
122 my $branchcode = $params->{branchcode} // '';
123 my $dbh = C4::Context->dbh;
124 my $letters = $dbh->selectall_arrayref(
126 SELECT module, code, branchcode, name, is_html, title, content, message_transport_type, lang
133 , $module, $code, $branchcode
139 =head2 GetLettersAvailableForALibrary
141 my $letters = GetLettersAvailableForALibrary(
143 branchcode => 'CPL', # '' for default
144 module => 'circulation',
148 Return an arrayref of letters, sorted by name.
149 If a specific letter exist for the given branchcode, it will be retrieve.
150 Otherwise the default letter will be.
154 sub GetLettersAvailableForALibrary {
156 my $branchcode = $filters->{branchcode};
157 my $module = $filters->{module};
159 croak "module should be provided" unless $module;
161 my $dbh = C4::Context->dbh;
162 my $default_letters = $dbh->selectall_arrayref(
164 SELECT module, code, branchcode, name
168 . q| AND branchcode = ''|
169 . ( $module ? q| AND module = ?| : q|| )
170 . q| ORDER BY name|, { Slice => {} }
171 , ( $module ? $module : () )
174 my $specific_letters;
176 $specific_letters = $dbh->selectall_arrayref(
178 SELECT module, code, branchcode, name
182 . q| AND branchcode = ?|
183 . ( $module ? q| AND module = ?| : q|| )
184 . q| ORDER BY name|, { Slice => {} }
186 , ( $module ? $module : () )
191 for my $l (@$default_letters) {
192 $letters{ $l->{code} } = $l;
194 for my $l (@$specific_letters) {
195 # Overwrite the default letter with the specific one.
196 $letters{ $l->{code} } = $l;
199 return [ map { $letters{$_} }
200 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
206 my ( $module, $code, $branchcode, $message_transport_type, $lang) = @_;
207 $message_transport_type //= '%';
208 $lang = 'default' unless( $lang && C4::Context->preference('TranslateNotices') );
211 my $only_my_library = C4::Context->only_my_library;
212 if ( $only_my_library and $branchcode ) {
213 $branchcode = C4::Context::mybranch();
217 my $dbh = C4::Context->dbh;
218 my $sth = $dbh->prepare(q{
221 WHERE module=? AND code=? AND (branchcode = ? OR branchcode = '')
222 AND message_transport_type LIKE ?
224 ORDER BY branchcode DESC LIMIT 1
226 $sth->execute( $module, $code, $branchcode, $message_transport_type, $lang );
227 my $line = $sth->fetchrow_hashref
229 $line->{'content-type'} = 'text/html; charset="UTF-8"' if $line->{is_html};
239 module => 'circulation',
245 Delete the letter. The mtt parameter is facultative.
246 If not given, all templates mathing the other parameters will be removed.
252 my $branchcode = $params->{branchcode};
253 my $module = $params->{module};
254 my $code = $params->{code};
255 my $mtt = $params->{mtt};
256 my $lang = $params->{lang};
257 my $dbh = C4::Context->dbh;
264 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
265 . ( $lang? q| AND lang = ?| : q|| )
266 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
269 =head2 addalert ($borrowernumber, $type, $externalid)
272 - $borrowernumber : the number of the borrower subscribing to the alert
273 - $type : the type of alert.
274 - $externalid : the primary key of the object to put alert on. For issues, the alert is made on subscriptionid.
276 create an alert and return the alertid (primary key)
281 my ( $borrowernumber, $type, $externalid ) = @_;
282 my $dbh = C4::Context->dbh;
285 "insert into alert (borrowernumber, type, externalid) values (?,?,?)");
286 $sth->execute( $borrowernumber, $type, $externalid );
288 # get the alert number newly created and return it
289 my $alertid = $dbh->{'mysql_insertid'};
293 =head2 delalert ($alertid)
296 - alertid : the alert id
302 my $alertid = shift or die "delalert() called without valid argument (alertid)"; # it's gonna die anyway.
303 $debug and warn "delalert: deleting alertid $alertid";
304 my $sth = C4::Context->dbh->prepare("delete from alert where alertid=?");
305 $sth->execute($alertid);
308 =head2 getalert ([$borrowernumber], [$type], [$externalid])
311 - $borrowernumber : the number of the borrower subscribing to the alert
312 - $type : the type of alert.
313 - $externalid : the primary key of the object to put alert on. For issues, the alert is made on subscriptionid.
314 all parameters NON mandatory. If a parameter is omitted, the query is done without the corresponding parameter. For example, without $externalid, returns all alerts for a borrower on a topic.
319 my ( $borrowernumber, $type, $externalid ) = @_;
320 my $dbh = C4::Context->dbh;
321 my $query = "SELECT a.*, b.branchcode FROM alert a JOIN borrowers b USING(borrowernumber) WHERE 1";
323 if ($borrowernumber and $borrowernumber =~ /^\d+$/) {
324 $query .= " AND borrowernumber=?";
325 push @bind, $borrowernumber;
328 $query .= " AND type=?";
332 $query .= " AND externalid=?";
333 push @bind, $externalid;
335 my $sth = $dbh->prepare($query);
336 $sth->execute(@bind);
337 return $sth->fetchall_arrayref({});
342 my $err = &SendAlerts($type, $externalid, $letter_code);
345 - $type : the type of alert
346 - $externalid : the id of the "object" to query
347 - $letter_code : the notice template to use
349 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
351 Currently it supports ($type):
352 - claim serial issues (claimissues)
353 - claim acquisition orders (claimacquisition)
354 - send acquisition orders to the vendor (orderacquisition)
355 - notify patrons about newly received serial issues (issue)
356 - notify patrons when their account is created (members)
358 Returns undef or { error => 'message } on failure.
359 Returns true on success.
364 my ( $type, $externalid, $letter_code ) = @_;
365 my $dbh = C4::Context->dbh;
366 if ( $type eq 'issue' ) {
368 # prepare the letter...
369 # search the subscriptionid
372 "SELECT subscriptionid FROM serial WHERE serialid=?");
373 $sth->execute($externalid);
374 my ($subscriptionid) = $sth->fetchrow
375 or warn( "No subscription for '$externalid'" ),
378 # search the biblionumber
381 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
382 $sth->execute($subscriptionid);
383 my ($biblionumber) = $sth->fetchrow
384 or warn( "No biblionumber for '$subscriptionid'" ),
388 # find the list of borrowers to alert
389 my $alerts = getalert( '', 'issue', $subscriptionid );
391 my $patron = Koha::Patrons->find( $_->{borrowernumber} );
392 next unless $patron; # Just in case
393 my $email = $patron->email or next;
395 # warn "sending issues...";
396 my $userenv = C4::Context->userenv;
397 my $library = Koha::Libraries->find( $_->{branchcode} );
398 my $letter = GetPreparedLetter (
400 letter_code => $letter_code,
401 branchcode => $userenv->{branch},
403 'branches' => $_->{branchcode},
404 'biblio' => $biblionumber,
405 'biblioitems' => $biblionumber,
406 'borrowers' => $patron->unblessed,
407 'subscription' => $subscriptionid,
408 'serial' => $externalid,
414 my $message = Koha::Email->new();
415 my %mail = $message->create_message_headers(
418 from => $library->branchemail,
419 replyto => $library->branchreplyto,
420 sender => $library->branchreturnpath,
421 subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
422 message => $letter->{'is_html'}
423 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
424 Encode::encode( "UTF-8", "" . $letter->{'title'} ))
425 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
426 contenttype => $letter->{'is_html'}
427 ? 'text/html; charset="utf-8"'
428 : 'text/plain; charset="utf-8"',
431 unless( Mail::Sendmail::sendmail(%mail) ) {
432 carp $Mail::Sendmail::error;
433 return { error => $Mail::Sendmail::error };
437 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
439 # prepare the letter...
444 if ( $type eq 'claimacquisition') {
446 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
448 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
449 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
450 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
451 WHERE aqorders.ordernumber IN (
455 carp "No order selected";
456 return { error => "no_order_selected" };
458 $strsth .= join( ",", ('?') x @$externalid ) . ")";
459 $action = "ACQUISITION CLAIM";
460 $sthorders = $dbh->prepare($strsth);
461 $sthorders->execute( @$externalid );
462 $dataorders = $sthorders->fetchall_arrayref( {} );
465 if ($type eq 'claimissues') {
467 SELECT serial.*,subscription.*, biblio.*, aqbooksellers.*,
468 aqbooksellers.id AS booksellerid
470 LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
471 LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
472 LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
473 WHERE serial.serialid IN (
477 carp "No Order selected";
478 return { error => "no_order_selected" };
481 $strsth .= join( ",", ('?') x @$externalid ) . ")";
482 $action = "CLAIM ISSUE";
483 $sthorders = $dbh->prepare($strsth);
484 $sthorders->execute( @$externalid );
485 $dataorders = $sthorders->fetchall_arrayref( {} );
488 if ( $type eq 'orderacquisition') {
490 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
492 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
493 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
494 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
495 WHERE aqbasket.basketno = ?
496 AND orderstatus IN ('new','ordered')
500 carp "No basketnumber given";
501 return { error => "no_basketno" };
503 $action = "ACQUISITION ORDER";
504 $sthorders = $dbh->prepare($strsth);
505 $sthorders->execute($externalid);
506 $dataorders = $sthorders->fetchall_arrayref( {} );
510 $dbh->prepare("select * from aqbooksellers where id=?");
511 $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
512 my $databookseller = $sthbookseller->fetchrow_hashref;
514 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
517 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
518 $sthcontact->execute( $dataorders->[0]->{booksellerid} );
519 my $datacontact = $sthcontact->fetchrow_hashref;
523 push @email, $databookseller->{bookselleremail} if $databookseller->{bookselleremail};
524 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
526 warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
527 return { error => "no_email" };
530 while ($addlcontact = $sthcontact->fetchrow_hashref) {
531 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
534 my $userenv = C4::Context->userenv;
535 my $letter = GetPreparedLetter (
537 letter_code => $letter_code,
538 branchcode => $userenv->{branch},
540 'branches' => $userenv->{branch},
541 'aqbooksellers' => $databookseller,
542 'aqcontacts' => $datacontact,
544 repeat => $dataorders,
546 ) or return { error => "no_letter" };
548 # Remove the order tag
549 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
552 my $library = Koha::Libraries->find( $userenv->{branch} );
554 To => join( ',', @email),
555 Cc => join( ',', @cc),
556 From => $library->branchemail || C4::Context->preference('KohaAdminEmailAddress'),
557 Subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
558 Message => $letter->{'is_html'}
559 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
560 Encode::encode( "UTF-8", "" . $letter->{'title'} ))
561 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
562 'Content-Type' => $letter->{'is_html'}
563 ? 'text/html; charset="utf-8"'
564 : 'text/plain; charset="utf-8"',
567 if ($type eq 'claimacquisition' || $type eq 'claimissues' ) {
568 $mail{'Reply-to'} = C4::Context->preference('ReplytoDefault')
569 if C4::Context->preference('ReplytoDefault');
570 $mail{'Sender'} = C4::Context->preference('ReturnpathDefault')
571 if C4::Context->preference('ReturnpathDefault');
572 $mail{'Bcc'} = $userenv->{emailaddress}
573 if C4::Context->preference("ClaimsBccCopy");
576 unless ( Mail::Sendmail::sendmail(%mail) ) {
577 carp $Mail::Sendmail::error;
578 return { error => $Mail::Sendmail::error };
586 . join( ',', @email )
591 ) if C4::Context->preference("LetterLog");
593 # send an "account details" notice to a newly created user
594 elsif ( $type eq 'members' ) {
595 my $library = Koha::Libraries->find( $externalid->{branchcode} )->unblessed;
596 my $letter = GetPreparedLetter (
598 letter_code => $letter_code,
599 branchcode => $externalid->{'branchcode'},
601 'branches' => $library,
602 'borrowers' => $externalid->{'borrowernumber'},
604 substitute => { 'borrowers.password' => $externalid->{'password'} },
607 return { error => "no_email" } unless $externalid->{'emailaddr'};
608 my $email = Koha::Email->new();
609 my %mail = $email->create_message_headers(
611 to => $externalid->{'emailaddr'},
612 from => $library->{branchemail},
613 replyto => $library->{branchreplyto},
614 sender => $library->{branchreturnpath},
615 subject => Encode::encode( "UTF-8", "" . $letter->{'title'} ),
616 message => $letter->{'is_html'}
617 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
618 Encode::encode( "UTF-8", "" . $letter->{'title'} ) )
619 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
620 contenttype => $letter->{'is_html'}
621 ? 'text/html; charset="utf-8"'
622 : 'text/plain; charset="utf-8"',
625 unless( Mail::Sendmail::sendmail(%mail) ) {
626 carp $Mail::Sendmail::error;
627 return { error => $Mail::Sendmail::error };
631 # If we come here, return an OK status
635 =head2 GetPreparedLetter( %params )
638 module => letter module, mandatory
639 letter_code => letter code, mandatory
640 branchcode => for letter selection, if missing default system letter taken
641 tables => a hashref with table names as keys. Values are either:
642 - a scalar - primary key value
643 - an arrayref - primary key values
644 - a hashref - full record
645 substitute => custom substitution key/value pairs
646 repeat => records to be substituted on consecutive lines:
647 - an arrayref - tries to guess what needs substituting by
648 taking remaining << >> tokensr; not recommended
649 - a hashref token => @tables - replaces <token> << >> << >> </token>
650 subtemplate for each @tables row; table is a hashref as above
651 want_librarian => boolean, if set to true triggers librarian details
652 substitution from the userenv
654 letter fields hashref (title & content useful)
658 sub GetPreparedLetter {
661 my $letter = $params{letter};
664 my $module = $params{module} or croak "No module";
665 my $letter_code = $params{letter_code} or croak "No letter_code";
666 my $branchcode = $params{branchcode} || '';
667 my $mtt = $params{message_transport_type} || 'email';
668 my $lang = $params{lang} || 'default';
670 $letter = getletter( $module, $letter_code, $branchcode, $mtt, $lang );
673 $letter = getletter( $module, $letter_code, $branchcode, $mtt, 'default' )
674 or warn( "No $module $letter_code letter transported by " . $mtt ),
679 my $tables = $params{tables} || {};
680 my $substitute = $params{substitute} || {};
681 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
682 my $repeat = $params{repeat};
683 %$tables || %$substitute || $repeat || %$loops
684 or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
686 my $want_librarian = $params{want_librarian};
689 while ( my ($token, $val) = each %$substitute ) {
690 if ( $token eq 'items.content' ) {
691 $val =~ s|\n|<br/>|g if $letter->{is_html};
694 $letter->{title} =~ s/<<$token>>/$val/g;
695 $letter->{content} =~ s/<<$token>>/$val/g;
699 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
700 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
702 if ($want_librarian) {
703 # parsing librarian name
704 my $userenv = C4::Context->userenv;
705 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
706 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
707 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
710 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
713 if (ref ($repeat) eq 'ARRAY' ) {
714 $repeat_no_enclosing_tags = $repeat;
716 $repeat_enclosing_tags = $repeat;
720 if ($repeat_enclosing_tags) {
721 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
722 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
725 my %subletter = ( title => '', content => $subcontent );
726 _substitute_tables( \%subletter, $_ );
729 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
735 _substitute_tables( $letter, $tables );
738 if ($repeat_no_enclosing_tags) {
739 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
744 $c =~ s/<<count>>/$i/go;
745 foreach my $field ( keys %{$_} ) {
746 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
750 } @$repeat_no_enclosing_tags;
752 my $replaceby = join( "\n", @lines );
753 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
757 $letter->{content} = _process_tt(
759 content => $letter->{content},
762 substitute => $substitute,
766 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
771 sub _substitute_tables {
772 my ( $letter, $tables ) = @_;
773 while ( my ($table, $param) = each %$tables ) {
776 my $ref = ref $param;
779 if ($ref && $ref eq 'HASH') {
783 my $sth = _parseletter_sth($table);
785 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
788 $sth->execute( $ref ? @$param : $param );
790 $values = $sth->fetchrow_hashref;
794 _parseletter ( $letter, $table, $values );
798 sub _parseletter_sth {
802 carp "ERROR: _parseletter_sth() called without argument (table)";
805 # NOTE: we used to check whether we had a statement handle cached in
806 # a %handles module-level variable. This was a dumb move and
807 # broke things for the rest of us. prepare_cached is a better
808 # way to cache statement handles anyway.
810 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
811 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
812 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
813 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
814 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE itemnumber = ? ORDER BY timestamp DESC LIMIT 1" :
815 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
816 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
817 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
818 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
819 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
820 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
821 ($table eq 'opac_news' ) ? "SELECT * FROM $table WHERE idnew = ?" :
822 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
823 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
824 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
825 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
828 warn "ERROR: No _parseletter_sth query for table '$table'";
829 return; # nothing to get
831 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
832 warn "ERROR: Failed to prepare query: '$query'";
835 return $sth; # now cache is populated for that $table
838 =head2 _parseletter($letter, $table, $values)
841 - $letter : a hash to letter fields (title & content useful)
842 - $table : the Koha table to parse.
843 - $values_in : table record hashref
844 parse all fields from a table, and replace values in title & content with the appropriate value
845 (not exported sub, used only internally)
850 my ( $letter, $table, $values_in ) = @_;
852 # Work on a local copy of $values_in (passed by reference) to avoid side effects
853 # in callers ( by changing / formatting values )
854 my $values = $values_in ? { %$values_in } : {};
856 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
857 $values->{'dateexpiry'} = output_pref({ str => $values->{dateexpiry}, dateonly => 1 });
860 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
861 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
864 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
865 my $todaysdate = output_pref( DateTime->now() );
866 $letter->{content} =~ s/<<today>>/$todaysdate/go;
869 while ( my ($field, $val) = each %$values ) {
870 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
871 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
872 #Therefore adding the test on biblio. This includes biblioitems,
873 #but excludes items. Removed unneeded global and lookahead.
875 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
876 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
877 $val = $av->count ? $av->next->lib : '';
881 my $replacedby = defined ($val) ? $val : '';
883 and not $replacedby =~ m|0000-00-00|
884 and not $replacedby =~ m|9999-12-31|
885 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
887 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
888 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
889 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
891 for my $letter_field ( qw( title content ) ) {
892 my $filter_string_used = q{};
893 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
894 # We overwrite $dateonly if the filter exists and we have a time in the datetime
895 $filter_string_used = $1 || q{};
896 $dateonly = $1 unless $dateonly;
898 my $replacedby_date = eval {
899 output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
902 if ( $letter->{ $letter_field } ) {
903 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
904 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
908 # Other fields replacement
910 for my $letter_field ( qw( title content ) ) {
911 if ( $letter->{ $letter_field } ) {
912 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
913 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
919 if ($table eq 'borrowers' && $letter->{content}) {
920 if ( my $attributes = GetBorrowerAttributes($values->{borrowernumber}) ) {
922 foreach (@$attributes) {
923 my $code = $_->{code};
924 my $val = $_->{value_description} || $_->{value};
925 $val =~ s/\p{P}(?=$)//g if $val;
926 next unless $val gt '';
928 push @{ $attr{$code} }, $val;
930 while ( my ($code, $val_ar) = each %attr ) {
931 my $replacefield = "<<borrower-attribute:$code>>";
932 my $replacedby = join ',', @$val_ar;
933 $letter->{content} =~ s/$replacefield/$replacedby/g;
942 my $success = EnqueueLetter( { letter => $letter,
943 borrowernumber => '12', message_transport_type => 'email' } )
945 places a letter in the message_queue database table, which will
946 eventually get processed (sent) by the process_message_queue.pl
947 cronjob when it calls SendQueuedMessages.
949 return message_id on success
954 my $params = shift or return;
956 return unless exists $params->{'letter'};
957 # return unless exists $params->{'borrowernumber'};
958 return unless exists $params->{'message_transport_type'};
960 my $content = $params->{letter}->{content};
961 $content =~ s/\s+//g if(defined $content);
962 if ( not defined $content or $content eq '' ) {
963 warn "Trying to add an empty message to the message queue" if $debug;
967 # If we have any attachments we should encode then into the body.
968 if ( $params->{'attachments'} ) {
969 $params->{'letter'} = _add_attachments(
970 { letter => $params->{'letter'},
971 attachments => $params->{'attachments'},
972 message => MIME::Lite->new( Type => 'multipart/mixed' ),
977 my $dbh = C4::Context->dbh();
978 my $statement = << 'ENDSQL';
979 INSERT INTO message_queue
980 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type )
982 ( ?, ?, ?, ?, ?, ?, ?, NOW(), ?, ?, ? )
985 my $sth = $dbh->prepare($statement);
986 my $result = $sth->execute(
987 $params->{'borrowernumber'}, # borrowernumber
988 $params->{'letter'}->{'title'}, # subject
989 $params->{'letter'}->{'content'}, # content
990 $params->{'letter'}->{'metadata'} || '', # metadata
991 $params->{'letter'}->{'code'} || '', # letter_code
992 $params->{'message_transport_type'}, # message_transport_type
994 $params->{'to_address'}, # to_address
995 $params->{'from_address'}, # from_address
996 $params->{'letter'}->{'content-type'}, # content_type
998 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
1001 =head2 SendQueuedMessages ([$hashref])
1003 my $sent = SendQueuedMessages({
1004 letter_code => $letter_code,
1005 borrowernumber => $who_letter_is_for,
1011 Sends all of the 'pending' items in the message queue, unless
1012 parameters are passed.
1014 The letter_code, borrowernumber and limit parameters are used
1015 to build a parameter set for _get_unsent_messages, thus limiting
1016 which pending messages will be processed. They are all optional.
1018 The verbose parameter can be used to generate debugging output.
1019 It is also optional.
1021 Returns number of messages sent.
1025 sub SendQueuedMessages {
1028 my $which_unsent_messages = {
1029 'limit' => $params->{'limit'} // 0,
1030 'borrowernumber' => $params->{'borrowernumber'} // q{},
1031 'letter_code' => $params->{'letter_code'} // q{},
1032 'type' => $params->{'type'} // q{},
1034 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
1035 MESSAGE: foreach my $message ( @$unsent_messages ) {
1036 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
1037 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
1038 $message_object->make_column_dirty('status');
1039 return unless $message_object->store;
1041 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
1042 warn sprintf( 'sending %s message to patron: %s',
1043 $message->{'message_transport_type'},
1044 $message->{'borrowernumber'} || 'Admin' )
1045 if $params->{'verbose'} or $debug;
1046 # This is just begging for subclassing
1047 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
1048 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
1049 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1051 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1052 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1053 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1054 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1055 unless ( $sms_provider ) {
1056 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1057 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1060 unless ( $patron->smsalertnumber ) {
1061 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1062 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1065 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1066 $message->{to_address} .= '@' . $sms_provider->domain();
1067 _update_message_to_address($message->{'message_id'},$message->{to_address});
1068 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1070 _send_message_by_sms( $message );
1074 return scalar( @$unsent_messages );
1077 =head2 GetRSSMessages
1079 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1081 returns a listref of all queued RSS messages for a particular person.
1085 sub GetRSSMessages {
1088 return unless $params;
1089 return unless ref $params;
1090 return unless $params->{'borrowernumber'};
1092 return _get_unsent_messages( { message_transport_type => 'rss',
1093 limit => $params->{'limit'},
1094 borrowernumber => $params->{'borrowernumber'}, } );
1097 =head2 GetPrintMessages
1099 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1101 Returns a arrayref of all queued print messages (optionally, for a particular
1106 sub GetPrintMessages {
1107 my $params = shift || {};
1109 return _get_unsent_messages( { message_transport_type => 'print',
1110 borrowernumber => $params->{'borrowernumber'},
1114 =head2 GetQueuedMessages ([$hashref])
1116 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1118 fetches messages out of the message queue.
1121 list of hashes, each has represents a message in the message queue.
1125 sub GetQueuedMessages {
1128 my $dbh = C4::Context->dbh();
1129 my $statement = << 'ENDSQL';
1130 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued
1136 if ( exists $params->{'borrowernumber'} ) {
1137 push @whereclauses, ' borrowernumber = ? ';
1138 push @query_params, $params->{'borrowernumber'};
1141 if ( @whereclauses ) {
1142 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1145 if ( defined $params->{'limit'} ) {
1146 $statement .= ' LIMIT ? ';
1147 push @query_params, $params->{'limit'};
1150 my $sth = $dbh->prepare( $statement );
1151 my $result = $sth->execute( @query_params );
1152 return $sth->fetchall_arrayref({});
1155 =head2 GetMessageTransportTypes
1157 my @mtt = GetMessageTransportTypes();
1159 returns an arrayref of transport types
1163 sub GetMessageTransportTypes {
1164 my $dbh = C4::Context->dbh();
1165 my $mtts = $dbh->selectcol_arrayref("
1166 SELECT message_transport_type
1167 FROM message_transport_types
1168 ORDER BY message_transport_type
1175 my $message = C4::Letters::Message($message_id);
1180 my ( $message_id ) = @_;
1181 return unless $message_id;
1182 my $dbh = C4::Context->dbh;
1183 return $dbh->selectrow_hashref(q|
1184 SELECT message_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type
1186 WHERE message_id = ?
1187 |, {}, $message_id );
1190 =head2 ResendMessage
1192 Attempt to resend a message which has failed previously.
1194 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1196 Updates the message to 'pending' status so that
1197 it will be resent later on.
1199 returns 1 on success, 0 on failure, undef if no message was found
1204 my $message_id = shift;
1205 return unless $message_id;
1207 my $message = GetMessage( $message_id );
1208 return unless $message;
1210 if ( $message->{status} ne 'pending' ) {
1211 $rv = C4::Letters::_set_message_status({
1212 message_id => $message_id,
1213 status => 'pending',
1215 $rv = $rv > 0? 1: 0;
1216 # Clear destination email address to force address update
1217 _update_message_to_address( $message_id, undef ) if $rv &&
1218 $message->{message_transport_type} eq 'email';
1223 =head2 _add_attachements
1226 letter - the standard letter hashref
1227 attachments - listref of attachments. each attachment is a hashref of:
1228 type - the mime type, like 'text/plain'
1229 content - the actual attachment
1230 filename - the name of the attachment.
1231 message - a MIME::Lite object to attach these to.
1233 returns your letter object, with the content updated.
1237 sub _add_attachments {
1240 my $letter = $params->{'letter'};
1241 my $attachments = $params->{'attachments'};
1242 return $letter unless @$attachments;
1243 my $message = $params->{'message'};
1245 # First, we have to put the body in as the first attachment
1247 Type => $letter->{'content-type'} || 'TEXT',
1248 Data => $letter->{'is_html'}
1249 ? _wrap_html($letter->{'content'}, $letter->{'title'})
1250 : $letter->{'content'},
1253 foreach my $attachment ( @$attachments ) {
1255 Type => $attachment->{'type'},
1256 Data => $attachment->{'content'},
1257 Filename => $attachment->{'filename'},
1260 # we're forcing list context here to get the header, not the count back from grep.
1261 ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1262 $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1263 $letter->{'content'} = $message->body_as_string;
1269 =head2 _get_unsent_messages
1271 This function's parameter hash reference takes the following
1272 optional named parameters:
1273 message_transport_type: method of message sending (e.g. email, sms, etc.)
1274 borrowernumber : who the message is to be sent
1275 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1276 limit : maximum number of messages to send
1278 This function returns an array of matching hash referenced rows from
1279 message_queue with some borrower information added.
1283 sub _get_unsent_messages {
1286 my $dbh = C4::Context->dbh();
1288 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
1289 FROM message_queue mq
1290 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1294 my @query_params = ('pending');
1295 if ( ref $params ) {
1296 if ( $params->{'message_transport_type'} ) {
1297 $statement .= ' AND mq.message_transport_type = ? ';
1298 push @query_params, $params->{'message_transport_type'};
1300 if ( $params->{'borrowernumber'} ) {
1301 $statement .= ' AND mq.borrowernumber = ? ';
1302 push @query_params, $params->{'borrowernumber'};
1304 if ( $params->{'letter_code'} ) {
1305 $statement .= ' AND mq.letter_code = ? ';
1306 push @query_params, $params->{'letter_code'};
1308 if ( $params->{'type'} ) {
1309 $statement .= ' AND message_transport_type = ? ';
1310 push @query_params, $params->{'type'};
1312 if ( $params->{'limit'} ) {
1313 $statement .= ' limit ? ';
1314 push @query_params, $params->{'limit'};
1318 $debug and warn "_get_unsent_messages SQL: $statement";
1319 $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1320 my $sth = $dbh->prepare( $statement );
1321 my $result = $sth->execute( @query_params );
1322 return $sth->fetchall_arrayref({});
1325 sub _send_message_by_email {
1326 my $message = shift or return;
1327 my ($username, $password, $method) = @_;
1329 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1330 my $to_address = $message->{'to_address'};
1331 unless ($to_address) {
1333 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1334 _set_message_status( { message_id => $message->{'message_id'},
1335 status => 'failed' } );
1338 $to_address = $patron->notice_email_address;
1339 unless ($to_address) {
1340 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1341 # warning too verbose for this more common case?
1342 _set_message_status( { message_id => $message->{'message_id'},
1343 status => 'failed' } );
1348 my $utf8 = decode('MIME-Header', $message->{'subject'} );
1349 $message->{subject}= encode('MIME-Header', $utf8);
1350 my $subject = encode('UTF-8', $message->{'subject'});
1351 my $content = encode('UTF-8', $message->{'content'});
1352 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1353 my $is_html = $content_type =~ m/html/io;
1354 my $branch_email = undef;
1355 my $branch_replyto = undef;
1356 my $branch_returnpath = undef;
1358 my $library = $patron->library;
1359 $branch_email = $library->branchemail;
1360 $branch_replyto = $library->branchreplyto;
1361 $branch_returnpath = $library->branchreturnpath;
1363 my $email = Koha::Email->new();
1364 my %sendmail_params = $email->create_message_headers(
1367 from => $message->{'from_address'} || $branch_email,
1368 replyto => $branch_replyto,
1369 sender => $branch_returnpath,
1370 subject => $subject,
1371 message => $is_html ? _wrap_html( $content, $subject ) : $content,
1372 contenttype => $content_type
1376 $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
1377 if ( my $bcc = C4::Context->preference('NoticeBcc') ) {
1378 $sendmail_params{ Bcc } = $bcc;
1381 _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
1383 if ( Mail::Sendmail::sendmail( %sendmail_params ) ) {
1384 _set_message_status( { message_id => $message->{'message_id'},
1385 status => 'sent' } );
1388 _set_message_status( { message_id => $message->{'message_id'},
1389 status => 'failed' } );
1390 carp $Mail::Sendmail::error;
1396 my ($content, $title) = @_;
1398 my $css = C4::Context->preference("NoticeCSS") || '';
1399 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1401 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1402 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1403 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1405 <title>$title</title>
1406 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1417 my ( $message ) = @_;
1418 my $dbh = C4::Context->dbh;
1419 my $count = $dbh->selectrow_array(q|
1422 WHERE message_transport_type = ?
1423 AND borrowernumber = ?
1425 AND CAST(time_queued AS date) = CAST(NOW() AS date)
1428 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1432 sub _send_message_by_sms {
1433 my $message = shift or return;
1434 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1436 unless ( $patron and $patron->smsalertnumber ) {
1437 _set_message_status( { message_id => $message->{'message_id'},
1438 status => 'failed' } );
1442 if ( _is_duplicate( $message ) ) {
1443 _set_message_status( { message_id => $message->{'message_id'},
1444 status => 'failed' } );
1448 my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
1449 message => $message->{'content'},
1451 _set_message_status( { message_id => $message->{'message_id'},
1452 status => ($success ? 'sent' : 'failed') } );
1456 sub _update_message_to_address {
1458 my $dbh = C4::Context->dbh();
1459 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1462 sub _set_message_status {
1463 my $params = shift or return;
1465 foreach my $required_parameter ( qw( message_id status ) ) {
1466 return unless exists $params->{ $required_parameter };
1469 my $dbh = C4::Context->dbh();
1470 my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1471 my $sth = $dbh->prepare( $statement );
1472 my $result = $sth->execute( $params->{'status'},
1473 $params->{'message_id'} );
1478 my ( $params ) = @_;
1480 my $content = $params->{content};
1481 my $tables = $params->{tables};
1482 my $loops = $params->{loops};
1483 my $substitute = $params->{substitute} || {};
1485 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1486 my $template = Template->new(
1490 PLUGIN_BASE => 'Koha::Template::Plugin',
1491 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1492 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1494 ENCODING => 'UTF-8',
1496 ) or die Template->error();
1498 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1500 $content = add_tt_filters( $content );
1501 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1504 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1509 sub _get_tt_params {
1510 my ($tables, $is_a_loop) = @_;
1516 article_requests => {
1517 module => 'Koha::ArticleRequests',
1518 singular => 'article_request',
1519 plural => 'article_requests',
1523 module => 'Koha::Biblios',
1524 singular => 'biblio',
1525 plural => 'biblios',
1526 pk => 'biblionumber',
1529 module => 'Koha::Biblioitems',
1530 singular => 'biblioitem',
1531 plural => 'biblioitems',
1532 pk => 'biblioitemnumber',
1535 module => 'Koha::Patrons',
1536 singular => 'borrower',
1537 plural => 'borrowers',
1538 pk => 'borrowernumber',
1541 module => 'Koha::Libraries',
1542 singular => 'branch',
1543 plural => 'branches',
1547 module => 'Koha::Items',
1553 module => 'Koha::News',
1559 module => 'Koha::Acquisition::Orders',
1560 singular => 'order',
1562 pk => 'ordernumber',
1565 module => 'Koha::Holds',
1568 fk => [ 'borrowernumber', 'biblionumber' ],
1571 module => 'Koha::Serials',
1572 singular => 'serial',
1573 plural => 'serials',
1577 module => 'Koha::Subscriptions',
1578 singular => 'subscription',
1579 plural => 'subscriptions',
1580 pk => 'subscriptionid',
1583 module => 'Koha::Suggestions',
1584 singular => 'suggestion',
1585 plural => 'suggestions',
1586 pk => 'suggestionid',
1589 module => 'Koha::Checkouts',
1590 singular => 'checkout',
1591 plural => 'checkouts',
1595 module => 'Koha::Old::Checkouts',
1596 singular => 'old_checkout',
1597 plural => 'old_checkouts',
1601 module => 'Koha::Checkouts',
1602 singular => 'overdue',
1603 plural => 'overdues',
1606 borrower_modifications => {
1607 module => 'Koha::Patron::Modifications',
1608 singular => 'patron_modification',
1609 plural => 'patron_modifications',
1610 fk => 'verification_token',
1614 foreach my $table ( keys %$tables ) {
1615 next unless $config->{$table};
1617 my $ref = ref( $tables->{$table} ) || q{};
1618 my $module = $config->{$table}->{module};
1620 if ( can_load( modules => { $module => undef } ) ) {
1621 my $pk = $config->{$table}->{pk};
1622 my $fk = $config->{$table}->{fk};
1625 my $values = $tables->{$table} || [];
1626 unless ( ref( $values ) eq 'ARRAY' ) {
1627 croak "ERROR processing table $table. Wrong API call.";
1629 my $key = $pk ? $pk : $fk;
1630 # $key does not come from user input
1631 my $objects = $module->search(
1632 { $key => $values },
1634 # We want to retrieve the data in the same order
1636 # field is a MySQLism, but they are no other way to do it
1637 # To be generic we could do it in perl, but we will need to fetch
1638 # all the data then order them
1639 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1642 $params->{ $config->{$table}->{plural} } = $objects;
1644 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1645 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1647 if ( $fk ) { # Using a foreign key for lookup
1648 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1650 foreach my $key ( @$fk ) {
1651 $search->{$key} = $id->{$key};
1653 $object = $module->search( $search )->last();
1654 } else { # Foreign key is single column
1655 $object = $module->search( { $fk => $id } )->last();
1657 } else { # using the table's primary key for lookup
1658 $object = $module->find($id);
1660 $params->{ $config->{$table}->{singular} } = $object;
1662 else { # $ref eq 'ARRAY'
1664 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1665 $object = $module->search( { $pk => $tables->{$table} } )->last();
1667 else { # Params are mutliple foreign keys
1668 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1670 $params->{ $config->{$table}->{singular} } = $object;
1674 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1678 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1683 =head3 add_tt_filters
1685 $content = add_tt_filters( $content );
1687 Add TT filters to some specific fields if needed.
1689 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1693 sub add_tt_filters {
1694 my ( $content ) = @_;
1695 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1696 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1700 =head2 get_item_content
1702 my $item = Koha::Items->find(...)->unblessed;
1703 my @item_content_fields = qw( date_due title barcode author itemnumber );
1704 my $item_content = C4::Letters::get_item_content({
1706 item_content_fields => \@item_content_fields
1709 This function generates a tab-separated list of values for the passed item. Dates
1710 are formatted following the current setup.
1714 sub get_item_content {
1715 my ( $params ) = @_;
1716 my $item = $params->{item};
1717 my $dateonly = $params->{dateonly} || 0;
1718 my $item_content_fields = $params->{item_content_fields} || [];
1720 return unless $item;
1722 my @item_info = map {
1726 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1730 } @$item_content_fields;
1731 return join( "\t", @item_info ) . "\n";