summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/AccessRight.pm3
-rw-r--r--FS/FS/ConfDefaults.pm3
-rw-r--r--FS/FS/Misc.pm164
-rw-r--r--FS/FS/cust_main.pm118
-rw-r--r--htetc/handler.pl5
-rw-r--r--httemplate/edit/invoice_template.html23
-rw-r--r--httemplate/elements/htmlarea.html36
-rw-r--r--httemplate/misc/email-customers.html145
-rw-r--r--httemplate/misc/process/email-customers.html9
-rwxr-xr-xhttemplate/search/cust_main.html25
10 files changed, 497 insertions, 34 deletions
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index 13dbd7f..5621a97 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -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',
diff --git a/FS/FS/ConfDefaults.pm b/FS/FS/ConfDefaults.pm
index 7978259..a3af52d 100644
--- a/FS/FS/ConfDefaults.pm
+++ b/FS/FS/ConfDefaults.pm
@@ -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
diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm
index 1f6eece..936f94a 100644
--- a/FS/FS/Misc.pm
+++ b/FS/FS/Misc.pm
@@ -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
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 33788f3..946495c 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -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
diff --git a/htetc/handler.pl b/htetc/handler.pl
index 383c5fc..be6f2f7 100644
--- a/htetc/handler.pl
+++ b/htetc/handler.pl
@@ -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;
diff --git a/httemplate/edit/invoice_template.html b/httemplate/edit/invoice_template.html
index 851ab5e..9cec62c 100644
--- a/httemplate/edit/invoice_template.html
+++ b/httemplate/edit/invoice_template.html
@@ -10,23 +10,12 @@
% 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
index 0000000..f27c4b5
--- /dev/null
+++ b/httemplate/elements/htmlarea.html
@@ -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
index 0000000..0d3d622
--- /dev/null
+++ b/httemplate/misc/email-customers.html
@@ -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
index 0000000..d254cfe
--- /dev/null
+++ b/httemplate/misc/process/email-customers.html
@@ -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>
diff --git a/httemplate/search/cust_main.html b/httemplate/search/cust_main.html
index a2ecd04..cd5e51f 100755
--- a/httemplate/search/cust_main.html
+++ b/httemplate/search/cust_main.html
@@ -1,5 +1,6 @@
<% include( 'elements/search.html',
'title' => 'Customer Search Results',
+ 'menubar' => $menubar,
'name' => 'customers',
'query' => $sql_query,
'count_query' => $count_query,
@@ -30,11 +31,6 @@
],
)
%>
-<%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>