[freeside-commits] branch FREESIDE_4_BRANCH updated. 7bdef72932450207f1dc3262460358a948a3c3dc

Mitch Jackson mitch at freeside.biz
Sun Oct 28 12:11:26 PDT 2018


The branch, FREESIDE_4_BRANCH has been updated
       via  7bdef72932450207f1dc3262460358a948a3c3dc (commit)
       via  a1214138edbe6aaeae7d2f88539c1548f4126a83 (commit)
       via  c9019c80c7572ed746375120a4a5a6e43dfa9213 (commit)
       via  be6ebb643a9482bdc036e5974d1939daeeaa7379 (commit)
       via  945bc9a8c3edf867cbaea0aa2af738d73ac180c8 (commit)
      from  9ecbe48e7226f9fe9b8b641d6fe7b3fddcd4bc1e (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 7bdef72932450207f1dc3262460358a948a3c3dc
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Sat Oct 27 11:24:17 2018 -0400

    RT# 73422 Fix XSS

diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html
index 35a74a593..50935baf4 100644
--- a/httemplate/search/contact.html
+++ b/httemplate/search/contact.html
@@ -162,10 +162,10 @@ my %classname =
 
 # And now for something completly different:
 my @report = (
-  { label => 'First',  field => sub { encode_entities shift->contact_first }},
-  { label => 'Last',   field => sub { encode_entities shift->contact_last }},
-  { label => 'Title',  field => sub { encode_entities shift->contact_title }},
-  { label => 'E-Mail', field => sub { encode_entities shift->contact_email_emailaddress }},
+  { label => 'First',  field => 'contact_first' },
+  { label => 'Last',   field => 'contact_last'  },
+  { label => 'Title',  field => 'contact_title' },
+  { label => 'E-Mail', field => 'contact_email_emailaddress' },
   { label => 'Work Phone',   field => $get_phone_sub->('Work') },
   { label => 'Mobile Phone', field => $get_phone_sub->('Mobile') },
   { label => 'Home Phone',   field => $get_phone_sub->('Home') },

commit a1214138edbe6aaeae7d2f88539c1548f4126a83
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Tue Oct 23 19:18:58 2018 -0400

    RT# 73422 Fix XSS

diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html
index aaa591cf4..35a74a593 100644
--- a/httemplate/search/contact.html
+++ b/httemplate/search/contact.html
@@ -162,10 +162,10 @@ my %classname =
 
 # And now for something completly different:
 my @report = (
-  { label => 'First',  field => sub { shift->contact_first }},
-  { label => 'Last',   field => sub { shift->contact_last }},
-  { label => 'Title',  field => sub { shift->contact_title }},
-  { label => 'E-Mail', field => sub { shift->contact_email_emailaddress }},
+  { label => 'First',  field => sub { encode_entities shift->contact_first }},
+  { label => 'Last',   field => sub { encode_entities shift->contact_last }},
+  { label => 'Title',  field => sub { encode_entities shift->contact_title }},
+  { label => 'E-Mail', field => sub { encode_entities shift->contact_email_emailaddress }},
   { label => 'Work Phone',   field => $get_phone_sub->('Work') },
   { label => 'Mobile Phone', field => $get_phone_sub->('Mobile') },
   { label => 'Home Phone',   field => $get_phone_sub->('Home') },
@@ -204,10 +204,15 @@ my @report = (
     field => sub {
       my $rec = shift;
       if ($rec->prospect_contact_prospectnum) {
-        return $rec->contact_company
-          || $rec->contact_last.' '.$rec->contact_first;
+        return encode_entities(
+          $rec->contact_company
+          || $rec->contact_last.' '.$rec->contact_first
+        );
       }
-      $rec->cust_main_company || $rec->cust_main_last.' '.$rec->cust_main_first;
+      encode_entities(
+        $rec->cust_main_company
+        || $rec->cust_main_last.' '.$rec->cust_main_first
+      );
     }},
   { label => 'Self-service',
     field => sub {
@@ -218,9 +223,11 @@ my @report = (
   { label => 'Comment',
     field => sub {
       my $rec = shift;
-      $rec->prospect_contact_prospectnum
-      ? $rec->prospect_contact_comment
-      : $rec->cust_contact_comment;
+      encode_entities(
+        $rec->prospect_contact_prospectnum
+        ? $rec->prospect_contact_comment
+        : $rec->cust_contact_comment
+      );
     }},
 );
 

commit c9019c80c7572ed746375120a4a5a6e43dfa9213
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Sat Feb 3 22:19:13 2018 -0600

    RT# 73422 Agent virt update for report Customer Contacts

diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html
index e02833319..aaa591cf4 100644
--- a/httemplate/search/contact.html
+++ b/httemplate/search/contact.html
@@ -3,15 +3,15 @@
   name_singular => 'contact',
   query         => {
     select    => join(', ', @select),
-    table     => 'contact',
+    table     => $link,
     addl_from => $addl_from,
-    hashref   => {}, #\%hash,
+    hashref   => {},
     extra_sql => "WHERE $extra_sql",
     order_by  => "ORDER BY contact_last,contact_first,contact_email_emailaddress"
   },
   count_query => "
     SELECT COUNT(*)
-    FROM contact
+    FROM $link
     $addl_from
     WHERE $extra_sql
   ",
@@ -19,7 +19,8 @@
   fields    => \@fields,
   links     => \@links,
   html_init => $send_email_link,
-#  agent_virt    => 1, # Not supported unless table is cust_main/prospect_main
+  agent_virt => 1,
+  agent_pos  => 11,
 &>
 <%init>
 
@@ -39,7 +40,6 @@ my $classnum_null = grep{ $_ eq 0           } $cgi->param('classnum');
 my @dest = grep{ /^(message|invoice)$/ } $cgi->param('dest');
 @dest = ('message') unless @dest;
 
-
 # Cache the contact_class table
 my %classname =
   map {$_->classnum => $_->classname}
@@ -53,14 +53,11 @@ my %colmap = (
       contact => [qw/first last title contactnum/],
       contact_email => [qw/emailaddress/],
     },
-    joinsql => "
-      LEFT JOIN contact_email
-        ON (contact.contactnum = contact_email.contactnum)
-    ",
+    joinsql => "",
   },
 
   # These are included if we're viewing customer records
-  customer => {
+  cust_main => {
     cols => {
       cust_main => [qw/first last company/],
       cust_contact => [qw/
@@ -69,23 +66,27 @@ my %colmap = (
     },
     joinsql => "
       LEFT JOIN cust_contact
-        ON (contact.contactnum = cust_contact.contactnum)
-      LEFT JOIN cust_main
-        ON (cust_contact.custnum = cust_main.custnum)
+        ON (cust_main.custnum = cust_contact.custnum)
+      LEFT JOIN contact
+        on (cust_contact.contactnum = contact.contactnum)
+      LEFT JOIN contact_email
+        ON (cust_contact.contactnum = contact_email.contactnum)
     ",
   },
 
   # These are included if we're viewing prospect records
-  prospect => {
+  prospect_main => {
     cols => {
       prospect_main => [qw/company/],
       prospect_contact => [qw/prospectnum classnum comment/],
     },
     joinsql => "
       LEFT JOIN prospect_contact
-        ON (contact.contactnum = prospect_contact.contactnum)
-      LEFT JOIN prospect_main
-        ON (prospect_contact.prospectnum = prospect_main.prospectnum)
+        ON (prospect_main.prospectnum = prospect_contact.prospectnum)
+      LEFT JOIN contact
+        on (prospect_contact.contactnum = contact.contactnum)
+      LEFT JOIN contact_email
+        ON (prospect_contact.contactnum = contact_email.contactnum)
     ",
   },
 );
@@ -94,14 +95,16 @@ my @select;
 my $addl_from;
 my $extra_sql;
 my $hashref;
-my $link = $cgi->param('link'); # cust_main, prospect_main or both
+my $link = $cgi->param('link'); # cust_main or prospect_main
+
+push @select,'agentnum';
 
-my @rectypes = ('common');
-push @rectypes,'customer' if $link eq 'cust_main'     || $link eq 'both';
-push @rectypes,'prospect' if $link eq 'prospect_main' || $link eq 'both';
+# this shouldn't happen without funny-busines
+die "Invalid \$link type ($link)"
+  unless $link eq 'cust_main' || $link eq 'prospect_main';
 
 # Build @select and $addl_from
-for my $key (@rectypes) {
+for my $key ('common', $link) {
   $addl_from .= $colmap{$key}->{joinsql};
   my $cols = $colmap{$key}->{cols};
   for my $tbl (keys %{$cols}) {
@@ -109,39 +112,19 @@ for my $key (@rectypes) {
   }
 }
 
-# Filter for custnum/prospectnum
-$extra_sql .= ' (';
-$extra_sql .= "cust_contact.custnum IS NOT NULL"
-  if $link eq 'cust_main' || $link eq 'both';
-$extra_sql .= " OR " if $link eq 'both';
-$extra_sql .= "prospect_contact.prospectnum IS NOT NULL"
-  if $link eq 'prospect_main' || $link eq 'both';
-$extra_sql .= ') ';
-
 # Filter for Contact Type
 if (@classnum || $classnum_null) {
   my @stm;
-
-  push @stm, 'cust_contact.classnum IN ('.join(',', at classnum).')'
-    if @classnum && ($link eq 'cust_main' || $link eq 'both');
-
-  push @stm, 'prospect_contact.classnum IN ('.join(',', at classnum).')'
-    if @classnum && ($link eq 'prospect_main' || $link eq 'both');
-
-  push @stm, 'cust_contact.classnum IS NULL'
-    if $classnum_null && ($link eq 'cust_main' || $link eq 'both');
-
-  push @stm, 'prospect_contact.classnum IS NULL'
-    if $classnum_null && ($link eq 'prospect_main' || $link eq 'both');
-
-  $extra_sql .= "\nAND (" . join(' OR ', at stm) . ') ';
+  my $tbl = $link eq 'cust_main' ? 'cust_contact' : 'prospect_contact';
+  push @stm, "${tbl}.classnum IN (".join(',', at classnum).')' if @classnum;
+  push @stm, "${tbl}.classnum IS NULL" if $classnum_null;
+  $extra_sql .= " (" . join(' OR ', at stm) . ') ';
 }
 
 # Filter for destination
-if (@dest && ($link eq 'cust_main' || $link eq 'both')) {
+if (@dest && $link eq 'cust_main') {
   my @stm;
   push @stm, "cust_contact.${_}_dest IS NOT NULL" for @dest;
-  push @stm, "prospect_contact.prospectnum IS NOT NULL" if $link eq 'both';
   $extra_sql .= "\nAND (".join(' OR ', at stm).') ';
 }
 
@@ -150,13 +133,14 @@ if ($DEBUG) {
   print "select \n";
   print join ",\n", at select;
   print "\n";
-  print "from contact \n";
+  print "from $link \n";
   print "$addl_from\n";
   print "WHERE \n $extra_sql\n";
   print "</pre>\n";
 }
 
 # Prepare to display phone numbers
+# adds 3 additional queries per table record :-(
 my %phonetype = (qw/1 Work 2 Home 3 Mobile 4 Fax/);
 my %phoneid   = (qw/Work 1 Home 2 Mobile 3 Fax 4/);
 my $get_phone_sub = sub {
@@ -178,13 +162,13 @@ my %classname =
 
 # And now for something completly different:
 my @report = (
-  { label => 'First', field => sub { shift->contact_first }},
-  { label => 'Last', field => sub { shift->contact_last }},
-  { label => 'Title', field => sub { shift->contact_title }},
+  { label => 'First',  field => sub { shift->contact_first }},
+  { label => 'Last',   field => sub { shift->contact_last }},
+  { label => 'Title',  field => sub { shift->contact_title }},
   { label => 'E-Mail', field => sub { shift->contact_email_emailaddress }},
-  { label => 'Work Phone', field => $get_phone_sub->('Work') },
+  { label => 'Work Phone',   field => $get_phone_sub->('Work') },
   { label => 'Mobile Phone', field => $get_phone_sub->('Mobile') },
-  { label => 'Home Phone', field => $get_phone_sub->('Home') },
+  { label => 'Home Phone',   field => $get_phone_sub->('Home') },
   { label => 'Type',
     field => sub {
       my $rec = shift;
@@ -213,9 +197,9 @@ my @report = (
   { label => 'Customer',
     link  => sub {
       my $rec = shift;
-      $rec->prospect_contact_prospectnum
-      ? ["${p}view/prospect_main.html?", 'prospect_contact_prospectnum' ]
-      : ["${p}view/cust_main.cgi?", 'cust_contact_custnum' ];
+      $rec->cust_main_custnum
+      ? ["${p}view/cust_main.cgi?", 'cust_main_custnum' ]
+      : ["${p}view/prospect_main.html?", 'prospect_main_prospectnum' ];
     },
     field => sub {
       my $rec = shift;
@@ -253,12 +237,14 @@ if (@classnum) {
   $classnum_url_part .= '&classnums=0' if $classnum_null;
 }
 
-# E-mail pipeline doesn't support mixing prospects and customers in one go
+# E-mail pipeline, from email-customers.html through to email queue job,
+# doesn't support cust_prospect table
 my $send_email_link = undef;
 if ($link eq 'cust_main') {
   $send_email_link =
     "<a href=\"${fsurl}misc/email-customers.html?".
       'table=cust_main'.
+      '&agentnum='.$cgi->param('agentnum').
       '&POST=on'.
       '&all_pkg_classnums=0'.
       '&all_tags=0'.
diff --git a/httemplate/search/elements/search.html b/httemplate/search/elements/search.html
index 209302a5d..d544c075f 100644
--- a/httemplate/search/elements/search.html
+++ b/httemplate/search/elements/search.html
@@ -119,7 +119,11 @@ Example:
                                #(query needs to be a qsearch hashref and
                                # header & fields need to be defined)
 
-    #handling agent virtualization
+    # Agent Virtualization parameters:
+    #   In this context, only available if your selected table has agentnum.
+    #   You must also include agentnum as a SELECT column in your SQL query,
+    #     or experience non-obvious problems
+    #
     'agent_virt'            => 1, # set true if this search should be
                                   # agent-virtualized
     'agent_null'            => 1, # set true to view global records always
diff --git a/httemplate/search/report_contact.html b/httemplate/search/report_contact.html
index 309f11e96..048fefb7a 100644
--- a/httemplate/search/report_contact.html
+++ b/httemplate/search/report_contact.html
@@ -4,22 +4,20 @@
 
 <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
 
-%#  This has never been actually supported on this report.
-%#  Remove the selectbox until support is implemented
-%#
-%#  <& /elements/tr-select-agent.html,
-%#                 'curr_value'    => scalar( $cgi->param('agentnum') ),
-%#                 'label'         => emt('Contacts for agent: '),
-%#                 'disable_empty' => 0,
-%#  &>
+  <& /elements/tr-select-agent.html,
+                 'curr_value'    => scalar( $cgi->param('agentnum') ),
+                 'label'         => emt('Contacts for agent: '),
+                 'disable_empty' => 0,
+  &>
 
+% # Selecting contacts and prospects at the same time has been sacrificed
+% # for agent virtualization
   <& /elements/tr-select.html,
-       'label'      => 'Contact source:', #??? not "type" - contacts have a type
+       'label'      => 'Contact source:',
        'field'      => 'link',
-       'options'    => [ 'prospect_main', 'cust_main', 'both' ],
+       'options'    => [ 'prospect_main', 'cust_main' ],
        'labels'     => { 'prospect_main' => 'Prospect contacts',
                          'cust_main'     => 'Customer contacts',
-                         'both'          => 'All contacts',
                        },
        'curr_value' => scalar( $cgi->param('link') ),
   &>

commit be6ebb643a9482bdc036e5974d1939daeeaa7379
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Sat Feb 3 20:16:47 2018 -0600

    RT# 73422 Fix report Customer Contacts

diff --git a/httemplate/elements/tr-checkbox-multiple.html b/httemplate/elements/tr-checkbox-multiple.html
index 4d754b007..baf18f916 100644
--- a/httemplate/elements/tr-checkbox-multiple.html
+++ b/httemplate/elements/tr-checkbox-multiple.html
@@ -1,3 +1,23 @@
+<%doc>
+
+Display a <tr> containing multiple checkboxes
+
+USAGE:
+
+<& /elements/tr-checkbox-multipe.html,
+  label => emt('Label'),
+  field => 'field_name',
+  options => ['opt1', 'opt2'],
+  labels => {
+    opt1 => 'Option 1',
+    opt2 => 'Option 2',
+  },
+  value => {
+    opt2 => '1', # opt2 defaults as checked
+  }
+&>
+
+</%doc>
 <% include('tr-td-label.html', @_ ) %>
 
   <TD <% $style %>>
diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html
index d25c6a091..3f8816caa 100644
--- a/httemplate/misc/email-customers.html
+++ b/httemplate/misc/email-customers.html
@@ -180,7 +180,7 @@ Template:
          # Called for each checkbox
          # Return true to default as checked, false as unchecked
          my($cgi, $name) = @_;
-         $name eq 'message'
+         exists $dest_ischecked{$name};
        },
      &>
    </div>
@@ -468,7 +468,9 @@ if ( !$cgi->param('preview') ) {
           push @contact_classname, 'Message recipients';
         } else {
           my $contact_class = FS::contact_class->by_key($1);
-          push @contact_classname, encode_entities($contact_class->classname);
+          push @contact_classname, encode_entities(
+            $contact_class ? $contact_class->classname : '(none)'
+          );
         }
       }
     }
@@ -484,19 +486,30 @@ if ( !$cgi->param('preview') ) {
 
 my @active_classes = qsearch(contact_class => {disabled => ''} );
 
+my %classnum_ischecked;
+my %dest_ischecked;
+
 $CGI::LIST_CONTEXT_WARN = 0;
-my @classnums = grep{ /^\d+$/ } $cgi->param('classnums');
+if ( my @in_classnums = $cgi->param('classnums') ) {
+  # Set checked boxes from form input
+  for my $v (@in_classnums) {
+
+    if ( $v =~ /^\d+$/ ) {
+      $classnum_ischecked{$v} = 1
+    } elsif ( $v =~ /^(invoice|message)$/ ) {
+      $dest_ischecked{$v} = 1;
+    }
 
-my %classnum_ischecked;
-if (@classnums) {
-  # values passed to form
-  $classnum_ischecked{$_} = 1 for @classnums;
+  }
 } else {
-  # default values
+  # Checked boxes default values
   $classnum_ischecked{$_->classnum} = 1 for @active_classes;
   $classnum_ischecked{0} = 1;
 }
 
+# At least one destination is required
+$dest_ischecked{message} = 1 unless %dest_ischecked;
+
 my @optin_checkboxes = (
   [ 'message' => { label => 'Message recipients' } ],
   [ 'invoice' => { label => 'Invoice recipients' } ],
diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html
index 9abbcfa1d..e02833319 100644
--- a/httemplate/search/contact.html
+++ b/httemplate/search/contact.html
@@ -1,178 +1,272 @@
 <& elements/search.html,
-  title         => 'Contacts',
+  title         => emt('Contacts'),
   name_singular => 'contact',
-  query         => { select    => join(', ', @select),
-                     table     => 'contact',
-                     addl_from => $addl_from,
-                     hashref   => \%hash,
-                     extra_sql => $extra_sql,
-                   },
-  count_query   => "SELECT COUNT(*) FROM contact $addl_from $extra_sql", #XXX
-  header        => \@header,
-  fields        => \@fields,
-  links         => \@links,
-  html_init     => $send_email_link,
+  query         => {
+    select    => join(', ', @select),
+    table     => 'contact',
+    addl_from => $addl_from,
+    hashref   => {}, #\%hash,
+    extra_sql => "WHERE $extra_sql",
+    order_by  => "ORDER BY contact_last,contact_first,contact_email_emailaddress"
+  },
+  count_query => "
+    SELECT COUNT(*)
+    FROM contact
+    $addl_from
+    WHERE $extra_sql
+  ",
+  header    => \@header,
+  fields    => \@fields,
+  links     => \@links,
+  html_init => $send_email_link,
+#  agent_virt    => 1, # Not supported unless table is cust_main/prospect_main
 &>
 <%init>
 
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('List contacts');
 
+my $DEBUG = 0;
+
 # Catch classnum values from multi-select box
 # A classnum of 0 indicates to include rows where classnum IS NULL
 $CGI::LIST_CONTEXT_WARN = 0;
 my @classnum      = grep{ /^\d+$/ && $_ > 0 } $cgi->param('classnum');
 my $classnum_null = grep{ $_ eq 0           } $cgi->param('classnum');
 
-my @select = 'contact.contactnum AS contact_contactnum'; #if we select it as bare contactnum, the multi-customer listings go away
-push @select, map "contact.$_", qw( first last title );
-my %hash = ();
-my $addl_from = '';
+# Catch destination values from dest multi-checkbox, default to message
+# irrelevant to prospect contacts
+my @dest = grep{ /^(message|invoice)$/ } $cgi->param('dest');
+ at dest = ('message') unless @dest;
 
-my $email_sub = sub {
-  my $contact = shift;
-  #can't because contactnum is in the wrong field #my @contact_email = $contact->contact_email;
-  my @contact_email = qsearch('contact_email', { 'contactnum' => $contact->contact_contactnum } );
-  join(', ', map $_->emailaddress, @contact_email);
-};
 
-my $work_phone_sub = sub {
-  my $contact = shift;
-  my $phone_type = qsearchs('phone_type', { 'typename' => 'Work' });
-  #can't because contactnum is in the wrong field
-  my @contact_workphone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } );
-  join(', ', map $_->phonenum, @contact_workphone);
-};
+# Cache the contact_class table
+my %classname =
+  map {$_->classnum => $_->classname}
+  qsearch(contact_class => {disabled => ''});
 
-my $mobile_phone_sub = sub {
-  my $contact = shift;
-  my $phone_type = qsearchs('phone_type', { 'typename' => 'Mobile' });
-  #can't because contactnum is in the wrong field
-  my @contact_mobilephone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } );
-  join(', ', map $_->phonenum, @contact_mobilephone);
-};
+# This data structure is used to generate the sql query parameters
+my %colmap = (
+  # These are included regardless of which tables we're viewing
+  common => {
+    cols => {
+      contact => [qw/first last title contactnum/],
+      contact_email => [qw/emailaddress/],
+    },
+    joinsql => "
+      LEFT JOIN contact_email
+        ON (contact.contactnum = contact_email.contactnum)
+    ",
+  },
 
-my $home_phone_sub = sub {
-  my $contact = shift;
-  my $phone_type = qsearchs('phone_type', { 'typename' => 'Home' });
-  #can't because contactnum is in the wrong field
-  my @contact_homephone = qsearch('contact_phone', { 'contactnum' => $contact->contact_contactnum, 'phonetypenum' => $phone_type->phonetypenum } );
-  join(', ', map $_->phonenum, @contact_homephone);
-};
+  # These are included if we're viewing customer records
+  customer => {
+    cols => {
+      cust_main => [qw/first last company/],
+      cust_contact => [qw/
+        custnum classnum invoice_dest message_dest selfservice_access comment
+      /],
+    },
+    joinsql => "
+      LEFT JOIN cust_contact
+        ON (contact.contactnum = cust_contact.contactnum)
+      LEFT JOIN cust_main
+        ON (cust_contact.custnum = cust_main.custnum)
+    ",
+  },
 
-my $invoice_dest_sub = sub {
-  my $contact = shift;
-  my $cust_contact = qsearchs(cust_contact => {custnum => $contact->custnum});
-  $cust_contact->invoice_dest ? 'Y' : 'N';
-};
+  # These are included if we're viewing prospect records
+  prospect => {
+    cols => {
+      prospect_main => [qw/company/],
+      prospect_contact => [qw/prospectnum classnum comment/],
+    },
+    joinsql => "
+      LEFT JOIN prospect_contact
+        ON (contact.contactnum = prospect_contact.contactnum)
+      LEFT JOIN prospect_main
+        ON (prospect_contact.prospectnum = prospect_main.prospectnum)
+    ",
+  },
+);
 
-my $message_dest_sub = sub {
-  my $contact = shift;
-  my $cust_contact = qsearchs(cust_contact => {custnum => $contact->custnum});
-  $cust_contact->message_dest ? 'Y' : 'N';
-};
+my @select;
+my $addl_from;
+my $extra_sql;
+my $hashref;
+my $link = $cgi->param('link'); # cust_main, prospect_main or both
+
+my @rectypes = ('common');
+push @rectypes,'customer' if $link eq 'cust_main'     || $link eq 'both';
+push @rectypes,'prospect' if $link eq 'prospect_main' || $link eq 'both';
 
-my $link; #for closure in this sub, we'll define it later
-my $contact_classname_sub = sub {
-  my $contact = shift;
-  my %hash = ( 'contactnum' => $contact->contact_contactnum );
-  my $X_contact;
-  if ( $link eq 'cust_main' ) {
-    $X_contact = qsearchs('cust_contact', { %hash, 'custnum' => $contact->custnum } );
-  } elsif ( $link eq 'prospect_main' ) {
-    $X_contact = qsearchs('prospect_contact', { %hash, 'prospectnum' => $contact->prospectnum } );
-  } else {
-    die "guru meditation #5555 (\$link: $link)";
+# Build @select and $addl_from
+for my $key (@rectypes) {
+  $addl_from .= $colmap{$key}->{joinsql};
+  my $cols = $colmap{$key}->{cols};
+  for my $tbl (keys %{$cols}) {
+    push @select, map{ "$tbl.$_ AS ${tbl}_$_" } @{$cols->{$tbl}};
   }
-  $X_contact->contact_classname;
-};
+}
 
-my @header = ( 'First', 'Last', 'Title', 'Email', 'Work Phone', 'Mobile Phone', 'Home Phone', 'Type', 'Invoice Destination', 'Message Destination');
-my @fields = ( 'first', 'last', 'title', $email_sub, $work_phone_sub, $mobile_phone_sub, $home_phone_sub, $contact_classname_sub, $invoice_dest_sub, $message_dest_sub );
-my @links = ( '', '', '', '', '', '', '', '', '', '');
+# Filter for custnum/prospectnum
+$extra_sql .= ' (';
+$extra_sql .= "cust_contact.custnum IS NOT NULL"
+  if $link eq 'cust_main' || $link eq 'both';
+$extra_sql .= " OR " if $link eq 'both';
+$extra_sql .= "prospect_contact.prospectnum IS NOT NULL"
+  if $link eq 'prospect_main' || $link eq 'both';
+$extra_sql .= ') ';
 
-my $company_link = '';
+# Filter for Contact Type
+if (@classnum || $classnum_null) {
+  my @stm;
 
-if ( $cgi->param('selfservice_access') eq 'Y' ) {
-  $hash{'selfservice_access'} = 'Y';
-}
+  push @stm, 'cust_contact.classnum IN ('.join(',', at classnum).')'
+    if @classnum && ($link eq 'cust_main' || $link eq 'both');
 
-my $extra_sql = '';
-$link = $cgi->param('link');
-if ( $link ) {
-
-  my $as       = ') AS prospect_or_customer';
-
-  if ( $link eq 'cust_main' ) {
-    push @header, 'Customer';
-    push @select,
-       "COALESCE( cust_main.company, cust_main.first||' '||cust_main.last $as",
-       map "cust_contact.$_", qw( custnum classnum comment selfservice_access );
-    $addl_from =
-      ' LEFT JOIN cust_contact USING ( contactnum ) '.
-      ' LEFT JOIN cust_main ON ( cust_contact.custnum = cust_main.custnum )';
-    $extra_sql = ' cust_contact.custnum IS NOT NULL ';
-    if (@classnum || $classnum_null) {
-      $extra_sql .= ' AND ( ';
-      $extra_sql .= ' cust_contact.classnum IN ('.join(',', at classnum).') '
-        if @classnum;
-      $extra_sql .= ' OR ' if $classnum_null && @classnum;
-      $extra_sql .= ' cust_contact.classnum IS NULL ' if $classnum_null;
-      $extra_sql .= ' ) ';
-    }
-    $company_link  = [ $p.'view/cust_main.cgi?', 'custnum' ];
-  } elsif ( $link eq 'prospect_main' ) {
-    push @header, 'Prospect';
-    push @select,
-      "COALESCE( prospect_main.company, contact.first||'  '||contact.last $as",
-      map "prospect_contact.$_", qw( prospectnum classnum comment );
-    $addl_from =
-      ' LEFT JOIN prospect_contact USING ( contactnum ) '.
-      ' LEFT JOIN prospect_main ON ( prospect_contact.prospectnum = prospect_main.prospectnum )';
-    $extra_sql = ' prospect_contact.prospectnum IS NOT NULL ';
-    if (@classnum || $classnum_null) {
-      $extra_sql .= ' AND ( ';
-      $extra_sql .= ' prospect_contact.classnum IN ('.join(',', at classnum).') '
-        if @classnum;
-      $extra_sql .= ' OR ' if $classnum_null && @classnum;
-      $extra_sql .= ' prospect_contact.classnum IS NULL ' if $classnum_null;
-      $extra_sql .= ' ) ';
-    }
-    $company_link  = [ $p.'view/prospect_main.html?', 'prospectnum' ];
-  } else {
-    die "don't know how to report on contacts linked to specified table";
-  }
+  push @stm, 'prospect_contact.classnum IN ('.join(',', at classnum).')'
+    if @classnum && ($link eq 'prospect_main' || $link eq 'both');
+
+  push @stm, 'cust_contact.classnum IS NULL'
+    if $classnum_null && ($link eq 'cust_main' || $link eq 'both');
+
+  push @stm, 'prospect_contact.classnum IS NULL'
+    if $classnum_null && ($link eq 'prospect_main' || $link eq 'both');
 
-  #because right now its harder to show it for both kinds of contacts
-  push @fields, 'prospect_or_customer';
-  push @links, $company_link; 
+  $extra_sql .= "\nAND (" . join(' OR ', at stm) . ') ';
+}
 
+# Filter for destination
+if (@dest && ($link eq 'cust_main' || $link eq 'both')) {
+  my @stm;
+  push @stm, "cust_contact.${_}_dest IS NOT NULL" for @dest;
+  push @stm, "prospect_contact.prospectnum IS NOT NULL" if $link eq 'both';
+  $extra_sql .= "\nAND (".join(' OR ', at stm).') ';
 }
 
-push @header, 'Self-service';
-push @fields, 'selfservice_access';
+if ($DEBUG) {
+  print "<pre>\n";
+  print "select \n";
+  print join ",\n", at select;
+  print "\n";
+  print "from contact \n";
+  print "$addl_from\n";
+  print "WHERE \n $extra_sql\n";
+  print "</pre>\n";
+}
 
-push @header, 'Comment';
-push @fields, 'comment';
+# Prepare to display phone numbers
+my %phonetype = (qw/1 Work 2 Home 3 Mobile 4 Fax/);
+my %phoneid   = (qw/Work 1 Home 2 Mobile 3 Fax 4/);
+my $get_phone_sub = sub {
+  my $type = shift;
+  return sub {
+    my $rec = shift;
+    my @p = qsearch('contact_phone', {
+      contactnum => $rec->contact_contactnum,
+      phonetypenum => $phoneid{$type}
+    });
+    @p ? (join ', ',map{$_->phonenum} @p) : undef;
+  };
+};
 
-$extra_sql = (keys(%hash) ? ' AND ' : ' WHERE '). $extra_sql
- if $extra_sql;
+# Cache contact types
+my %classname =
+  map {$_->classnum => $_->classname}
+  qsearch(contact_class => {disabled => ''});
+
+# And now for something completly different:
+my @report = (
+  { label => 'First', field => sub { shift->contact_first }},
+  { label => 'Last', field => sub { shift->contact_last }},
+  { label => 'Title', field => sub { shift->contact_title }},
+  { label => 'E-Mail', field => sub { shift->contact_email_emailaddress }},
+  { label => 'Work Phone', field => $get_phone_sub->('Work') },
+  { label => 'Mobile Phone', field => $get_phone_sub->('Mobile') },
+  { label => 'Home Phone', field => $get_phone_sub->('Home') },
+  { label => 'Type',
+    field => sub {
+      my $rec = shift;
+      if ($rec->cust_contact_custnum) {
+        return $rec->cust_contact_classnum
+               ? $classname{$rec->cust_contact_classnum}
+               : undef;
+      } else {
+        return $rec->prospect_contact_classnum
+               ? $classname{$rec->prospect_contact_classnum}
+               : undef;
+      }
+  }},
+  { label => 'Send Invoices',
+    field => sub {
+      my $rec = shift;
+      return 'N/A' if $rec->prospect_contact_prospectnum;
+      $rec->cust_contact_invoice_dest ? 'Y' : 'N';
+    }},
+  { label => 'Send Messages',
+    field => sub {
+      my $rec = shift;
+      return 'N/A' if $rec->prospect_contact_prospectnum;
+      $rec->cust_contact_message_dest ? 'Y' : 'N';
+    }},
+  { label => 'Customer',
+    link  => sub {
+      my $rec = shift;
+      $rec->prospect_contact_prospectnum
+      ? ["${p}view/prospect_main.html?", 'prospect_contact_prospectnum' ]
+      : ["${p}view/cust_main.cgi?", 'cust_contact_custnum' ];
+    },
+    field => sub {
+      my $rec = shift;
+      if ($rec->prospect_contact_prospectnum) {
+        return $rec->contact_company
+          || $rec->contact_last.' '.$rec->contact_first;
+      }
+      $rec->cust_main_company || $rec->cust_main_last.' '.$rec->cust_main_first;
+    }},
+  { label => 'Self-service',
+    field => sub {
+      my $rec = shift;
+      return 'N/A' if $rec->prospect_contact_prospectnum;
+      $rec->cust_contact_selfservice_access ? 'Y' : 'N';
+    }},
+  { label => 'Comment',
+    field => sub {
+      my $rec = shift;
+      $rec->prospect_contact_prospectnum
+      ? $rec->prospect_contact_comment
+      : $rec->cust_contact_comment;
+    }},
+);
+
+my (@header, @fields, @links);
+for my $col (@report) {
+  push @header, emt($col->{label});
+  push @fields, $col->{field};
+  push @links, ($col->{link} || "");
+}
 
 my $classnum_url_part;
 if (@classnum) {
-  $classnum_url_part = join '', map{ "&classnums=$_" } @classnum;
+  $classnum_url_part = join '', map{ "&classnums=$_" } @classnum, @dest;
   $classnum_url_part .= '&classnums=0' if $classnum_null;
 }
-my $send_email_link =
-  "<a href=\"${fsurl}misc/email-customers.html?".
-    'table=cust_main'.
-    '&POST=on'.
-    '&all_pkg_classnums=0'.
-    '&all_tags=0'.
-    '&any_pkg_status=0'.
-    '&refnum=1'.
-    '&with_email=on'.
-    $classnum_url_part.
-  "\">Email a notice to these customers</a>";
+
+# E-mail pipeline doesn't support mixing prospects and customers in one go
+my $send_email_link = undef;
+if ($link eq 'cust_main') {
+  $send_email_link =
+    "<a href=\"${fsurl}misc/email-customers.html?".
+      'table=cust_main'.
+      '&POST=on'.
+      '&all_pkg_classnums=0'.
+      '&all_tags=0'.
+      '&any_pkg_status=0'.
+      '&refnum=1'.
+      '&with_email=on'.
+      $classnum_url_part.
+    "\">Email a notice to these customers</a>";
+}
 
 </%init>
diff --git a/httemplate/search/report_contact.html b/httemplate/search/report_contact.html
index ba91b4e7e..309f11e96 100644
--- a/httemplate/search/report_contact.html
+++ b/httemplate/search/report_contact.html
@@ -4,25 +4,39 @@
 
 <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
 
-  <& /elements/tr-select-agent.html,
-                 'curr_value'    => scalar( $cgi->param('agentnum') ),
-                 'label'         => emt('Contacts for agent: '),
-                 'disable_empty' => 0,
-  &>
+%#  This has never been actually supported on this report.
+%#  Remove the selectbox until support is implemented
+%#
+%#  <& /elements/tr-select-agent.html,
+%#                 'curr_value'    => scalar( $cgi->param('agentnum') ),
+%#                 'label'         => emt('Contacts for agent: '),
+%#                 'disable_empty' => 0,
+%#  &>
 
   <& /elements/tr-select.html,
-       'label'      => 'Contact source', #??? not "type" - contacts have a type
+       'label'      => 'Contact source:', #??? not "type" - contacts have a type
        'field'      => 'link',
-       'options'    => [ 'prospect_main', 'cust_main', '' ],
+       'options'    => [ 'prospect_main', 'cust_main', 'both' ],
        'labels'     => { 'prospect_main' => 'Prospect contacts',
                          'cust_main'     => 'Customer contacts',
-                         ''              => 'All contacts',
+                         'both'          => 'All contacts',
                        },
        'curr_value' => scalar( $cgi->param('link') ),
   &>
 
+  <& /elements/tr-checkbox-multiple.html,
+    label => emt('Destinations').':',
+    field => 'dest',
+    options => [ 'message', 'invoice' ],
+    labels => {
+      invoice => 'Invoice recipients',
+      message => 'Message recipients',
+    },
+    value => { message => 1 },
+  &>
+
   <& /elements/tr-select-multiple-contact_class.html,
-    label => 'Contact Type',
+    label => emt('Contact Type').':',
     field => 'classnum',
   &>
 

commit 945bc9a8c3edf867cbaea0aa2af738d73ac180c8
Author: Mitch Jackson <mitch at freeside.biz>
Date:   Tue Jan 30 09:27:42 2018 -0600

    RT# 73422 Changes to report Customer Contacts

diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index d2c4a36ed..f35ec12f7 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -3315,14 +3315,15 @@ sub contact_list {
 
   # WHERE ...
   # AND (
-  #   ( cust_contact.classnum IN (1,2,3) )
-  #   OR
-  #   ( cust_contact.classnum IS NULL )
-  #
+  #   (
+  #     cust_contact.classnum IN (1,2,3)
+  #     OR
+  #     cust_contact.classnum IS NULL
+  #   )
   #   AND (
-  #     ( cust_contact.invoice_dest = 'Y' )
+  #     cust_contact.invoice_dest = 'Y'
   #     OR
-  #     ( cust_contact.message_dest = 'Y' )
+  #     cust_contact.message_dest = 'Y'
   #   )
   # )
 
@@ -3348,12 +3349,14 @@ sub contact_list {
     $search->{extra_sql} .= ' AND ( ';
 
       if (@or_classnum) {
-        $search->{extra_sql} .= join ' OR ', map {" ($_) "} @or_classnum;
+        $search->{extra_sql} .= ' ( ';
+        $search->{extra_sql} .= join ' OR ', map {" $_ "} @or_classnum;
+        $search->{extra_sql} .= ' ) ';
         $search->{extra_sql} .= ' AND ( ' if @and_dest;
       }
 
       if (@and_dest) {
-        $search->{extra_sql} .= join ' OR ', map {" ($_) "} @and_dest;
+        $search->{extra_sql} .= join ' OR ', map {" $_ "} @and_dest;
         $search->{extra_sql} .= ' ) ' if @or_classnum;
       }
 
diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm
index 353542df0..3e77704e6 100644
--- a/FS/FS/cust_main/Search.pm
+++ b/FS/FS/cust_main/Search.pm
@@ -1,6 +1,7 @@
 package FS::cust_main::Search;
 
 use strict;
+use Carp qw( croak );
 use base qw( Exporter );
 use vars qw( @EXPORT_OK $DEBUG $me $conf @fuzzyfields );
 use String::Approx qw(amatch);
@@ -814,15 +815,51 @@ sub search {
     unless $params->{'cancelled_pkgs'};
 
   ##
-  # "with email address(es)" checkbox
+  # "with email address(es)" checkbox,
+  #    also optionally: with_email_dest and with_contact_type
   ##
 
-  push @where,
-    'EXISTS ( SELECT 1 FROM contact_email
+  if ($params->{with_email}) {
+    my @email_dest;
+    my $email_dest_sql;
+    my $contact_type_sql;
+
+    if ($params->{with_email_dest}) {
+      croak unless ref $params->{with_email_dest} eq 'ARRAY';
+
+      @email_dest = @{$params->{with_email_dest}};
+      $email_dest_sql =
+        " AND ( ".
+        join(' OR ',map(" cust_contact.${_}_dest IS NOT NULL ", @email_dest)).
+        " ) ";
+        # Can't use message_dist = 'Y' because single quotes are escaped later
+    }
+    if ($params->{with_contact_type}) {
+      croak unless ref $params->{with_contact_type} eq 'ARRAY';
+
+      my @contact_type = grep {/^\d+$/ && $_ > 0} @{$params->{with_contact_type}};
+      my $has_null_type = 0;
+      $has_null_type = 1 if grep { $_ eq 0 } @{$params->{with_contact_type}};
+      my $hnt_sql;
+      if ($has_null_type) {
+        $hnt_sql  = ' OR ' if @contact_type;
+        $hnt_sql .= ' cust_contact.classnum IS NULL ';
+      }
+
+      $contact_type_sql =
+        " AND ( ".
+        join(' OR ', map(" cust_contact.classnum = $_ ", @contact_type)).
+        $hnt_sql.
+        " ) ";
+    }
+    push @where,
+      "EXISTS ( SELECT 1 FROM contact_email
                 JOIN cust_contact USING (contactnum)
                 WHERE cust_contact.custnum = cust_main.custnum
-            )'
-    if $params->{'with_email'};
+                $email_dest_sql
+                $contact_type_sql
+              ) ";
+  }
 
   ##
   # "with postal mail invoices" checkbox
@@ -1401,4 +1438,3 @@ L<FS::cust_main>, L<FS::Record>
 =cut
 
 1;
-
diff --git a/httemplate/elements/select-multiple-contact_class.html b/httemplate/elements/select-multiple-contact_class.html
new file mode 100644
index 000000000..81a71cc25
--- /dev/null
+++ b/httemplate/elements/select-multiple-contact_class.html
@@ -0,0 +1,21 @@
+<%doc>
+
+Display a multi-select box containing all Email Types listed in
+the contact_class table.
+
+NOTE:
+  Don't confuse "Contact Type" (contact_email.classnum) with
+  "Customer Class" (cust_main.classnum)
+
+</%doc>
+<% include( '/elements/select-table.html',
+    table        => 'contact_class',
+    hashref      => { disabled => '' },
+    name_col     => 'classname',
+    field        => 'classnum',
+    pre_options  => [ 0 => '(No Type)' ],
+    multiple     => 1,
+    all_selected => 1,
+    @_,
+  )
+%>
diff --git a/httemplate/elements/tr-select-multiple-contact_class.html b/httemplate/elements/tr-select-multiple-contact_class.html
new file mode 100644
index 000000000..5de129324
--- /dev/null
+++ b/httemplate/elements/tr-select-multiple-contact_class.html
@@ -0,0 +1,32 @@
+<%doc>
+
+  Displays Contact Types as a multi-select box.
+
+  If no non-disabled Contact Types have been defined in contact_class table,
+  renders a hidden input field with a blank value.
+
+</%doc>
+
+% if ($has_types) {
+<TR>
+  <TD ALIGN="right"><% $opt{'label'} || emt('Contact Type') %></TD>
+  <TD>
+    <% include( '/elements/select-multiple-contact_class.html', %opt ) %>
+  </TD>
+</TR>
+% } else {
+<INPUT TYPE="hidden" NAME="<% $opt{field} %>" VALUE="">
+% }
+
+<%init>
+
+my %opt = @_;
+$opt{field} ||= $opt{element_name} ||= 'classnum';
+
+my $has_types =()= qsearch({
+  table     => 'contact_class',
+  hashref   => { disabled => '' },
+  extra_sql => ' LIMIT 1 ',
+});
+
+</%init>
diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html
index b3a21767c..d25c6a091 100644
--- a/httemplate/misc/email-customers.html
+++ b/httemplate/misc/email-customers.html
@@ -174,7 +174,7 @@ Template:
      <& /elements/checkboxes.html,
        'style'               => 'display: inline; vertical-align: top',
        'disable_links'       => 1,
-       'names_list'          => \@contact_checkboxes,
+       'names_list'          => \@optin_checkboxes,
        'element_name_prefix' => 'contact_class_',
        'checked_callback'    => sub {
          # Called for each checkbox
@@ -199,6 +199,27 @@ Template:
    </div>
 % }
  </TD>
+% if (@active_classes) {
+</tr>
+<tr>
+<TD>Contact Type:</TD>
+<TD>
+  <div id="contactclassesdiv">
+    <& /elements/checkboxes.html,
+      'style'               => 'display: inline; vertical-align: top',
+      'disable_links'       => 1,
+      'names_list'          => \@classnum_checkboxes,
+      'element_name_prefix' => 'contact_class_',
+      'checked_callback'    => sub {
+        # Called for each checkbox
+        # Return true to default as checked, false as unchecked
+        my($cgi, $name) = @_;
+        exists $classnum_ischecked{$name};
+      },
+    &>
+  </div>
+</TD>
+% }
 </TR>
 </TABLE>
 <BR>
@@ -344,6 +365,21 @@ if ( !$cgi->param('preview') ) {
 
 } else {
 
+  my @checked_email_dest;
+  my @checked_contact_type;
+  for ($cgi->param) {
+    if (/^contact_class_(.+)$/) {
+      my $f = $1;
+      if ($f eq 'invoice' || $f eq 'message') {
+        push @checked_email_dest, $f;
+      } elsif ( $f =~ /^\d+$/ ) {
+        push @checked_contact_type, $f;
+      }
+    }
+  }
+  $search{with_email_dest} = \@checked_email_dest if @checked_email_dest;
+  $search{with_contact_type} = \@checked_contact_type if @checked_contact_type;
+
   my $sql_query = "FS::$table"->search(\%search);
   my $count_query = delete($sql_query->{'count_query'});
   my $count_sth = dbh->prepare($count_query)
@@ -393,6 +429,8 @@ if ( !$cgi->param('preview') ) {
     $sql_query->{'select'} = "$table.*";
     $sql_query->{'order_by'} = '';
     my $object = qsearchs($sql_query);
+    # Could use better error handling here...
+    die "No customers match the search criteria" unless ref $object;
     $cust = $object->cust_main;
     my %msgopts = (
       'cust_main' => $cust,
@@ -437,16 +475,35 @@ if ( !$cgi->param('preview') ) {
   }
 }
 
-my @contact_checkboxes = (
+# Build data structures for "Opt In" and "Contact Type" checkboxes
+#
+# By default, message recipients will be selected, this is a message.
+# By default, all Contact Types will be selected, but this may be
+#   overridden by passing 'classnums' get/post values.  If no contact
+#   types have been defined, the option will not be presented.
+
+my @active_classes = qsearch(contact_class => {disabled => ''} );
+
+$CGI::LIST_CONTEXT_WARN = 0;
+my @classnums = grep{ /^\d+$/ } $cgi->param('classnums');
+
+my %classnum_ischecked;
+if (@classnums) {
+  # values passed to form
+  $classnum_ischecked{$_} = 1 for @classnums;
+} else {
+  # default values
+  $classnum_ischecked{$_->classnum} = 1 for @active_classes;
+  $classnum_ischecked{0} = 1;
+}
+
+my @optin_checkboxes = (
   [ 'message' => { label => 'Message recipients' } ],
   [ 'invoice' => { label => 'Invoice recipients' } ],
 );
-
-foreach my $class (qsearch('contact_class', { disabled => '' })) {
-  push @contact_checkboxes, [
-    $class->classnum,
-    { label => $class->classname }
-  ];
-}
+my @classnum_checkboxes = (
+  [ '0' => { label => '(None)' }],
+  map { [ $_->classnum => {label => $_->classname} ] } @active_classes,
+);
 
 </%init>
diff --git a/httemplate/search/contact.html b/httemplate/search/contact.html
index 5f02fef2f..9abbcfa1d 100644
--- a/httemplate/search/contact.html
+++ b/httemplate/search/contact.html
@@ -11,12 +11,19 @@
   header        => \@header,
   fields        => \@fields,
   links         => \@links,
+  html_init     => $send_email_link,
 &>
 <%init>
 
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('List contacts');
 
+# Catch classnum values from multi-select box
+# A classnum of 0 indicates to include rows where classnum IS NULL
+$CGI::LIST_CONTEXT_WARN = 0;
+my @classnum      = grep{ /^\d+$/ && $_ > 0 } $cgi->param('classnum');
+my $classnum_null = grep{ $_ eq 0           } $cgi->param('classnum');
+
 my @select = 'contact.contactnum AS contact_contactnum'; #if we select it as bare contactnum, the multi-customer listings go away
 push @select, map "contact.$_", qw( first last title );
 my %hash = ();
@@ -53,6 +60,18 @@ my $home_phone_sub = sub {
   join(', ', map $_->phonenum, @contact_homephone);
 };
 
+my $invoice_dest_sub = sub {
+  my $contact = shift;
+  my $cust_contact = qsearchs(cust_contact => {custnum => $contact->custnum});
+  $cust_contact->invoice_dest ? 'Y' : 'N';
+};
+
+my $message_dest_sub = sub {
+  my $contact = shift;
+  my $cust_contact = qsearchs(cust_contact => {custnum => $contact->custnum});
+  $cust_contact->message_dest ? 'Y' : 'N';
+};
+
 my $link; #for closure in this sub, we'll define it later
 my $contact_classname_sub = sub {
   my $contact = shift;
@@ -63,14 +82,14 @@ my $contact_classname_sub = sub {
   } elsif ( $link eq 'prospect_main' ) {
     $X_contact = qsearchs('prospect_contact', { %hash, 'prospectnum' => $contact->prospectnum } );
   } else {
-    die 'guru meditation #5555';
+    die "guru meditation #5555 (\$link: $link)";
   }
   $X_contact->contact_classname;
 };
 
-my @header = ( 'First', 'Last', 'Title', 'Email', 'Work Phone', 'Mobile Phone', 'Home Phone', 'Type' );
-my @fields = ( 'first', 'last', 'title', $email_sub, $work_phone_sub, $mobile_phone_sub, $home_phone_sub, $contact_classname_sub );
-my @links = ( '', '', '', '', '', '', '', '', );
+my @header = ( 'First', 'Last', 'Title', 'Email', 'Work Phone', 'Mobile Phone', 'Home Phone', 'Type', 'Invoice Destination', 'Message Destination');
+my @fields = ( 'first', 'last', 'title', $email_sub, $work_phone_sub, $mobile_phone_sub, $home_phone_sub, $contact_classname_sub, $invoice_dest_sub, $message_dest_sub );
+my @links = ( '', '', '', '', '', '', '', '', '', '');
 
 my $company_link = '';
 
@@ -93,6 +112,14 @@ if ( $link ) {
       ' LEFT JOIN cust_contact USING ( contactnum ) '.
       ' LEFT JOIN cust_main ON ( cust_contact.custnum = cust_main.custnum )';
     $extra_sql = ' cust_contact.custnum IS NOT NULL ';
+    if (@classnum || $classnum_null) {
+      $extra_sql .= ' AND ( ';
+      $extra_sql .= ' cust_contact.classnum IN ('.join(',', at classnum).') '
+        if @classnum;
+      $extra_sql .= ' OR ' if $classnum_null && @classnum;
+      $extra_sql .= ' cust_contact.classnum IS NULL ' if $classnum_null;
+      $extra_sql .= ' ) ';
+    }
     $company_link  = [ $p.'view/cust_main.cgi?', 'custnum' ];
   } elsif ( $link eq 'prospect_main' ) {
     push @header, 'Prospect';
@@ -103,6 +130,14 @@ if ( $link ) {
       ' LEFT JOIN prospect_contact USING ( contactnum ) '.
       ' LEFT JOIN prospect_main ON ( prospect_contact.prospectnum = prospect_main.prospectnum )';
     $extra_sql = ' prospect_contact.prospectnum IS NOT NULL ';
+    if (@classnum || $classnum_null) {
+      $extra_sql .= ' AND ( ';
+      $extra_sql .= ' prospect_contact.classnum IN ('.join(',', at classnum).') '
+        if @classnum;
+      $extra_sql .= ' OR ' if $classnum_null && @classnum;
+      $extra_sql .= ' prospect_contact.classnum IS NULL ' if $classnum_null;
+      $extra_sql .= ' ) ';
+    }
     $company_link  = [ $p.'view/prospect_main.html?', 'prospectnum' ];
   } else {
     die "don't know how to report on contacts linked to specified table";
@@ -123,4 +158,21 @@ push @fields, 'comment';
 $extra_sql = (keys(%hash) ? ' AND ' : ' WHERE '). $extra_sql
  if $extra_sql;
 
+my $classnum_url_part;
+if (@classnum) {
+  $classnum_url_part = join '', map{ "&classnums=$_" } @classnum;
+  $classnum_url_part .= '&classnums=0' if $classnum_null;
+}
+my $send_email_link =
+  "<a href=\"${fsurl}misc/email-customers.html?".
+    'table=cust_main'.
+    '&POST=on'.
+    '&all_pkg_classnums=0'.
+    '&all_tags=0'.
+    '&any_pkg_status=0'.
+    '&refnum=1'.
+    '&with_email=on'.
+    $classnum_url_part.
+  "\">Email a notice to these customers</a>";
+
 </%init>
diff --git a/httemplate/search/report_contact.html b/httemplate/search/report_contact.html
index 3583bb428..ba91b4e7e 100644
--- a/httemplate/search/report_contact.html
+++ b/httemplate/search/report_contact.html
@@ -21,6 +21,11 @@
        'curr_value' => scalar( $cgi->param('link') ),
   &>
 
+  <& /elements/tr-select-multiple-contact_class.html,
+    label => 'Contact Type',
+    field => 'classnum',
+  &>
+
 </FORM>
 
 </TABLE>

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

Summary of changes:
 FS/FS/cust_main.pm                                 |  19 +-
 FS/FS/cust_main/Search.pm                          |  48 ++-
 .../elements/select-multiple-contact_class.html    |  21 ++
 httemplate/elements/tr-checkbox-multiple.html      |  20 ++
 .../elements/tr-select-multiple-contact_class.html |  32 ++
 httemplate/misc/email-customers.html               |  92 +++++-
 httemplate/search/contact.html                     | 339 +++++++++++++++------
 httemplate/search/elements/search.html             |   6 +-
 httemplate/search/report_contact.html              |  23 +-
 9 files changed, 471 insertions(+), 129 deletions(-)
 create mode 100644 httemplate/elements/select-multiple-contact_class.html
 create mode 100644 httemplate/elements/tr-select-multiple-contact_class.html




More information about the freeside-commits mailing list