empty From address in default message templates, #15448
[freeside.git] / FS / FS / msg_template.pm
index 4a1e345..1c458a0 100644 (file)
@@ -6,9 +6,11 @@ use Text::Template;
 use FS::Misc qw( generate_email send_email );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs );
 use FS::Misc qw( generate_email send_email );
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs );
+use FS::UID qw( dbh );
 
 use FS::cust_main;
 use FS::cust_msg;
 
 use FS::cust_main;
 use FS::cust_msg;
+use FS::template_content;
 
 use Date::Format qw( time2str );
 use HTML::Entities qw( decode_entities encode_entities ) ;
 
 use Date::Format qw( time2str );
 use HTML::Entities qw( decode_entities encode_entities ) ;
@@ -47,37 +49,19 @@ supported:
 
 =over 4
 
 
 =over 4
 
-=item msgnum
+=item msgnum - primary key
 
 
-primary key
+=item msgname - Name of the template.  This will appear in the user interface;
+if it needs to be localized for some users, add it to the message catalog.
 
 
-=item msgname
+=item agentnum - Agent associated with this template.  Can be NULL for a 
+global template.
 
 
-Template name.
+=item mime_type - MIME type.  Defaults to text/html.
 
 
-=item agentnum
+=item from_addr - Source email address.
 
 
-Agent associated with this template.  Can be NULL for a global template.
-
-=item mime_type
-
-MIME type.  Defaults to text/html.
-
-=item from_addr
-
-Source email address.
-
-=item subject
-
-The message subject line, in L<Text::Template> format.
-
-=item body
-
-The message body, as plain text or HTML, in L<Text::Template> format.
-
-=item disabled
-
-disabled
+=item disabled - disabled ('Y' or NULL).
 
 =back
 
 
 =back
 
@@ -98,14 +82,41 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 sub table { 'msg_template'; }
 
 
 sub table { 'msg_template'; }
 
-=item insert
+=item insert [ CONTENT ]
 
 Adds this record to the database.  If there is an error, returns the error,
 otherwise returns false.
 
 
 Adds this record to the database.  If there is an error, returns the error,
 otherwise returns false.
 
+A default (no locale) L<FS::template_content> object will be created.  CONTENT 
+is an optional hash containing 'subject' and 'body' for this object.
+
 =cut
 
 =cut
 
-# the insert method can be inherited from FS::Record
+sub insert {
+  my $self = shift;
+  my %content = @_;
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+  if ( !$error ) {
+    $content{'msgnum'} = $self->msgnum;
+    $content{'subject'} ||= '';
+    $content{'body'} ||= '';
+    my $template_content = new FS::template_content (\%content);
+    $error = $template_content->insert;
+  }
+
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit if $oldAutoCommit;
+  return;
+}
 
 =item delete
 
 
 =item delete
 
@@ -115,14 +126,59 @@ Delete this record from the database.
 
 # the delete method can be inherited from FS::Record
 
 
 # the delete method can be inherited from FS::Record
 
-=item replace OLD_RECORD
+=item replace [ OLD_RECORD ] [ CONTENT ]
 
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
 
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
+CONTENT is an optional hash containing 'subject', 'body', and 'locale'.  If 
+supplied, an L<FS::template_content> object will be created (or modified, if 
+one already exists for this locale).
+
 =cut
 
 =cut
 
-# the replace method can be inherited from FS::Record
+sub replace {
+  my $self = shift;
+  my $old = ( ref($_[0]) and $_[0]->isa('FS::Record') ) 
+              ? shift
+              : $self->replace_old;
+  my %content = @_;
+  
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::replace($old);
+
+  if ( !$error and %content ) {
+    $content{'locale'} ||= '';
+    my $new_content = qsearchs('template_content', {
+                        'msgnum' => $self->msgnum,
+                        'locale' => $content{'locale'},
+                      } );
+    if ( $new_content ) {
+      $new_content->subject($content{'subject'});
+      $new_content->body($content{'body'});
+      $error = $new_content->replace;
+    }
+    else {
+      $content{'msgnum'} = $self->msgnum;
+      $new_content = new FS::template_content \%content;
+      $error = $new_content->insert;
+    }
+  }
+
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  warn "committing FS::msg_template->replace\n" if $DEBUG and $oldAutoCommit;
+  $dbh->commit if $oldAutoCommit;
+  return;
+}
+    
+
 
 =item check
 
 
 =item check
 
@@ -143,8 +199,6 @@ sub check {
     || $self->ut_text('msgname')
     || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
     || $self->ut_textn('mime_type')
     || $self->ut_text('msgname')
     || $self->ut_foreign_keyn('agentnum', 'agent', 'agentnum')
     || $self->ut_textn('mime_type')
-    || $self->ut_anything('subject')
-    || $self->ut_anything('body')
     || $self->ut_enum('disabled', [ '', 'Y' ] )
     || $self->ut_textn('from_addr')
   ;
     || $self->ut_enum('disabled', [ '', 'Y' ] )
     || $self->ut_textn('from_addr')
   ;
@@ -155,6 +209,21 @@ sub check {
   $self->SUPER::check;
 }
 
   $self->SUPER::check;
 }
 
+=item content_locales
+
+Returns a hashref of the L<FS::template_content> objects attached to 
+this template, with the locale as key.
+
+=cut
+
+sub content_locales {
+  my $self = shift;
+  return $self->{'_content_locales'} ||= +{
+    map { $_->locale , $_ } 
+    qsearch('template_content', { 'msgnum' => $self->msgnum })
+  };
+}
+
 =item prepare OPTION => VALUE
 
 Fills in the template and returns a hash of the 'from' address, 'to' 
 =item prepare OPTION => VALUE
 
 Fills in the template and returns a hash of the 'from' address, 'to' 
@@ -193,10 +262,9 @@ The I<from_addr> field in the template takes precedence over this.
 Destination address.  The default is to use the customer's 
 invoicing_list addresses.  Multiple addresses may be comma-separated.
 
 Destination address.  The default is to use the customer's 
 invoicing_list addresses.  Multiple addresses may be comma-separated.
 
-=item preview
+=item substitutions
 
 
-Set to true when preparing a message for previewing, rather than to actually 
-send it.  This turns off logging.
+A hash reference of additional substitutions
 
 =back
 
 
 =back
 
@@ -207,6 +275,12 @@ sub prepare {
 
   my $cust_main = $opt{'cust_main'};
   my $object = $opt{'object'};
 
   my $cust_main = $opt{'cust_main'};
   my $object = $opt{'object'};
+
+  # localization
+  my $locale = $cust_main->locale || '';
+  warn "no locale for cust#".$cust_main->custnum."; using default content\n"
+    if $DEBUG and !$locale;
+  my $content = $self->content($cust_main->locale);
   warn "preparing template '".$self->msgname."' to cust#".$cust_main->custnum."\n"
     if($DEBUG);
 
   warn "preparing template '".$self->msgname."' to cust#".$cust_main->custnum."\n"
     if($DEBUG);
 
@@ -254,19 +328,23 @@ sub prepare {
       } 
     } 
   } 
       } 
     } 
   } 
-  $_ = encode_entities($_) foreach values(%hash);
 
 
+  if ( $opt{substitutions} ) {
+    $hash{$_} = $opt{substitutions}->{$_} foreach keys %{$opt{substitutions}};
+  }
+
+  $_ = encode_entities($_ || '') foreach values(%hash);
 
   ###
   # clean up template
   ###
   my $subject_tmpl = new Text::Template (
     TYPE   => 'STRING',
 
   ###
   # clean up template
   ###
   my $subject_tmpl = new Text::Template (
     TYPE   => 'STRING',
-    SOURCE => $self->subject,
+    SOURCE => $content->subject,
   );
   my $subject = $subject_tmpl->fill_in( HASH => \%hash );
 
   );
   my $subject = $subject_tmpl->fill_in( HASH => \%hash );
 
-  my $body = $self->body;
+  my $body = $content->body;
   my ($skin, $guts) = eviscerate($body);
   @$guts = map { 
     $_ = decode_entities($_); # turn all punctuation back into itself
   my ($skin, $guts) = eviscerate($body);
   @$guts = map { 
     $_ = decode_entities($_); # turn all punctuation back into itself
@@ -318,16 +396,16 @@ sub prepare {
     $from_addr ||= scalar( $conf->config('invoice_from',
                                          $cust_main->agentnum) );
   }
     $from_addr ||= scalar( $conf->config('invoice_from',
                                          $cust_main->agentnum) );
   }
-  my @cust_msg = ();
-  if ( $conf->exists('log_sent_mail') and !$opt{'preview'} ) {
-    my $cust_msg = FS::cust_msg->new({
-        'custnum' => $cust_main->custnum,
-        'msgnum'  => $self->msgnum,
-        'status'  => 'prepared',
-      });
-    $cust_msg->insert;
-    @cust_msg = ('cust_msg' => $cust_msg);
-  }
+#  my @cust_msg = ();
+#  if ( $conf->exists('log_sent_mail') and !$opt{'preview'} ) {
+#    my $cust_msg = FS::cust_msg->new({
+#        'custnum' => $cust_main->custnum,
+#        'msgnum'  => $self->msgnum,
+#        'status'  => 'prepared',
+#      });
+#    $cust_msg->insert;
+#    @cust_msg = ('cust_msg' => $cust_msg);
+#  }
 
   (
     'custnum' => $cust_main->custnum,
 
   (
     'custnum' => $cust_main->custnum,
@@ -339,7 +417,6 @@ sub prepare {
     'html_body' => $body,
     'text_body' => HTML::FormatText->new(leftmargin => 0, rightmargin => 70
                     )->format( HTML::TreeBuilder->new_from_content($body) ),
     'html_body' => $body,
     'text_body' => HTML::FormatText->new(leftmargin => 0, rightmargin => 70
                     )->format( HTML::TreeBuilder->new_from_content($body) ),
-    @cust_msg,
   );
 
 }
   );
 
 }
@@ -392,12 +469,11 @@ sub substitutions {
       cust_status ucfirst_cust_status cust_statuscolor
 
       signupdate dundate
       cust_status ucfirst_cust_status cust_statuscolor
 
       signupdate dundate
-      expdate
       packages recurdates
       ),
       packages recurdates
       ),
-      # expdate is a special case
-      [ signupdate_ymd    => sub { time2str('%Y-%m-%d', shift->signupdate) } ],
-      [ dundate_ymd       => sub { time2str('%Y-%m-%d', shift->dundate) } ],
+      [ expdate           => sub { shift->paydate_epoch } ], #compatibility
+      [ signupdate_ymd    => sub { $ymd->(shift->signupdate) } ],
+      [ dundate_ymd       => sub { $ymd->(shift->dundate) } ],
       [ paydate_my        => sub { sprintf('%02d/%04d', shift->paydate_monthyear) } ],
       [ otaker_first      => sub { shift->access_user->first } ],
       [ otaker_last       => sub { shift->access_user->last } ],
       [ paydate_my        => sub { sprintf('%02d/%04d', shift->paydate_monthyear) } ],
       [ otaker_first      => sub { shift->access_user->first } ],
       [ otaker_last       => sub { shift->access_user->last } ],
@@ -512,6 +588,32 @@ sub substitutions {
   };
 }
 
   };
 }
 
+=item content LOCALE
+
+Returns the L<FS::template_content> object appropriate to LOCALE, if there 
+is one.  If not, returns the one with a NULL locale.
+
+=cut
+
+sub content {
+  my $self = shift;
+  my $locale = shift;
+  qsearchs('template_content', 
+            { 'msgnum' => $self->msgnum, 'locale' => $locale }) || 
+  qsearchs('template_content',
+            { 'msgnum' => $self->msgnum, 'locale' => '' });
+}
+
+=item agent
+
+Returns the L<FS::agent> object for this template.
+
+=cut
+
+sub agent {
+  qsearchs('agent', { 'agentnum' => $_[0]->agentnum });
+}
+
 sub _upgrade_data {
   my ($self, %opts) = @_;
 
 sub _upgrade_data {
   my ($self, %opts) = @_;
 
@@ -531,14 +633,13 @@ sub _upgrade_data {
       my ($newname, $oldname, $subject, $from, $bcc) = @$_;
       if ($conf->exists($oldname, $agentnum)) {
         my $new = new FS::msg_template({
       my ($newname, $oldname, $subject, $from, $bcc) = @$_;
       if ($conf->exists($oldname, $agentnum)) {
         my $new = new FS::msg_template({
-           'msgname'   => $oldname,
-           'agentnum'  => $agentnum,
-           'from_addr' => ($from && $conf->config($from, $agentnum)) || 
-                          $conf->config('invoice_from', $agentnum),
-           'bcc_addr'  => ($bcc && $conf->config($from, $agentnum)) || '',
-           'subject'   => ($subject && $conf->config($subject, $agentnum)) || '',
-           'mime_type' => 'text/html',
-           'body'      => join('<BR>',$conf->config($oldname, $agentnum)),
+          'msgname'   => $oldname,
+          'agentnum'  => $agentnum,
+          'from_addr' => ($from && $conf->config($from, $agentnum)) || '',
+          'bcc_addr'  => ($bcc && $conf->config($from, $agentnum)) || '',
+          'subject'   => ($subject && $conf->config($subject, $agentnum)) || '',
+          'mime_type' => 'text/html',
+          'body'      => join('<BR>',$conf->config($oldname, $agentnum)),
         });
         my $error = $new->insert;
         die $error if $error;
         });
         my $error = $new->insert;
         die $error if $error;
@@ -549,6 +650,19 @@ sub _upgrade_data {
       }
     }
   }
       }
     }
   }
+  foreach my $msg_template ( qsearch('msg_template', {}) ) {
+    if ( $msg_template->subject || $msg_template->body ) {
+      # create new default content
+      my %content;
+      foreach ('subject','body') {
+        $content{$_} = $msg_template->$_;
+        $msg_template->setfield($_, '');
+      }
+
+      my $error = $msg_template->replace(%content);
+      die $error if $error;
+    }
+  }
 }
 
 sub eviscerate {
 }
 
 sub eviscerate {