finish adding a feature to easily list all email addresses for an agent & send them...
authorivan <ivan>
Tue, 17 Jun 2008 03:35:56 +0000 (03:35 +0000)
committerivan <ivan>
Tue, 17 Jun 2008 03:35:56 +0000 (03:35 +0000)
FS/FS/AccessRight.pm
FS/FS/ConfDefaults.pm
FS/FS/Misc.pm
FS/FS/cust_main.pm
htetc/handler.pl
httemplate/edit/invoice_template.html
httemplate/elements/htmlarea.html [new file with mode: 0644]
httemplate/misc/email-customers.html [new file with mode: 0644]
httemplate/misc/process/email-customers.html [new file with mode: 0644]
httemplate/search/cust_main.html

index 13dbd7f..5621a97 100644 (file)
@@ -100,6 +100,7 @@ tie my %rights, 'Tie::IxHash',
     'Add customer note', #NEW
     'Edit customer note', #NEW
     'Bill customer now', #NEW
+    'Bulk send customer notices', #NEW
   ],
   
   ###
@@ -193,7 +194,7 @@ tie my %rights, 'Tie::IxHash',
   ###
   # report/listing rights...
   ###
-  'Reprting/listing rights' => [
+  'Reporting/listing rights' => [
     'List customers',
     'List zip codes', #NEW
     'List invoices',
index 7978259..a3af52d 100644 (file)
@@ -56,6 +56,9 @@ sub cust_fields_avail { (
   'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Fax number | Invoicing email(s) | Payment Type | Current Balance' => 
     'custnum | Status | Last, First | Company | (all address fields ) | ( all phones ) | Invoicing email(s) | Payment Type | Current Balance',
 
+  'Invoicing email(s)' => 'Invoicing email(s)',
+  'Cust# | Invoicing email(s)' => 'custnum | Invoicing email(s)',
+
 ); }
 
 =back
index 1f6eece..936f94a 100644 (file)
@@ -12,7 +12,7 @@ use IPC::Run3; # for do_print... should just use IPC::Run i guess
 #instead
 
 @ISA = qw( Exporter );
-@EXPORT_OK = qw( send_email send_fax
+@EXPORT_OK = qw( generate_email send_email send_fax
                  states_hash counties state_label
                  card_types
                  generate_ps generate_pdf do_print
@@ -40,29 +40,173 @@ but are collected here to elimiate code duplication.
 
 =over 4
 
+=item generate_email OPTION => VALUE ...
+
+Options:
+
+=item from
+
+Sender address, required
+
+=item to
+
+Recipient address, required
+
+=item subject
+
+email subject, required
+
+=item html_body
+
+Email body (HTML alternative).  Arrayref of lines, or scalar.
+
+Will be placed inside an HTML <BODY> tag.
+
+=item text_body
+
+Email body (Text alternative).  Arrayref of lines, or scalar.
+
+=back
+
+Returns an argument list to be passsed to L<send_email>.
+
+=cut
+
+#false laziness w/FS::cust_bill::generate_email
+
+use MIME::Entity;
+use HTML::Entities;
+
+sub generate_email {
+  my %args = @_;
+
+  my $me = '[FS::Misc::generate_email]';
+
+  my %return = (
+    'from'    => $args{'from'},
+    'to'      => $args{'to'},
+    'subject' => $args{'subject'},
+  );
+
+  #if (ref($args{'to'}) eq 'ARRAY') {
+  #  $return{'to'} = $args{'to'};
+  #} else {
+  #  $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
+  #                         $self->cust_main->invoicing_list
+  #                  ];
+  #}
+
+  warn "$me creating HTML/text multipart message"
+    if $DEBUG;
+
+  $return{'nobody'} = 1;
+
+  my $alternative = build MIME::Entity
+    'Type'        => 'multipart/alternative',
+    'Encoding'    => '7bit',
+    'Disposition' => 'inline'
+  ;
+
+  my $data;
+  if ( ref($args{'text_body'}) eq 'ARRAY' ) {
+    $data = $args{'text_body'};
+  } else {
+    $data = [ split(/\n/, $args{'text_body'}) ];
+  }
+
+  $alternative->attach(
+    'Type'        => 'text/plain',
+    #'Encoding'    => 'quoted-printable',
+    'Encoding'    => '7bit',
+    'Data'        => $data,
+    'Disposition' => 'inline',
+  );
+
+  my @html_data;
+  if ( ref($args{'html_body'}) eq 'ARRAY' ) {
+    @html_data = @{ $args{'html_body'} };
+  } else {
+    @html_data = split(/\n/, $args{'html_body'});
+  }
+
+  $alternative->attach(
+    'Type'        => 'text/html',
+    'Encoding'    => 'quoted-printable',
+    'Data'        => [ '<html>',
+                       '  <head>',
+                       '    <title>',
+                       '      '. encode_entities($return{'subject'}), 
+                       '    </title>',
+                       '  </head>',
+                       '  <body bgcolor="#e8e8e8">',
+                       @html_data,
+                       '  </body>',
+                       '</html>',
+                     ],
+    'Disposition' => 'inline',
+    #'Filename'    => 'invoice.pdf',
+  );
+
+  #no other attachment:
+  # multipart/related
+  #   multipart/alternative
+  #     text/plain
+  #     text/html
+
+  $return{'content-type'} = 'multipart/related';
+  $return{'mimeparts'} = [ $alternative ];
+  $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
+  #$return{'disposition'} = 'inline';
+
+  %return;
+
+}
+
 =item send_email OPTION => VALUE ...
 
 Options:
 
-I<from> - (required)
+=over 4
+
+=item from
+
+(required)
+
+=item to
+
+(required) comma-separated scalar or arrayref of recipients
+
+=item subject
 
-I<to> - (required) comma-separated scalar or arrayref of recipients
+(required)
 
-I<subject> - (required)
+=item content-type
 
-I<content-type> - (optional) MIME type for the body
+(optional) MIME type for the body
 
-I<body> - (required unless I<nobody> is true) arrayref of body text lines
+=item body
 
-I<mimeparts> - (optional, but required if I<nobody> is true) arrayref of MIME::Entity->build PARAMHASH refs or MIME::Entity objects.  These will be passed as arguments to MIME::Entity->attach().
+(required unless I<nobody> is true) arrayref of body text lines
 
-I<nobody> - (optional) when set true, send_email will ignore the I<body> option and simply construct a message with the given I<mimeparts>.  In this case,
+=item mimeparts
+
+(optional, but required if I<nobody> is true) arrayref of MIME::Entity->build PARAMHASH refs or MIME::Entity objects.  These will be passed as arguments to MIME::Entity->attach().
+
+=item nobody
+
+(optional) when set true, send_email will ignore the I<body> option and simply construct a message with the given I<mimeparts>.  In this case,
 I<content-type>, if specified, overrides the default "multipart/mixed" for the outermost MIME container.
 
-I<content-encoding> - (optional) when using nobody, optional top-level MIME
+=item content-encoding
+
+(optional) when using nobody, optional top-level MIME
 encoding which, if specified, overrides the default "7bit".
 
-I<type> - (optional) type parameter for multipart/related messages
+=item type
+
+(optional) type parameter for multipart/related messages
+
+=back
 
 =cut
 
index 33788f3..946495c 100644 (file)
@@ -22,7 +22,7 @@ use Locale::Country;
 use Data::Dumper;
 use FS::UID qw( getotaker dbh driver_name );
 use FS::Record qw( qsearchs qsearch dbdef );
-use FS::Misc qw( send_email generate_ps do_print );
+use FS::Misc qw( generate_email send_email generate_ps do_print );
 use FS::Msgcat qw(gettext);
 use FS::cust_pkg;
 use FS::cust_svc;
@@ -5470,6 +5470,122 @@ sub search_sql {
 
 }
 
+=item email_search_sql HASHREF
+
+(Class method)
+
+Emails a notice to the specified customers.
+
+Valid parameters are those of the L<search_sql> method, plus the following:
+
+=over 4
+
+=item from
+
+From: address
+
+=item subject
+
+Email Subject:
+
+=item html_body
+
+HTML body
+
+=item text_body
+
+Text body
+
+=item job
+
+Optional job queue job for status updates.
+
+=back
+
+Returns an error message, or false for success.
+
+If an error occurs during any email, stops the enture send and returns that
+error.  Presumably if you're getting SMTP errors aborting is better than 
+retrying everything.
+
+=cut
+
+sub email_search_sql {
+  my($class, $params) = @_;
+
+  my $from = delete $params->{from};
+  my $subject = delete $params->{subject};
+  my $html_body = delete $params->{html_body};
+  my $text_body = delete $params->{text_body};
+
+  my $job = delete $params->{'job'};
+
+  my $sql_query = $class->search_sql($params);
+
+  my $count_query   = delete($sql_query->{'count_query'});
+  my $count_sth = dbh->prepare($count_query)
+    or die "Error preparing $count_query: ". dbh->errstr;
+  $count_sth->execute
+    or die "Error executing $count_query: ". $count_sth->errstr;
+  my $count_arrayref = $count_sth->fetchrow_arrayref;
+  my $num_cust = $count_arrayref->[0];
+
+  #my @extra_headers = @{ delete($sql_query->{'extra_headers'}) };
+  #my @extra_fields  = @{ delete($sql_query->{'extra_fields'})  };
+
+
+  my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
+
+  #eventually order+limit magic to reduce memory use?
+  foreach my $cust_main ( qsearch($sql_query) ) {
+
+    my $to = $cust_main->invoicing_list_emailonly_scalar;
+    next unless $to;
+
+    my $error = send_email(
+      generate_email(
+        'from'      => $from,
+        'to'        => $to,
+        'subject'   => $subject,
+        'html_body' => $html_body,
+        'text_body' => $text_body,
+      )
+    );
+    return $error if $error;
+
+    if ( $job ) { #progressbar foo
+      $num++;
+      if ( time - $min_sec > $last ) {
+        my $error = $job->update_statustext(
+          int( 100 * $num / $num_cust )
+        );
+        die $error if $error;
+        $last = time;
+      }
+    }
+
+  }
+
+  return '';
+}
+
+use Storable qw(thaw);
+use Data::Dumper;
+use MIME::Base64;
+sub process_email_search_sql {
+  my $job = shift;
+  #warn "$me process_re_X $method for job $job\n" if $DEBUG;
+
+  my $param = thaw(decode_base64(shift));
+  warn Dumper($param) if $DEBUG;
+
+  $param->{'job'} = $job;
+
+  my $error = FS::cust_main->email_search_sql( $param );
+  die $error if $error;
+
+}
+
 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
 
 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
index 383c5fc..be6f2f7 100644 (file)
@@ -104,8 +104,11 @@ sub handler
       use DateTime::Format::Strptime;
       use Lingua::EN::Inflect qw(PL);
       use Tie::IxHash;
+      use URI::URL;
       use URI::Escape;
       use HTML::Entities;
+      use HTML::TreeBuilder;
+      use HTML::FormatText;
       use JSON;
       use MIME::Base64;
       use IO::Handle;
@@ -159,6 +162,8 @@ sub handler
       use FS::part_pkg_taxclass;
       use FS::cust_pkg_reason;
       use FS::cust_refund;
+      use FS::cust_credit_refund;
+      use FS::cust_pay_refund;
       use FS::cust_svc;
       use FS::nas;
       use FS::part_bill_event;
index 851ab5e..9cec62c 100644 (file)
 
 % if ( $type eq 'html' ) {
 
-% #init
-  <SCRIPT TYPE="text/javascript" src="<% $p %>elements/fckeditor/fckeditor.js">
-  </SCRIPT>
-
-% #editor
-  <SCRIPT TYPE="text/javascript">
-    var oFCKeditor = new FCKeditor('value');
-    oFCKeditor.Value = <% $value |js_string %>;
-
-    oFCKeditor.BasePath = '<% $p %>elements/fckeditor/';
-    oFCKeditor.Config['SkinPath'] = '<% $p %>elements/fckeditor/editor/skins/silver/';
-    oFCKeditor.Height = '800';
-    oFCKeditor.Config['StartupFocus'] = true;
-
-    oFCKeditor.Create();
-
-  </SCRIPT>
+  <% include('/elements/htmlarea.html',
+               'field'      => 'value',
+               'curr_value' => $value,
+               'height'     => 800,
+            )
+  %>
 
 % } else {
 
diff --git a/httemplate/elements/htmlarea.html b/httemplate/elements/htmlarea.html
new file mode 100644 (file)
index 0000000..f27c4b5
--- /dev/null
@@ -0,0 +1,36 @@
+<%doc>
+
+Example:
+
+  include('/elements/htmlarea.html',
+            'field'      => 'fieldname',
+            'curr_value' => $curr_value,
+            'height'     => 800,
+         );
+
+</%doc>
+
+% #init
+<SCRIPT TYPE="text/javascript" src="<% $p %>elements/fckeditor/fckeditor.js">
+</SCRIPT>
+
+% #editor
+<SCRIPT TYPE="text/javascript">
+
+  var oFCKeditor = new FCKeditor('<% $opt{'field'} %>');
+  oFCKeditor.Value = <% $opt{'curr_value'} |js_string %>;
+
+  oFCKeditor.BasePath = '<% $p %>elements/fckeditor/';
+  oFCKeditor.Config['SkinPath'] = '<% $p %>elements/fckeditor/editor/skins/silver/';
+  oFCKeditor.Height = '<% $opt{'height'} || 420 %>';
+  oFCKeditor.Config['StartupFocus'] = true;
+
+  oFCKeditor.Create();
+
+</SCRIPT>
+
+<%init>
+
+my %opt = @_;
+
+</%init>
diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html
new file mode 100644 (file)
index 0000000..0d3d622
--- /dev/null
@@ -0,0 +1,145 @@
+<% include('/elements/header.html', $title) %>
+
+<FORM NAME="OneTrueForm" ACTION="email-customers.html" METHOD="POST">
+% foreach my $key ( keys %search ) {
+%   my @values = ref($search{$key}) ? @{$search{$key}} : ( $search{$key} );
+%   foreach my $value ( @values ) {
+      <INPUT TYPE="hidden" NAME="<% $key |h %>" VALUE="<% $value |h %>">
+%   }
+% }
+
+% if ( $cgi->param('magic') eq 'send' ) {
+
+    <FONT SIZE="+2">Sending notice</FONT>
+
+    <% include('/elements/progress-init.html',
+                 'OneTrueForm',
+                 [ keys(%search), qw( from subject html_body text_body ) ],
+                 'process/email-customers.html',
+                 { 'message' => "Notice sent" }, #would be nice to show #, but..
+              )
+    %>
+
+% } elsif ( $cgi->param('magic') eq 'preview' ) {
+
+    <FONT SIZE="+2">Preview notice</FONT>
+
+% }
+
+% if ( $cgi->param('magic') ) {
+
+    <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+      <% include('/elements/tr-fixed.html',
+                   'field'      => 'from',
+                   'label'      => 'From:',
+                   'value' => scalar( $cgi->param('from') ),
+                )
+      %>
+
+      <% include('/elements/tr-fixed.html',
+                   'field'      => 'subject',
+                   'label'      => 'Subject:',
+                   'value' => scalar( $cgi->param('subject') ),
+                )
+      %>
+
+      <INPUT TYPE="hidden" NAME="html_body" VALUE="<% $cgi->param('html_body') |h %>">
+      <TR>
+        <TD ALIGN="right" VALIGN="top">Message (HTML display): </TD>
+        <TD BGCOLOR="#e8e8e8" ALIGN="left"><% $cgi->param('html_body') %></TD>
+      </TR>
+
+%     my $text_body = HTML::FormatText->new(leftmargin=>0)->format(
+%                       HTML::TreeBuilder->new_from_content(
+%                         $cgi->param('html_body')
+%                       )
+%                     );
+      <INPUT TYPE="hidden" NAME="text_body" VALUE="<% $text_body |h %>">
+      <TR>
+        <TD ALIGN="right" VALIGN="top">Message (Text display): </TD>
+        <TD BGCOLOR="#e8e8e8" ALIGN="left"><PRE><% $text_body %></PRE></TD>
+      </TR>
+
+    </TABLE>
+
+% if ( $cgi->param('magic') eq 'preview' ) {
+
+      <SCRIPT>
+        function areyousure(href) {
+          return confirm("Send this notice to <% $num_cust %> customers?");
+        }
+      </SCRIPT>
+
+      <BR>
+      <INPUT TYPE="hidden" NAME="magic" VALUE="send">
+      <INPUT TYPE="submit" VALUE="Send notice" onClick="return areyousure()">
+    
+%   }
+
+% } else {
+
+  <TABLE BGCOLOR="#cccccc" CELLSPACING=0 WIDTH="100%">
+
+    <% include('/elements/tr-input-text.html',
+                 'field' => 'from',
+                 'label' => 'From:',
+              )
+    %>
+
+    <% include('/elements/tr-input-text.html',
+                 'field' => 'subject',
+                 'label' => 'Subject:',
+              )
+    %>
+
+    <TR>
+      <TD ALIGN="right" VALIGN="top">Message: </TD>
+      <TD><% include('/elements/htmlarea.html', 'field'=>'html_body') %></TD>
+    </TR>
+
+  </TABLE>
+
+%#Substitution vars:
+
+    <BR><BR>
+    <INPUT TYPE="hidden" NAME="magic" VALUE="preview">
+    <INPUT TYPE="submit" VALUE="Preview notice">
+
+% }
+
+</FORM>
+
+% if ( $cgi->param('magic') eq 'send' ) {
+    <SCRIPT TYPE="text/javascript">
+      process();
+    </SCRIPT>
+% }
+
+<% include('/elements/footer.html') %>
+
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices');
+
+my %search = $cgi->Vars;
+delete $search{$_} for qw( magic from subject html_body text_body );
+$search{$_} = [ split(/\0/, $search{$_}) ]
+  foreach grep $search{$_} =~ /\0/, keys %search;
+
+my $title = 'Bulk send customer notices';
+
+my $num_cust;
+if ( $cgi->param('magic') eq 'preview' ) {
+  my $sql_query = FS::cust_main->search_sql(\%search);
+  my $count_query = delete($sql_query->{'count_query'});
+  my $count_sth = dbh->prepare($count_query)
+    or die "Error preparing $count_query: ". dbh->errstr;
+  $count_sth->execute
+    or die "Error executing $count_query: ". $count_sth->errstr;
+  my $count_arrayref = $count_sth->fetchrow_arrayref;
+  $num_cust = $count_arrayref->[0];
+}
+
+</%init>
diff --git a/httemplate/misc/process/email-customers.html b/httemplate/misc/process/email-customers.html
new file mode 100644 (file)
index 0000000..d254cfe
--- /dev/null
@@ -0,0 +1,9 @@
+<% $server->process %>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices');
+
+my $server = new FS::UI::Web::JSRPC 'FS::cust_main::process_email_search_sql', $cgi; 
+
+</%init>
index a2ecd04..cd5e51f 100755 (executable)
@@ -1,5 +1,6 @@
 <% include( 'elements/search.html',
                   'title'       => 'Customer Search Results', 
+                  'menubar'     => $menubar,
                   'name'        => 'customers',
                   'query'       => $sql_query,
                   'count_query' => $count_query,
                                    ],
               )
 %>
-<%once>
-
-my $link = [ "${p}view/cust_main.cgi?", 'custnum' ];
-
-</%once>
 <%init>
 
 die "access denied"
@@ -89,4 +85,23 @@ my $count_query   = delete($sql_query->{'count_query'});
 my @extra_headers = @{ delete($sql_query->{'extra_headers'}) };
 my @extra_fields  = @{ delete($sql_query->{'extra_fields'})  };
 
+my $link = [ "${p}view/cust_main.cgi?", 'custnum' ];
+
+###
+# email links
+###
+
+my $menubar = [];
+
+if ( $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices') ) {
+
+  my $uri = new URI::URL;
+  $uri->query_form( \%search_hash );
+  my $query = $uri->query;
+
+  push @$menubar, 'Email a notice to these customers' =>
+                    "${p}misc/email-customers.html?$query",
+
+}
+
 </%init>