[freeside-commits] branch master updated. c6ef5a3a043c4fafa2f8d21028609f1b9b70eb47

Jonathan Prykop jonathan at 420.am
Mon Jun 22 16:36:20 PDT 2015


The branch, master has been updated
       via  c6ef5a3a043c4fafa2f8d21028609f1b9b70eb47 (commit)
      from  11ca9a51a76837f1821b2b0e8972c78bf221c6a1 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit c6ef5a3a043c4fafa2f8d21028609f1b9b70eb47
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Mon Jun 22 18:34:27 2015 -0500

    RT#34078: Payment History Report / Statement

diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index 85a96c7..610754c 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -696,73 +696,15 @@ sub billing_history {
     $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} )
                             : '(none)';
 
-  my @history = ();
-
   my $conf = new FS::Conf;
 
-  if ( $conf->exists('selfservice-billing_history-line_items') ) {
-
-    foreach my $cust_bill ( $cust_main->cust_bill ) {
-
-      push @history, {
-        'type'        => 'Line item',
-        'description' => $_->desc( $cust_main->locale ).
-                           ( $_->sdate && $_->edate
-                               ? ' '. time2str('%d-%b-%Y', $_->sdate).
-                                 ' To '. time2str('%d-%b-%Y', $_->edate)
-                               : ''
-                           ),
-        'amount'      => sprintf('%.2f', $_->setup + $_->recur ),
-        'date'        => $cust_bill->_date,
-        'date_pretty' =>  time2str('%m/%d/%Y', $cust_bill->_date ),
-      }
-        foreach $cust_bill->cust_bill_pkg;
-
-    }
-
-  } else {
+  $return{'history'} = [
+    $cust_main->payment_history(
+      'line_items' => $conf->exists('selfservice-billing_history-line_items'),
+      'reverse_sort' => 1,
+    )
+  ];
 
-    push @history, {
-                     'type'        => 'Invoice',
-                     'description' => 'Invoice #'. $_->display_invnum,
-                     'amount'      => sprintf('%.2f', $_->charged ),
-                     'date'        => $_->_date,
-                     'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
-                   }
-      foreach $cust_main->cust_bill;
-
-  }
-
-  push @history, {
-                   'type'        => 'Payment',
-                   'description' => 'Payment', #XXX type
-                   'amount'      => sprintf('%.2f', 0 - $_->paid ),
-                   'date'        => $_->_date,
-                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
-                 }
-    foreach $cust_main->cust_pay;
-
-  push @history, {
-                   'type'        => 'Credit',
-                   'description' => 'Credit', #more info?
-                   'amount'      => sprintf('%.2f', 0 -$_->amount ),
-                   'date'        => $_->_date,
-                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
-                 }
-    foreach $cust_main->cust_credit;
-
-  push @history, {
-                   'type'        => 'Refund',
-                   'description' => 'Refund', #more info?  type, like payment?
-                   'amount'      => $_->refund,
-                   'date'        => $_->_date,
-                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
-                 }
-    foreach $cust_main->cust_refund;
-
-  @history = sort { $b->{'date'} <=> $a->{'date'} } @history;
-
-  $return{'history'} = \@history;
   $return{'money_char'} = $conf->config("money_char") || '$',
 
   return \%return;
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 191a712..f3e2447 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -2655,6 +2655,13 @@ and customer address. Include units.',
   },
 
   {
+    'key'         => 'payment_history_msgnum',
+    'section'     => 'notification',
+    'description' => 'Template to use for sending payment history to customer',
+    %msg_template_options,
+  },
+
+  {
     'key'         => 'payby',
     'section'     => 'billing',
     'description' => 'Available payment types.',
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index f102d97..b7efa18 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -4095,6 +4095,204 @@ my ($self,$field) = @_;
 
 }
 
+=item payment_history
+
+Returns an array of hashrefs standardizing information from cust_bill, cust_pay,
+cust_credit and cust_refund objects.  Each hashref has the following fields:
+
+I<type> - one of 'Line item', 'Invoice', 'Payment', 'Credit', 'Refund' or 'Previous'
+
+I<date> - value of _date field, unix timestamp
+
+I<date_pretty> - user-friendly date
+
+I<description> - user-friendly description of item
+
+I<amount> - impact of item on user's balance 
+(positive for Invoice/Refund/Line item, negative for Payment/Credit.)
+Not to be confused with the native 'amount' field in cust_credit, see below.
+
+I<amount_pretty> - includes money char
+
+I<balance> - customer balance, chronologically as of this item
+
+I<balance_pretty> - includes money char
+
+I<charged> - amount charged for cust_bill (Invoice or Line item) records, undef for other types
+
+I<paid> - amount paid for cust_pay records, undef for other types
+
+I<credit> - amount credited for cust_credit records, undef for other types.
+Literally the 'amount' field from cust_credit, renamed here to avoid confusion.
+
+I<refund> - amount refunded for cust_refund records, undef for other types
+
+The four table-specific keys always have positive values, whether they reflect charges or payments.
+
+The following options may be passed to this method:
+
+I<line_items> - if true, returns charges ('Line item') rather than invoices
+
+I<start_date> - unix timestamp, only include records on or after.
+If specified, an item of type 'Previous' will also be included.
+It does not have table-specific fields.
+
+I<end_date> - unix timestamp, only include records before
+
+I<reverse_sort> - order from newest to oldest (default is oldest to newest)
+
+I<conf> - optional already-loaded FS::Conf object.
+
+=cut
+
+# Caution: this gets used by FS::ClientAPI::MyAccount::billing_history,
+# and also payment_history_text, which should both be kept customer-friendly.
+# If you add anything that shouldn't be passed on through the API or exposed 
+# to customers, add a new option to include it, don't include it by default
+sub payment_history {
+  my $self = shift;
+  my $opt = ref($_[0]) ? $_[0] : { @_ };
+
+  my $conf = $$opt{'conf'} || new FS::Conf;
+  my $money_char = $conf->config("money_char") || '$',
+
+  #first load entire history, 
+  #need previous to calculate previous balance
+  #loading after end_date shouldn't hurt too much?
+  my @history = ();
+  if ( $$opt{'line_items'} ) {
+
+    foreach my $cust_bill ( $self->cust_bill ) {
+
+      push @history, {
+        'type'        => 'Line item',
+        'description' => $_->desc( $self->locale ).
+                           ( $_->sdate && $_->edate
+                               ? ' '. time2str('%d-%b-%Y', $_->sdate).
+                                 ' To '. time2str('%d-%b-%Y', $_->edate)
+                               : ''
+                           ),
+        'amount'      => sprintf('%.2f', $_->setup + $_->recur ),
+        'charged'     => sprintf('%.2f', $_->setup + $_->recur ),
+        'date'        => $cust_bill->_date,
+        'date_pretty' =>  time2str('%m/%d/%Y', $cust_bill->_date ),
+      }
+        foreach $cust_bill->cust_bill_pkg;
+
+    }
+
+  } else {
+
+    push @history, {
+                     'type'        => 'Invoice',
+                     'description' => 'Invoice #'. $_->display_invnum,
+                     'amount'      => sprintf('%.2f', $_->charged ),
+                     'charged'     => sprintf('%.2f', $_->charged ),
+                     'date'        => $_->_date,
+                     'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                   }
+      foreach $self->cust_bill;
+
+  }
+
+  push @history, {
+                   'type'        => 'Payment',
+                   'description' => 'Payment', #XXX type
+                   'amount'      => sprintf('%.2f', 0 - $_->paid ),
+                   'paid'        => sprintf('%.2f', $_->paid ),
+                   'date'        => $_->_date,
+                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                 }
+    foreach $self->cust_pay;
+
+  push @history, {
+                   'type'        => 'Credit',
+                   'description' => 'Credit', #more info?
+                   'amount'      => sprintf('%.2f', 0 -$_->amount ),
+                   'credit'      => sprintf('%.2f', $_->amount ),
+                   'date'        => $_->_date,
+                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                 }
+    foreach $self->cust_credit;
+
+  push @history, {
+                   'type'        => 'Refund',
+                   'description' => 'Refund', #more info?  type, like payment?
+                   'amount'      => $_->refund,
+                   'refund'      => $_->refund,
+                   'date'        => $_->_date,
+                   'date_pretty' =>  time2str('%m/%d/%Y', $_->_date ),
+                 }
+    foreach $self->cust_refund;
+
+  #put it all in chronological order
+  @history = sort { $a->{'date'} <=> $b->{'date'} } @history;
+
+  #calculate balance, filter items outside date range
+  my $previous = 0;
+  my $balance = 0;
+  my @out = ();
+  foreach my $item (@history) {
+    last if $$opt{'end_date'} && ($$item{'date'} >= $$opt{'end_date'});
+    $balance += $$item{'amount'};
+    if ($$opt{'start_date'} && ($$item{'date'} < $$opt{'start_date'})) {
+      $previous += $$item{'amount'};
+      next;
+    }
+    $$item{'balance'} = sprintf("%.2f",$balance);
+    foreach my $key ( qw(amount balance) ) {
+      $$item{$key.'_pretty'} = $$item{$key};
+      $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/;
+    }
+    push(@out,$item);
+  }
+
+  # start with previous balance, if there was one
+  if ($previous) {
+    my $item = {
+      'type'        => 'Previous',
+      'description' => 'Previous balance',
+      'amount'      => sprintf("%.2f",$previous),
+      'balance'     => sprintf("%.2f",$previous),
+    };
+    #false laziness with above
+    foreach my $key ( qw(amount balance) ) {
+      $$item{$key.'_pretty'} = $$item{$key};
+      $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/;
+    }
+    unshift(@out,$item);
+  }
+
+  @out = reverse @history if $$opt{'reverse_sort'};
+
+  return @out;
+}
+
+=item payment_history_text
+
+Accepts the same options as L</payment_history> and returns those
+results as a string table with fixed-width columns, max width 80 char.
+
+=cut
+
+sub payment_history_text {
+  my $self = shift;
+  my $opt = ref($_[0]) ? $_[0] : { @_ };
+  my $out = sprintf("%-12s",'Date');
+  $out .= sprintf("%11s",'Amount') . '  ';
+  $out .= sprintf("%11s",'Balance') . '  ';
+  $out .= 'Description'; #don't need to pad with spaces
+  $out .= "\n";
+  foreach my $item ($self->payment_history($opt)) {
+    $out .= sprintf("%-10.10s",$$item{'date_pretty'}) . '  ';   #12 width
+    $out .= sprintf("%11.11s",$$item{'amount_pretty'}) . '  ';  #13 width
+    $out .= sprintf("%11.11s",$$item{'balance_pretty'}) . '  '; #13 width
+    $out .= sprintf("%.42s",$$item{'description'});             #max 42 width
+    $out .= "\n";
+  }
+  return $out;
+}
+
 =back
 
 =head1 CLASS METHODS
diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm
index bdad511..83ca3a2 100644
--- a/FS/FS/cust_main_Mixin.pm
+++ b/FS/FS/cust_main_Mixin.pm
@@ -394,6 +394,11 @@ HTML body
 
 Text body
 
+=item sub_param
+
+Optional list of parameter hashrefs to be passed
+along to L<FS::msg_template/prepare>.
+
 =back
 
 Returns an error message, or false for success.
@@ -470,6 +475,8 @@ sub email_search_result {
         'cust_main' => $cust_main,
         'object'    => $obj,
       );
+      $message{'sub_param'} = $param->{'sub_param'}
+        if $param->{'sub_param'};
     }
     else {
       my @to = $cust_main->invoicing_list_emailonly;
@@ -547,7 +554,9 @@ sub process_email_search_result {
 
   $param->{'search'} = thaw(decode_base64($param->{'search'}))
     or die "process_email_search_result requires search params.\n";
-
+  $param->{'sub_param'} = thaw(decode_base64($param->{'sub_param'}))
+    or die "process_email_search_result error decoding sub_param\n"
+      if $param->{'sub_param'};
 #  $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
 #    unless ref($param->{'payby'});
 
diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm
index c52b633..fe8cbeb 100644
--- a/FS/FS/msg_template.pm
+++ b/FS/FS/msg_template.pm
@@ -269,7 +269,19 @@ invoicing_list addresses.  Multiple addresses may be comma-separated.
 
 =item substitutions
 
-A hash reference of additional substitutions
+A hash reference of additional string substitutions
+
+=item sub_param
+
+A hash reference, keys are the names of existing substitutions,
+values are an addition parameter object to pass to the subroutine
+for that substitution, e.g.
+
+	'sub_param' => {
+	  'payment_history' => {
+	    'start_date' => 1434764295,
+	  },
+	},
 
 =back
 
@@ -327,7 +339,10 @@ sub prepare {
       }
       elsif( ref($name) eq 'ARRAY' ) {
         # [ foo => sub { ... } ]
-        $hash{$prefix.($name->[0])} = $name->[1]->($obj);
+        my @subparam = ();
+        push(@subparam, $opt{'sub_param'}->{$name->[0]})
+          if $opt{'sub_param'} && $opt{'sub_param'}->{$name->[0]};
+        $hash{$prefix.($name->[0])} = $name->[1]->($obj, at subparam);
       }
       else {
         warn "bad msg_template substitution: '$name'\n";
@@ -340,7 +355,10 @@ sub prepare {
     $hash{$_} = $opt{substitutions}->{$_} foreach keys %{$opt{substitutions}};
   }
 
-  $_ = encode_entities($_ || '') foreach values(%hash);
+  foreach my $key (keys %hash) {
+    next if $self->no_encode($key);
+    $hash{$key} = encode_entities($_ || '');
+  };
 
   ###
   # clean up template
@@ -509,6 +527,13 @@ my $usage_warning = sub {
 
 #my $conf = new FS::Conf;
 
+# for substitutions that handle their own encoding
+sub no_encode {
+  my $self = shift;
+  my $field = shift;
+  return ($field eq 'payment_history');
+}
+
 #return contexts and fill-in values
 # If you add anything, be sure to add a description in 
 # httemplate/edit/msg_template.html.
@@ -567,6 +592,12 @@ sub substitutions {
       [ selfservice_server_base_url => sub { 
           $conf->config('selfservice_server-base_url') #, shift->agentnum) 
         } ],
+      [ payment_history => sub {
+          my $cust_main = shift;
+          my $param = shift || {};
+          #html works, see no_encode method
+          return '<PRE>' . encode_entities($cust_main->payment_history_text($param)) . '</PRE>';
+        } ],
     ],
     # next_bill_date
     'cust_pkg'  => [qw( 
diff --git a/FS/FS/msg_template/InitialData.pm b/FS/FS/msg_template/InitialData.pm
index a4e27fd..87c407c 100644
--- a/FS/FS/msg_template/InitialData.pm
+++ b/FS/FS/msg_template/InitialData.pm
@@ -21,6 +21,15 @@ If you did not request this password reset, you may safely ignore and delete thi
 END
                       ],
     },
+    { msgname   => 'payment_history_template',
+      mime_type => 'text/html',
+      _conf        => 'payment_history_msgnum',
+      _insert_args => [ subject => '{ $company_name } payment history',
+                        body    => <<'END',
+{ $payment_history }
+END
+                      ],
+    },
   ];
 }
 
diff --git a/httemplate/edit/msg_template.html b/httemplate/edit/msg_template.html
index 7f38241..ced98fe 100644
--- a/httemplate/edit/msg_template.html
+++ b/httemplate/edit/msg_template.html
@@ -210,6 +210,7 @@ my %substitutions = (
     '$company_address'=> 'Our company address',
     '$company_phonenum' => 'Our phone number',
     '$selfservice_server_base_url' => 'Base URL of customer self-service',
+    '$payment_history' => 'List of invoices/payments/credits/refunds',
   ],
   'contact' => [ # duplicate this for shipping
     '$name'           => 'Company and contact name',
@@ -322,7 +323,7 @@ my $widget = new HTML::Widgets::SelectLayers(
     my @hints = @{ $substitutions{$section} };
     while(@hints) {
       my $key = shift @hints;
-      $html .= qq!\n<TR><TD><A href="javascript:insertHtml('{$key}')">$key</A></TD>!;
+      $html .= qq!\n<TR><TD STYLE="padding-right: .25em;"><A href="javascript:insertHtml('{$key}')">$key</A></TD>!;
       $html .= "\n<TD>".shift(@hints).'</TD></TR>';
     }
     $html .= "\n</TABLE>";
diff --git a/httemplate/misc/email-customers-history.html b/httemplate/misc/email-customers-history.html
new file mode 100644
index 0000000..2f9a38d
--- /dev/null
+++ b/httemplate/misc/email-customers-history.html
@@ -0,0 +1,51 @@
+
+ <% include('email-customers.html',
+      'form_action'       => 'email-customers-history.html',
+      'sub_param_process' => $sub_param_process,
+      'alternate_form'    => $alternate_form,
+      'title'             => 'Send payment history',
+    )
+ %>
+
+<%init>
+
+my $sub_param_process = sub {
+  my $conf = shift;
+  my %sub_param;
+  foreach my $field ( qw( start_date end_date ) ) {
+    $sub_param{'payment_history'}->{$field} = parse_datetime($cgi->param($field));
+    $cgi->delete($field);
+  }
+  $cgi->param('msgnum',$conf->config('payment_history_msgnum'));
+  return %sub_param;
+};
+
+my $alternate_form = sub {
+  my %sub_param = @_;
+  # this could maaaybe be a separate element, for cleanliness
+  # but it's really only for use by this page, and it's not overly complicated
+  my $noinit = 0;
+  return join("\n",
+    '<TABLE BORDER="0">',
+    (
+      map {
+        my $label = ucfirst($_);
+        $label =~ s/_/ /;
+        include('/elements/tr-input-date-field.html',{
+          'name' => $_,
+          'value' => $sub_param{'payment_history'}->{$_} || '',
+          'label' => $label,
+          'noinit' => $noinit++
+        });
+      }
+      qw( start_date end_date )
+    ),
+    '</TABLE>',
+    '<INPUT TYPE="hidden" NAME="msgnum" VALUE="' . $cgi->param('msgnum') . '">',
+    '<INPUT TYPE="hidden" NAME="action" VALUE="preview">',
+    '<INPUT TYPE="submit" VALUE="Preview notice">',
+  );
+};
+
+</%init>
+
diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html
index 83e8615..d1d5ac7 100644
--- a/httemplate/misc/email-customers.html
+++ b/httemplate/misc/email-customers.html
@@ -1,3 +1,26 @@
+<%doc>
+
+Allows emailing one or more customers, based on a search for customers.  Search can
+be specified either through cust_main fields as cgi params, or through a base64 encoded
+frozen hash in the 'search' cgi param.  Form allows selecting an existing msg_template,
+or creating a custom message, and shows a preview of the message before sending.
+If linked to as a popup, include the cgi parameter 'popup' for proper header handling.
+
+This may also be used as an element in other pages, enabling you to pass along
+additional substitution parameters to a message template, with the following options:
+
+form_action - the URL to submit the form to
+
+sub_param_process - subroutine to override cgi param values (such as msgnum) 
+and parse/delete additional form fields from the cgi;  should return a %sub_param 
+hash to be passed along for message substitution
+
+alternate_form - an alternate form for template selection/message creation
+
+title - the title of the page
+
+</%doc>
+
 % if ($popup) {
 <% include('/elements/header-popup.html', $title) %>
 % } else {
@@ -5,13 +28,16 @@
 % }
 
 
-<FORM NAME="OneTrueForm" ACTION="email-customers.html" METHOD="POST">
+<FORM NAME="OneTrueForm" ACTION="<% $form_action %>" METHOD="POST">
 <INPUT TYPE="hidden" NAME="table" VALUE="<% $table %>">
 %# Mixing search params with from address, subject, etc. required special-case
 %# handling of those, risked name conflicts, and caused massive problems with 
 %# multi-valued search params.  We are no longer in search context, so we 
 %# pack the search into a Storable string for later use.
 <INPUT TYPE="hidden" NAME="search" VALUE="<% encode_base64(nfreeze(\%search)) %>">
+% if (%sub_param) {
+<INPUT TYPE="hidden" NAME="sub_param" VALUE="<% encode_base64(nfreeze(\%sub_param)) %>">
+% }
 <INPUT TYPE="hidden" NAME="popup" VALUE="<% $popup %>">
 <INPUT TYPE="hidden" NAME="url" VALUE="<% $url | h %>">
 
@@ -21,7 +47,7 @@
 
     <% include('/elements/progress-init.html',
                  'OneTrueForm',
-                 [ qw( search table from subject html_body text_body msgnum ) ],
+                 [ qw( search table from subject html_body text_body msgnum sub_param ) ],
                  'process/email-customers.html',
                  $pdest,
               )
@@ -93,6 +119,10 @@
     
 %   }
 
+% } elsif ($alternate_form) {
+
+<% $alternate_form %>
+
 % } else {
 
 <SCRIPT TYPE="text/javascript">
@@ -144,7 +174,7 @@ Template:
     <INPUT TYPE="hidden" NAME="action" VALUE="preview">
     <INPUT TYPE="submit" VALUE="Preview notice">
 
-% }
+% } #end not preview or alternate form
 
 </FORM>
 
@@ -158,11 +188,18 @@ Template:
 
 <%init>
 
+my %opt = @_;
+
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices');
 
 my $conf = FS::Conf->new;
 
+my $form_action = $opt{'form_action'} || 'email-customers.html';
+my %sub_param = $opt{'sub_param_process'} ? &{$opt{'sub_param_process'}}($conf) : ();
+my $alternate_form = $opt{'alternate_form'} ? &{$opt{'alternate_form'}}(%sub_param) : ();
+my $title = $opt{'title'} || 'Send customer notices';
+
 my $table = $cgi->param('table') or die "'table' required";
 my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || '';
 
@@ -177,7 +214,7 @@ if ( $cgi->param('search') ) {
 }
 else {
   %search = $cgi->Vars;
-  delete $search{$_} for qw( action table from subject html_body text_body popup url );
+  delete $search{$_} for qw( action table from subject html_body text_body popup url sub_param );
   # FS::$table->search is expected to know which parameters might be 
   # multi-valued, and to accept scalar values for them also.  No good 
   # solution to this since CGI can't tell whether a parameter _might_
@@ -185,8 +222,6 @@ else {
   @search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search;
 } 
 
-my $title = 'Send customer notices';
-
 my $num_cust;
 my $from = '';
 if ( $cgi->param('from') ) {
@@ -221,10 +256,12 @@ if ( $cgi->param('action') eq 'preview' ) {
     $sql_query->{'order_by'} = '';
     my $object = qsearchs($sql_query);
     my $cust = $object->cust_main;
-    my %message = $msg_template->prepare(
+    my %msgopts = (
       'cust_main' => $cust,
-      'object' => $object
+      'object' => $object,
     );
+    $msgopts{'sub_param'} = \%sub_param if %sub_param; 
+    my %message = $msg_template->prepare(%msgopts);
     ($from, $subject, $html_body) = @message{'from', 'subject', 'html_body'};
   }
 }
diff --git a/httemplate/view/cust_main/menu.html b/httemplate/view/cust_main/menu.html
index e85cc25..ff8937a 100644
--- a/httemplate/view/cust_main/menu.html
+++ b/httemplate/view/cust_main/menu.html
@@ -112,6 +112,9 @@ my $conf = FS::Conf->new;
 
 my %payby = map { $_ => 1 } $conf->config('payby');
 
+# cached for conditions, to avoid looking it up twice
+my $invoicing_list_emailonly = $cust_main->invoicing_list_emailonly;
+
 # nice declarative menu; should be a parameter to some kind of menu generator
 my @menu = ( 
   [
@@ -197,7 +200,8 @@ my @menu = (
                       'misc/email-customers.html?table=cust_main;search_hash='.
                       'agent_virt_agentnum='.$agentnum.";custnum=$custnum";
                      },
-      condition   => sub { shift->invoicing_list_emailonly },
+      condition   => sub { $invoicing_list_emailonly },
+      acl         => 'Bulk send customer notices',
     },
   ],
   [
@@ -478,6 +482,18 @@ my @menu = (
         FS::cust_pay_pending->count('custnum = ?', shift->custnum) > 0
       },
     },
+    {
+      label       => 'Email payment history to this customer',
+      url         => sub {
+                      my $cust_main = shift;
+                      my $agentnum = $cust_main->agentnum;
+                      'misc/email-customers-history.html?table=cust_main;search_hash='.
+                      'agent_virt_agentnum='.$agentnum.";custnum=$custnum;url=".
+                      uri_escape($cgi->self_url);
+                     },
+      condition   => sub { $invoicing_list_emailonly },
+      acl         => 'Bulk send customer notices',
+    },
   ],
   [
     {

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/ClientAPI/MyAccount.pm                 |   70 +--------
 FS/FS/Conf.pm                                |    7 +
 FS/FS/cust_main.pm                           |  198 ++++++++++++++++++++++++++
 FS/FS/cust_main_Mixin.pm                     |   11 +-
 FS/FS/msg_template.pm                        |   37 ++++-
 FS/FS/msg_template/InitialData.pm            |    9 ++
 httemplate/edit/msg_template.html            |    3 +-
 httemplate/misc/email-customers-history.html |   51 +++++++
 httemplate/misc/email-customers.html         |   53 +++++--
 httemplate/view/cust_main/menu.html          |   18 ++-
 10 files changed, 379 insertions(+), 78 deletions(-)
 create mode 100644 httemplate/misc/email-customers-history.html




More information about the freeside-commits mailing list