contacts can be shared among customers / "duplicate contact emails", RT#27943
authorIvan Kohler <ivan@freeside.biz>
Tue, 3 Feb 2015 15:18:45 +0000 (07:18 -0800)
committerIvan Kohler <ivan@freeside.biz>
Tue, 3 Feb 2015 15:18:45 +0000 (07:18 -0800)
46 files changed:
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI_XMLRPC.pm
FS/FS/Conf.pm
FS/FS/Daemon/Preforking.pm
FS/FS/Mason.pm
FS/FS/Record.pm
FS/FS/Schema.pm
FS/FS/Upgrade.pm
FS/FS/contact.pm
FS/FS/cust_contact.pm [new file with mode: 0644]
FS/FS/cust_main.pm
FS/FS/msg_template.pm
FS/FS/o2m_Common.pm
FS/FS/part_event/Condition/cust_bill_has_service.pm
FS/FS/part_event/Condition/has_cust_tag.pm
FS/FS/part_export/amazon_ec2.pm
FS/FS/part_export/cardfortress.pm
FS/FS/phone_avail.pm
FS/FS/prospect_contact.pm [new file with mode: 0644]
FS/FS/prospect_main.pm
FS/MANIFEST
FS/bin/freeside-cdrd
FS/t/cust_contact.t [new file with mode: 0644]
FS/t/prospect_contact.t [new file with mode: 0644]
bin/cust_bill-credit_ship2
eg/table_template.pm
fs_selfservice/DEPLOY
fs_selfservice/FS-SelfService/SelfService.pm
fs_selfservice/FS-SelfService/cgi/select_cust.html [new file with mode: 0644]
fs_selfservice/FS-SelfService/cgi/selfservice.cgi
httemplate/browse/discount.html
httemplate/docs/about.html
httemplate/edit/cust_main-contacts.html
httemplate/edit/cust_main.cgi
httemplate/edit/elements/edit.html
httemplate/elements/contact.html
httemplate/elements/popup_link.html
httemplate/elements/tr-fixed.html
httemplate/elements/tr-select-contact.html
httemplate/elements/tr-select-cust_location.html
httemplate/misc/email-quotation.html
httemplate/search/contact.html
httemplate/search/cust_msg.html
httemplate/search/prospect_main.html
httemplate/view/cust_main/contacts_new.html
httemplate/view/prospect_main.html

index 8276d7e..86c7ac3 100644 (file)
@@ -46,6 +46,7 @@ use FS::payby;
 use FS::acct_rt_transaction;
 use FS::msg_template;
 use FS::contact;
+use FS::cust_contact;
 
 $DEBUG = 1;
 $me = '[FS::ClientAPI::MyAccount]';
@@ -82,7 +83,7 @@ sub skin_info {
   #return { 'error' => $session } if $context eq 'error';
 
   my $agentnum = '';
-  if ( $context eq 'customer' ) {
+  if ( $context eq 'customer' && $custnum ) {
 
     my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?')
       or die dbh->errstr;
@@ -237,7 +238,16 @@ sub login {
     return { error => 'Incorrect contact password.' }
       unless $contact->authenticate_password($p->{'password'});
 
-    $session->{'custnum'} = $contact->custnum;
+    my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+    if ( scalar(@cust_contact) == 1 ) {
+      $session->{'custnum'} = $cust_contact[0]->custnum;
+    } elsif ( scalar(@cust_contact) ) {
+      $session->{'customers'} = { map { $_->custnum => $_->cust_main->name }
+                                    @cust_contact
+                                };
+    } else {
+      return { error => 'No customer self-service access for contact' }; #??
+    }
 
   } else {
 
@@ -303,6 +313,7 @@ sub login {
 
   return { 'error'      => '',
            'session_id' => $session_id,
+           %$session,
          };
 }
 
@@ -336,6 +347,23 @@ sub switch_acct {
 
 }
 
+sub switch_cust {
+  my $p = shift;
+  my($context, $session, $custnum) = _custoragent_session_custnum($p);
+  return { 'error' => $session } if $context eq 'error';
+
+  $session->{'custnum'} = $p->{'custnum'}
+    if exists $session->{'customers'}{ $p->{'custnum'} };
+
+  my $conf = new FS::Conf;
+  my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
+  _cache->set( $p->{'session_id'}, $session, $timeout );
+
+  return { 'error'      => '',
+           %{ customer_info( { session_id=>$p->{'session_id'} } ) },
+         };
+}
+
 sub payment_gateway {
   # internal use only
   # takes a cust_main and a cust_payby entry, returns the payment_gateway
@@ -380,22 +408,23 @@ sub access_info {
   my($context, $session, $custnum) = _custoragent_session_custnum($p);
   return { 'error' => $session } if $context eq 'error';
 
-  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
-    or return { 'error' => "unknown custnum $custnum" };
+  my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
 
   $info->{'hide_payment_fields'} = [ 
     map { 
-      my $pg = payment_gateway($cust_main, $_);
+      my $pg = $cust_main && payment_gateway($cust_main, $_);
       $pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
     } @{ $info->{cust_paybys} }
   ];
 
   $info->{'self_suspend_reason'} = 
-      $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum);
+      $conf->config('selfservice-self_suspend_reason',
+                      $cust_main ? $cust_main->agentnum : ''
+                   );
 
   $info->{'edit_ticket_subject'} =
       $conf->exists('ticket_system-selfservice_edit_subject') && 
-      $cust_main->edit_subject;
+      $cust_main && $cust_main->edit_subject;
 
   $info->{'timeout'} = $conf->config('selfservice-timeout') || 3600;
 
@@ -432,7 +461,7 @@ sub customer_info {
     my $search = { 'custnum' => $custnum };
     $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
     my $cust_main = qsearchs('cust_main', $search )
-      or return { 'error' => "unknown custnum $custnum" };
+      or return { 'error' => "customer_info: unknown custnum $custnum" };
 
     my $list_tickets = list_tickets($p);
     $return{'tickets'} = $list_tickets->{'tickets'};
@@ -536,7 +565,7 @@ sub customer_info_short {
     my $search = { 'custnum' => $custnum };
     $search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
     my $cust_main = qsearchs('cust_main', $search )
-      or return { 'error' => "unknown custnum $custnum" };
+      or return { 'error' => "customer_info_short: unknown custnum $custnum" };
 
     $return{display_custnum} = $cust_main->display_custnum;
 
@@ -2916,7 +2945,12 @@ sub myaccount_passwd {
   #need to support the "ISP provides email that's used as a contact email" case
   #as well as we can.
   my $contact = FS::contact->by_selfservice_email($svc_acct->email);
-  if ( $contact && $contact->custnum == $custnum ) {
+  if ( $contact && qsearchs('cust_contact', { contactnum=> $contact->contactnum,
+                                              custnum   => $custnum,
+                                              selfservice_access => 'Y',
+                                            }
+                           )
+  ) {
     #svc_acct was successful but this one returns an error?  "shouldn't happen"
     $error ||= $contact->change_password($p->{'new_password'});
   }
@@ -2993,7 +3027,10 @@ sub reset_passwd {
   
     $contact = FS::contact->by_selfservice_email($p->{'email'});
 
-    $cust_main = $contact->cust_main if $contact;
+    if ( $contact ) {
+      my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+      $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
+    }
 
     #also look for an svc_acct, otherwise it would be super confusing
 
@@ -3035,6 +3072,9 @@ sub reset_passwd {
 
   }
 
+  return { %$info, 'error' => 'Multi-customer contacts incompatible with customer-based verification' }
+    if ! $cust_main && $verification ne 'email';
+
   my %verify = (
     'email'   => sub { 1; },
     'paymask' => sub { 
@@ -3157,7 +3197,9 @@ sub check_reset_passwd {
     my @contact_email = $contact->contact_email;
     return { 'error' => 'No contact email' } unless @contact_email;
 
-    $p->{'agentnum'} = $contact->cust_main->agentnum;
+    my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+    $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
+      if scalar(@cust_contact) == 1;
     my $info = skin_info($p);
 
     return { %$info,
@@ -3207,7 +3249,9 @@ sub process_reset_passwd {
     $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
       or return { 'error' => "Contact not found" };
 
-    $p->{'agentnum'} ||= $contact->cust_main->agentnum;
+    my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+    $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
+      if scalar(@cust_contact) == 1;
     $info ||= skin_info($p);
 
   }
index 62f61d6..952b199 100644 (file)
@@ -102,6 +102,7 @@ sub ss2clientapi {
   'login'                     => 'MyAccount/login',
   'logout'                    => 'MyAccount/logout',
   'switch_acct'               => 'MyAccount/switch_acct',
+  'switch_cust'               => 'MyAccount/switch_cust',
   'customer_info'             => 'MyAccount/customer_info',
   'customer_info_short'       => 'MyAccount/customer_info_short',
   'billing_history'           => 'MyAccount/billing_history',
index 029f1a1..2b959e6 100644 (file)
@@ -2996,7 +2996,7 @@ and customer address. Include units.',
     'type'        => 'select',
     'select_hash' => [ '' => 'Password reset disabled',
                        'email' => 'Click on a link in email',
-                       'paymask,amount,zip' => 'Click on a link in email, and also verify with credit card (or bank account) last 4 digits, payment amount and zip code',
+                       'paymask,amount,zip' => 'Click on a link in email, and also verify with credit card (or bank account) last 4 digits, payment amount and zip code.  Note: Do not use if you have multi-customer contacts, as they will be unable to reset their passwords.',
                      ],
   },
 
index 98b4fa6..f3a39a6 100644 (file)
@@ -96,6 +96,7 @@ sub daemon_run {
   #parent doesn't need to hold a DB connection open
   dbh->disconnect;
   undef $FS::UID::dbh;
+  undef $RT::Handle;
 
   server_spawn(MAX_PROCESSES);
   POE::Kernel->run();
index d3e45df..37e3ad2 100644 (file)
@@ -396,6 +396,9 @@ if ( -e $addl_handler_use_file ) {
   use FS::circuit_provider;
   use FS::circuit_termination;
   use FS::svc_circuit;
+  use FS::cust_credit_source_bill_pkg;
+  use FS::prospect_contact;
+  use FS::cust_contact;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index f8282c0..92fb896 100644 (file)
@@ -876,6 +876,7 @@ sub qsearchs { # $result_record = &FS::Record:qsearchs('table',\%hash);
   my $table = $_[0];
   my(@result) = qsearch(@_);
   cluck "warning: Multiple records in scalar search ($table)"
+        #.join(' / ', map "$_=>".$_[1]->{$_}, keys %{ $_[1] } )
     if scalar(@result) > 1;
   #should warn more vehemently if the search was on a primary key?
   scalar(@result) ? ($result[0]) : ();
index d5ed1b7..133b6d8 100644 (file)
@@ -1740,20 +1740,69 @@ sub tables_hashref {
       'index' => [ ['disabled'] ],
     },
 
+    'cust_contact' => {
+      'columns' => [
+        'custcontactnum',     'serial',     '',  '', '', '',
+        'custnum',               'int',     '',  '', '', '',
+        'contactnum',            'int',     '',  '', '', '',
+        'classnum',              'int', 'NULL',  '', '', '',
+        'comment',           'varchar', 'NULL', 255, '', '',
+        'selfservice_access',   'char', 'NULL',   1, '', '',
+      ],
+      'primary_key'  => 'custcontactnum',
+      'unique'       => [ [ 'custnum', 'contactnum' ], ],
+      'index'        => [ [ 'custnum' ], [ 'contactnum' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'custnum' ],
+                            table      => 'cust_main',
+                          },
+                          { columns    => [ 'contactnum' ],
+                            table      => 'contact',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'contact_class',
+                          },
+                        ],
+    },
+
+    'prospect_contact' => {
+      'columns' => [
+        'prospectcontactnum', 'serial',     '',  '', '', '',
+        'prospectnum',       'int',     '',  '', '', '',
+        'contactnum',        'int',     '',  '', '', '',
+        'classnum',          'int', 'NULL',  '', '', '',
+        'comment',       'varchar', 'NULL', 255, '', '',
+      ],
+      'primary_key'  => 'prospectcontactnum',
+      'unique'       => [ [ 'prospectnum', 'contactnum' ], ],
+      'index'        => [ [ 'prospectnum' ], [ 'contactnum' ], ],
+      'foreign_keys' => [
+                          { columns    => [ 'prospectnum' ],
+                            table      => 'prospect_main',
+                          },
+                          { columns    => [ 'contactnum' ],
+                            table      => 'contact',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'contact_class',
+                          },
+                        ],
+    },
+
     'contact' => {
       'columns' => [
         'contactnum', 'serial',     '',      '', '', '',
-        'prospectnum',   'int', 'NULL',      '', '', '',
-        'custnum',       'int', 'NULL',      '', '', '',
+        'prospectnum',   'int', 'NULL',      '', '', '', #deprecated, now prospect_contact table
+        'custnum',       'int', 'NULL',      '', '', '', #deprecated, now cust_contact table
         'locationnum',   'int', 'NULL',      '', '', '', #not yet
-        'classnum',      'int', 'NULL',      '', '', '',
+        'classnum',      'int', 'NULL',      '', '', '', #deprecated, now prospect_contact or cust_contact
 #        'titlenum',      'int', 'NULL',      '', '', '', #eg Mr. Mrs. Dr. Rev.
         'last',      'varchar',     '', $char_d, '', '', 
 #        'middle',    'varchar', 'NULL', $char_d, '', '', 
         'first',     'varchar',     '', $char_d, '', '', 
         'title',     'varchar', 'NULL', $char_d, '', '', #eg Head Bottle Washer
-        'comment',   'varchar', 'NULL',     255, '', '', 
-        'selfservice_access',    'char', 'NULL',       1, '', '',
+        'comment',   'varchar', 'NULL',     255, '', '',  #depredated, now prospect_contact or cust_contact
+        'selfservice_access',    'char', 'NULL',       1, '', '', #deprecated, now cust_contact
         '_password',          'varchar', 'NULL', $char_d, '', '',
         '_password_encoding', 'varchar', 'NULL', $char_d, '', '',
         'disabled',              'char', 'NULL',       1, '', '', 
index 4719caa..d05b309 100644 (file)
@@ -312,6 +312,9 @@ sub upgrade_data {
     #cust_main (remove paycvv from history)
     'cust_main' => [],
 
+    #contact -> cust_contact / prospect_contact
+    'contact' => [],
+
     #msgcat
     'msgcat' => [],
 
index 437fd16..589fc7c 100644 (file)
@@ -3,12 +3,15 @@ use base qw( FS::Record );
 
 use strict;
 use vars qw( $skip_fuzzyfiles );
+use Carp;
 use Scalar::Util qw( blessed );
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::contact_phone;
 use FS::contact_email;
 use FS::queue;
 use FS::phone_type; #for cgi_contact_fields
+use FS::cust_contact;
+use FS::prospect_contact;
 
 $skip_fuzzyfiles = 0;
 
@@ -123,10 +126,88 @@ sub insert {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my $error = $self->SUPER::insert;
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return $error;
+  #save off and blank values that move to cust_contact / prospect_contact now
+  my $prospectnum = $self->prospectnum;
+  $self->prospectnum('');
+  my $custnum = $self->custnum;
+  $self->custnum('');
+
+  my %link_hash = ();
+  for (qw( classnum comment selfservice_access )) {
+    $link_hash{$_} = $self->get($_);
+    $self->$_('');
+  }
+
+  #look for an existing contact with this email address
+  my $existing_contact = '';
+  if ( $self->get('emailaddress') =~ /\S/ ) {
+  
+    my %existing_contact = ();
+
+    foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
+      my $contact_email = qsearchs('contact_email', { emailaddress=>$email } )
+        or next;
+
+      my $contact = $contact_email->contact;
+      $existing_contact{ $contact->contactnum } = $contact;
+
+    }
+
+    if ( scalar( keys %existing_contact ) > 1 ) {
+      $dbh->rollback if $oldAutoCommit;
+      return 'Multiple email addresses specified '.
+             ' that already belong to separate contacts';
+    } elsif ( scalar( keys %existing_contact ) ) {
+      ($existing_contact) = values %existing_contact;
+    }
+
+  }
+
+  if ( $existing_contact ) {
+
+    $self->$_($existing_contact->$_())
+      for qw( contactnum _password _password_encoding );
+    $self->SUPER::replace($existing_contact);
+
+  } else {
+
+    my $error = $self->SUPER::insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+
+  }
+
+  my $cust_contact = '';
+  if ( $custnum ) {
+    my %hash = ( 'contactnum' => $self->contactnum,
+                 'custnum'    => $custnum,
+               );
+    $cust_contact =  qsearchs('cust_contact', \%hash )
+                  || new FS::cust_contact { %hash, %link_hash };
+    my $error = $cust_contact->custcontactnum ? $cust_contact->replace
+                                              : $cust_contact->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  if ( $prospectnum ) {
+    my %hash = ( 'contactnum'  => $self->contactnum,
+                 'prospectnum' => $prospectnum,
+               );
+    my $prospect_contact =  qsearchs('prospect_contact', \%hash )
+                         || new FS::prospect_contact { %hash, %link_hash };
+    my $error =
+      $prospect_contact->prospectcontactnum ? $prospect_contact->replace
+                                            : $prospect_contact->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
   }
 
   foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
@@ -134,12 +215,14 @@ sub insert {
     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
     my $phonetypenum = $1;
 
-    my $contact_phone = new FS::contact_phone {
-      'contactnum' => $self->contactnum,
-      'phonetypenum' => $phonetypenum,
-      _parse_phonestring( $self->get($pf) ),
-    };
-    $error = $contact_phone->insert;
+    my %hash = ( 'contactnum'   => $self->contactnum,
+                 'phonetypenum' => $phonetypenum,
+               );
+    my $contact_phone =
+      qsearchs('contact_phone', \%hash)
+        || new FS::contact_phone { %hash, _parse_phonestring($self->get($pf)) };
+    my $error = $contact_phone->contactphonenum ? $contact_phone->replace
+                                                : $contact_phone->insert;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
@@ -149,17 +232,18 @@ sub insert {
   if ( $self->get('emailaddress') =~ /\S/ ) {
 
     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
-      my $contact_email = new FS::contact_email {
+      my %hash = (
         'contactnum'   => $self->contactnum,
         'emailaddress' => $email,
-      };
-      $error = $contact_email->insert;
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return $error;
+      );
+      unless ( qsearchs('contact_email', \%hash) ) {
+        my $contact_email = new FS::contact_email \%hash;
+        my $error = $contact_email->insert;
+        if ( $error ) {
+          $dbh->rollback if $oldAutoCommit;
+          return $error;
+        }
       }
-
     }
 
   }
@@ -167,14 +251,17 @@ sub insert {
   unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
     #warn "  queueing fuzzyfiles update\n"
     #  if $DEBUG > 1;
-    $error = $self->queue_fuzzyfiles_update;
+    my $error = $self->queue_fuzzyfiles_update;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "updating fuzzy search cache: $error";
     }
   }
 
-  if ( $self->selfservice_access ) {
+  if (      $link_hash{'selfservice_access'} eq 'R'
+       or ( $link_hash{'selfservice_access'} && $cust_contact )
+     )
+  {
     my $error = $self->send_reset_email( queue=>1 );
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
@@ -208,6 +295,44 @@ sub delete {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  #got a prospetnum or custnum? delete the prospect_contact or cust_contact link
+
+  if ( $self->prospectnum ) {
+    my $prospect_contact = qsearchs('prospect_contact', {
+                             'contactnum'  => $self->contactnum,
+                             'prospectnum' => $self->prospectnum,
+                           });
+    my $error = $prospect_contact->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  if ( $self->custnum ) {
+    my $cust_contact = qsearchs('cust_contact', {
+                         'contactnum'  => $self->contactnum,
+                         'custnum' => $self->custnum,
+                       });
+    my $error = $cust_contact->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  # then, proceed with deletion only if the contact isn't attached to any other
+  # prospects or customers
+
+  #inefficient, but how many prospects/customers can a single contact be
+  # attached too?  (and is removing them from one a common operation?)
+  if ( $self->prospect_contact || $self->cust_contact ) {
+    $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+    return '';
+  }
+
+  #proceed with deletion
+
   foreach my $cust_pkg ( $self->cust_pkg ) {
     $cust_pkg->contactnum('');
     my $error = $cust_pkg->replace;
@@ -262,12 +387,61 @@ sub replace {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  #save off and blank values that move to cust_contact / prospect_contact now
+  my $prospectnum = $self->prospectnum;
+  $self->prospectnum('');
+  my $custnum = $self->custnum;
+  $self->custnum('');
+
+  my %link_hash = ();
+  for (qw( classnum comment selfservice_access )) {
+    $link_hash{$_} = $self->get($_);
+    $self->$_('');
+  }
+
   my $error = $self->SUPER::replace($old);
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   }
 
+  my $cust_contact = '';
+  if ( $custnum ) {
+    my %hash = ( 'contactnum' => $self->contactnum,
+                 'custnum'    => $custnum,
+               );
+    my $error;
+    if ( $cust_contact = qsearchs('cust_contact', \%hash ) ) {
+      $cust_contact->$_($link_hash{$_}) for keys %link_hash;
+      $error = $cust_contact->replace;
+    } else {
+      $cust_contact = new FS::cust_contact { %hash, %link_hash };
+      $error = $cust_contact->insert;
+    }
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  if ( $prospectnum ) {
+    my %hash = ( 'contactnum'  => $self->contactnum,
+                 'prospectnum' => $prospectnum,
+               );
+    my $error;
+    if ( my $prospect_contact = qsearchs('prospect_contact', \%hash ) ) {
+      $prospect_contact->$_($link_hash{$_}) for keys %link_hash;
+      $error = $prospect_contact->replace;
+    } else {
+      my $prospect_contact = new FS::prospect_contact { %hash, %link_hash };
+      $error = $prospect_contact->insert;
+    }
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
   foreach my $pf ( grep { /^phonetypenum(\d+)$/ }
                         keys %{ $self->hashref } ) {
     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
@@ -278,7 +452,7 @@ sub replace {
              );
     my $contact_phone = qsearchs('contact_phone', \%cp);
 
-    # if new value is empty, delete old entry
+    #if new value is empty, delete old entry
     if (!$self->get($pf)) {
       if ($contact_phone) {
         $error = $contact_phone->delete;
@@ -342,11 +516,14 @@ sub replace {
     }
   }
 
-  if (    ( $old->selfservice_access eq '' && $self->selfservice_access
-              && ! $self->_password
-          )
-       || $self->_resend()
-     )
+  if ( $cust_contact and (
+                              (      $cust_contact->selfservice_access eq ''
+                                  && $link_hash{selfservice_access}
+                                  && ! length($self->_password)
+                              )
+                           || $cust_contact->_resend()
+                         )
+    )
   {
     my $error = $self->send_reset_email( queue=>1 );
     if ( $error ) {
@@ -463,7 +640,6 @@ sub check {
   ;
   return $error if $error;
 
-  return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
 
   return "One of first name, last name, or title must have a value"
@@ -500,17 +676,35 @@ sub firstlast {
   $self->first . ' ' . $self->last;
 }
 
-=item contact_classname
-
-Returns the name of this contact's class (see L<FS::contact_class>).
-
-=cut
-
-sub contact_classname {
-  my $self = shift;
-  my $contact_class = $self->contact_class or return '';
-  $contact_class->classname;
-}
+#=item contact_classname PROSPECT_OBJ | CUST_MAIN_OBJ
+#
+#Returns the name of this contact's class for the specified prospect or
+#customer (see L<FS::prospect_contact>, L<FS::cust_contact> and
+#L<FS::contact_class>).
+#
+#=cut
+#
+#sub contact_classname {
+#  my( $self, $prospect_or_cust ) = @_;
+#
+#  my $link = '';
+#  if ( ref($prospect_or_cust) eq 'FS::prospect_main' ) {
+#    $link = qsearchs('prospect_contact', {
+#              'contactnum'  => $self->contactnum,
+#              'prospectnum' => $prospect_or_cust->prospectnum,
+#            });
+#  } elsif ( ref($prospect_or_cust) eq 'FS::cust_main' ) {
+#    $link = qsearchs('cust_contact', {
+#              'contactnum'  => $self->contactnum,
+#              'custnum'     => $prospect_or_cust->custnum,
+#            });
+#  } else {
+#    croak "$prospect_or_cust is not an FS::prospect_main or FS::cust_main object";
+#  }
+#
+#  my $contact_class = $link->contact_class or return '';
+#  $contact_class->classname;
+#}
 
 =item by_selfservice_email EMAILADDRESS
 
@@ -527,8 +721,7 @@ sub by_selfservice_email {
     'table'     => 'contact_email',
     'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
     'hashref'   => { 'emailaddress' => $email, },
-    'extra_sql' => " AND selfservice_access = 'Y' ".
-                   " AND ( disabled IS NULL OR disabled = '' )",
+    'extra_sql' => " AND ( disabled IS NULL OR disabled = '' )",
   }) or return '';
 
   $contact_email->contact;
@@ -629,10 +822,12 @@ sub send_reset_email {
 
   my $conf = new FS::Conf;
 
-  my $cust_main = $self->cust_main
-    or die "no customer"; #reset a password for a prospect contact?  someday
+  my $cust_main = '';
+  my @cust_contact = grep $_->selfservice_access, $self->cust_contact;
+  $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
 
-  my $msgnum = $conf->config('selfservice-password_reset_msgnum', $cust_main->agentnum);
+  my $agentnum = $cust_main ? $cust_main->agentnum : '';
+  my $msgnum = $conf->config('selfservice-password_reset_msgnum', $agentnum);
   #die "selfservice-password_reset_msgnum unset" unless $msgnum;
   return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum;
   my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
@@ -647,7 +842,7 @@ sub send_reset_email {
 
     my $queue = new FS::queue {
       'job'     => 'FS::Misc::process_send_email',
-      'custnum' => $cust_main->custnum,
+      'custnum' => $cust_main ? $cust_main->custnum : '',
     };
     $queue->insert( $msg_template->prepare( %msg_template ) );
 
@@ -690,7 +885,21 @@ sub cgi_contact_fields {
 
 }
 
-use FS::phone_type;
+use FS::upgrade_journal;
+sub _upgrade_data { #class method
+  my ($class, %opts) = @_;
+
+  unless ( FS::upgrade_journal->is_done('contact__DUPEMAIL') ) {
+
+    foreach my $contact (qsearch('contact', {})) {
+      my $error = $contact->replace;
+      die $error if $error;
+    }
+
+    FS::upgrade_journal->set_done('contact__DUPEMAIL');
+  }
+
+}
 
 =back
 
diff --git a/FS/FS/cust_contact.pm b/FS/FS/cust_contact.pm
new file mode 100644 (file)
index 0000000..6f899d8
--- /dev/null
@@ -0,0 +1,146 @@
+package FS::cust_contact;
+use base qw( FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::cust_contact - Object methods for cust_contact records
+
+=head1 SYNOPSIS
+
+  use FS::cust_contact;
+
+  $record = new FS::cust_contact \%hash;
+  $record = new FS::cust_contact { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_contact object represents a contact's attachment to a specific
+customer.  FS::cust_contact inherits from FS::Record.  The following fields are
+currently supported:
+
+=over 4
+
+=item custcontactnum
+
+primary key
+
+=item custnum
+
+custnum
+
+=item contactnum
+
+contactnum
+
+=item classnum
+
+classnum
+
+=item comment
+
+comment
+
+=item selfservice_access
+
+empty or Y
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'cust_contact'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  if ( $self->selfservice_access eq 'R' ) {
+    $self->selfservice_access('Y');
+    $self->_resend('Y');
+  }
+
+  my $error = 
+    $self->ut_numbern('custcontactnum')
+    || $self->ut_number('custnum')
+    || $self->ut_number('contactnum')
+    || $self->ut_numbern('classnum')
+    || $self->ut_textn('comment')
+    || $self->ut_enum('selfservice_access', [ '', 'Y' ])
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item contact_classname
+
+Returns the name of this contact's class (see L<FS::contact_class>).
+
+=cut
+
+sub contact_classname {
+  my $self = shift;
+  my $contact_class = $self->contact_class or return '';
+  $contact_class->classname;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::contact>, L<FS::cust_main>, L<FS::Record>
+
+=cut
+
+1;
+
index d6f1a31..cd675f9 100644 (file)
@@ -71,7 +71,7 @@ use FS::agent_payment_gateway;
 use FS::banned_pay;
 use FS::cust_main_note;
 use FS::cust_attachment;
-use FS::contact;
+use FS::cust_contact;
 use FS::Locales;
 use FS::upgrade_journal;
 use FS::sales;
@@ -529,11 +529,23 @@ sub insert {
       return $error;
     }
 
-    my @contact = $prospect_main->contact;
+    foreach my $prospect_contact ( $prospect_main->prospect_contact ) {
+      my $cust_contact = new FS::cust_contact {
+        'custnum' => $self->custnum,
+        map { $_ => $prospect_contact->$_() } qw( contactnum classnum comment )
+      };
+      my $error =  $cust_contact->insert
+                || $prospect_contact->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
     my @cust_location = $prospect_main->cust_location;
     my @qual = $prospect_main->qual;
 
-    foreach my $r ( @contact, @cust_location, @qual ) {
+    foreach my $r ( @cust_location, @qual ) {
       $r->prospectnum('');
       $r->custnum($self->custnum);
       my $error = $r->replace;
@@ -1915,14 +1927,13 @@ sub cust_location {
 
 =item cust_contact
 
-Returns all contacts (see L<FS::contact>) for this customer.
+Returns all contact associations (see L<FS::cust_contact>) for this customer.
 
 =cut
 
-#already used :/ sub contact {
 sub cust_contact {
   my $self = shift;
-  qsearch('contact', { 'custnum' => $self->custnum } );
+  qsearch('cust_contact', { 'custnum' => $self->custnum } );
 }
 
 =item cust_payby
@@ -3656,9 +3667,11 @@ sub service_contact {
     my $classnum = $self->scalar_sql(
       'SELECT classnum FROM contact_class WHERE classname = \'Service\''
     ) || 0; #if it's zero, qsearchs will return nothing
-    $self->{service_contact} = qsearchs('contact', { 
-        'classnum' => $classnum, 'custnum' => $self->custnum
-      }) || undef;
+    my $cust_contact = qsearchs('cust_contact', { 
+        'classnum' => $classnum,
+        'custnum'  => $self->custnum,
+    });
+    $self->{service_contact} = $cust_contact->contact if $cust_contact;
   }
   $self->{service_contact};
 }
@@ -4614,6 +4627,42 @@ sub _agent_plandata {
 
 }
 
+sub process_o2m_qsearch {
+  my $self = shift;
+  my $table = shift;
+  return qsearch($table, @_) unless $table eq 'contact';
+
+  my $hashref = shift;
+  my %hash = %$hashref;
+  ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
+    or die 'guru meditation #4343';
+
+  qsearch({ 'table'     => 'contact',
+            'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
+            'hashref'   => \%hash,
+            'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
+                           " cust_contact.custnum = $custnum "
+         });                
+}
+
+sub process_o2m_qsearchs {
+  my $self = shift;
+  my $table = shift;
+  return qsearchs($table, @_) unless $table eq 'contact';
+
+  my $hashref = shift;
+  my %hash = %$hashref;
+  ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
+    or die 'guru meditation #2121';
+
+  qsearchs({ 'table'     => 'contact',
+             'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
+             'hashref'   => \%hash,
+             'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
+                            " cust_contact.custnum = $custnum "
+          });                
+}
+
 =item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
 
 Subroutine (not a method), designed to be called from the queue.
index f45fb2a..94d478f 100644 (file)
@@ -278,16 +278,17 @@ A hash reference of additional substitutions
 sub prepare {
   my( $self, %opt ) = @_;
 
-  my $cust_main = $opt{'cust_main'} or die 'cust_main required';
+  my $cust_main = $opt{'cust_main'}; # or die 'cust_main required';
   my $object = $opt{'object'} or die 'object required';
 
   # localization
-  my $locale = $cust_main->locale || '';
+  my $locale = $cust_main && $cust_main->locale || '';
   warn "no locale for cust#".$cust_main->custnum."; using default content\n"
-    if $DEBUG and !$locale;
-  my $content = $self->content($cust_main->locale);
-  warn "preparing template '".$self->msgname."' to cust#".$cust_main->custnum."\n"
-    if($DEBUG);
+    if $DEBUG and $cust_main && !$locale;
+  my $content = $self->content($locale);
+
+  warn "preparing template '".$self->msgname."\n"
+    if $DEBUG;
 
   my $subs = $self->substitutions;
 
@@ -295,7 +296,8 @@ sub prepare {
   # create substitution table
   ###  
   my %hash;
-  my @objects = ($cust_main);
+  my @objects = ();
+  push @objects, $cust_main if $cust_main;
   my @prefixes = ('');
   my $svc;
   if( ref $object ) {
@@ -385,20 +387,22 @@ sub prepare {
   my @to;
   if ( exists($opt{'to'}) ) {
     @to = split(/\s*,\s*/, $opt{'to'});
-  }
-  else {
+  } elsif ( $cust_main ) {
     @to = $cust_main->invoicing_list_emailonly;
+  } else {
+    die 'no To: address or cust_main object specified';
   }
-  # no warning when preparing with no destination
 
   my $from_addr = $self->from_addr;
 
   if ( !$from_addr ) {
+
+    my $agentnum = $cust_main ? $cust_main->agentnum : '';
+
     if ( $opt{'from_config'} ) {
-      $from_addr = scalar( $conf->config($opt{'from_config'}, 
-                                         $cust_main->agentnum) );
+      $from_addr = $conf->config($opt{'from_config'}, $agentnum);
     }
-    $from_addr ||= $conf->invoice_from_full($cust_main->agentnum);
+    $from_addr ||= $conf->invoice_from_full($agentnum);
   }
 #  my @cust_msg = ();
 #  if ( $conf->exists('log_sent_mail') and !$opt{'preview'} ) {
@@ -416,11 +420,11 @@ sub prepare {
                       ->format( HTML::TreeBuilder->new_from_content($body) )
                   );
   (
-    'custnum' => $cust_main->custnum,
-    'msgnum'  => $self->msgnum,
-    'from' => $from_addr,
-    'to'   => \@to,
-    'bcc'  => $self->bcc_addr || undef,
+    'custnum'   => ( $cust_main ? $cust_main->custnum : ''),
+    'msgnum'    => $self->msgnum,
+    'from'      => $from_addr,
+    'to'        => \@to,
+    'bcc'       => $self->bcc_addr || undef,
     'subject'   => $subject,
     'html_body' => $body,
     'text_body' => $text_body
index 0e03b52..4848649 100644 (file)
@@ -87,7 +87,7 @@ sub process_o2m {
 
   foreach my $del_obj (
     grep { ! $edits{$_->$table_pkey()} }
-         qsearch( $table, $hashref )
+         $self->process_o2m_qsearch( $table, $hashref )
   ) {
     my $error = $del_obj->delete;
     if ( $error ) {
@@ -97,7 +97,7 @@ sub process_o2m {
   }
 
   foreach my $pkey_value ( keys %edits ) {
-    my $old_obj = qsearchs( $table, { %$hashref, $table_pkey => $pkey_value } ),
+    my $old_obj = $self->process_o2m_qsearchs( $table, { %$hashref, $table_pkey => $pkey_value } );
     my $add_param = $edits{$pkey_value};
     my %hash = ( $table_pkey => $pkey_value,
                  map { $_ => $opt{'params'}->{$add_param."_$_"} }
@@ -131,6 +131,9 @@ sub process_o2m {
   '';
 }
 
+sub process_o2m_qsearch  { shift->qsearch( @_  ); }
+sub process_o2m_qsearchs { shift->qsearchs( @_ ); }
+
 sub _load_table {
   my( $self, $table ) = @_;
   eval "use FS::$table";
index 6e981ee..898b08d 100644 (file)
@@ -44,13 +44,13 @@ sub condition_sql {
   my $servicenums =
     $class->condition_sql_option_option_integer('has_service');
 
-  my $sql = qq| 0 < ( SELECT COUNT(cs.svcpart)
+  my $sql = " 0 < ( SELECT COUNT(cs.svcpart)
      FROM cust_bill_pkg cbp, cust_svc cs
     WHERE cbp.invnum = cust_bill.invnum
       AND cs.pkgnum = cbp.pkgnum
       AND cs.svcpart IN $servicenums
   )
-  |;
+  ";
   return $sql;
 }
 
index cde9338..79bf2d3 100644 (file)
@@ -16,7 +16,6 @@ sub eventtable_hashref {
     };
 }
 
-#something like this
 sub option_fields {
   (
     'tagnum'  => { 'label'    => 'Customer tag',
index 06e2c23..c1082a8 100644 (file)
@@ -8,10 +8,12 @@ use FS::Record qw( qsearchs );
 use FS::svc_external;
 
 tie my %options, 'Tie::IxHash',
-  'access_key' => { label => 'AWS access key', },
-  'secret_key' => { label => 'AWS secret key', },
-  'ami'        => { label => 'AMI', 'default' => 'ami-ff46a796', },
-  'keyname'    => { label => 'Keypair name', },
+  'access_key'   => { label => 'AWS access key', },
+  'secret_key'   => { label => 'AWS secret key', },
+  'ami'          => { label => 'AMI', 'default' => 'ami-ff46a796', },
+  'keyname'      => { label => 'Keypair name', },
+  'region'       => { label => 'Region', },
+  'InstanceType' => { label => 'Instance Type', },
   #option to turn off (or on) ip address allocation
 ;
 
@@ -38,6 +40,7 @@ sub _export_insert {
     $svc_external->svcnum,
     $self->option('ami'),
     $self->option('keyname'),
+    $self->option('InstanceType'),
   );
   ref($err_or_queue) ? '' : $err_or_queue;
 }
@@ -96,31 +99,35 @@ sub amazon_ec2_queue {
   };
   $queue->insert( $self->option('access_key'),
                   $self->option('secret_key'),
+                  $self->option('region'),
                   @_
                 )
     or $queue;
 }
 
 sub amazon_ec2_new {
-  my( $access_key, $secret_key, @rest ) = @_;
+  my( $access_key, $secret_key, $region, @rest ) = @_;
 
   eval 'use Net::Amazon::EC2;';
   die $@ if $@;
 
   my $ec2 = new Net::Amazon::EC2 'AWSAccessKeyId'  => $access_key,
-                                 'SecretAccessKey' => $secret_key;
-
+                                 'SecretAccessKey' => $secret_key,
+                                 'region'          => $region || 'us-east-1',
+                                ;
   ( $ec2, @rest );
 }
 
 sub amazon_ec2_insert { #subroutine, not method
-  my( $ec2, $svcnum, $ami, $keyname ) = amazon_ec2_new(@_);
-
-  my $reservation_info = $ec2->run_instances( 'ImageId'  => $ami,
-                                              'KeyName'  => $keyname,
-                                              'MinCount' => 1,
-                                              'MaxCount' => 1,
-                                            );
+  my( $ec2, $svcnum, $ami, $keyname, $InstanceType ) = amazon_ec2_new(@_);
+
+  my $reservation_info = $ec2->run_instances(
+    'ImageId'      => $ami,
+    'KeyName'      => $keyname,
+    'InstanceType' => $InstanceType || 'm1.small',
+    'MinCount'     => 1,
+    'MaxCount'     => 1,
+  );
 
   my $instance_id = $reservation_info->instances_set->[0]->instance_id;
 
index 7ff7280..154f979 100644 (file)
@@ -28,6 +28,7 @@ sub _export_insert {
   my $ssh = Net::OpenSSH->new( $self->machine,
                                default_stdin_fh => $def_in );
 
+  #capture2 and return STDERR, its probably useful if there's a problem
   my $private_key = $ssh->capture(
     { 'stdin_data' => $svc_acct->_password. "\n" },
     '/usr/local/bin/merchant_create', map $svc_acct->$_, qw( username finger )
@@ -67,6 +68,7 @@ sub _export_delete {
   my $ssh = Net::OpenSSH->new( $self->machine,
                                default_stdin_fh => $def_in );
 
+  #capture2 and return STDERR, its probably useful if there's a problem
   my $unused_output = $ssh->capture(
     '/usr/local/bin/merchant_disable', map $svc_acct->$_, qw( username )
   );
index 52bbdeb..ae8526c 100644 (file)
@@ -283,8 +283,8 @@ sub _upgrade_data {
   my $sth = dbh->prepare(
     'UPDATE phone_avail SET svcnum = NULL
        WHERE svcnum IS NOT NULL
-         AND 0 = ( SELECT COUNT(*) FROM svc_phone
-                     WHERE phone_avail.svcnum = svc_phone.svcnum )'
+         AND NOT EXISTS ( SELECT 1 FROM svc_phone
+                            WHERE phone_avail.svcnum = svc_phone.svcnum )'
   ) or die dbh->errstr;
 
   $sth->execute or die $sth->errstr;
diff --git a/FS/FS/prospect_contact.pm b/FS/FS/prospect_contact.pm
new file mode 100644 (file)
index 0000000..6626132
--- /dev/null
@@ -0,0 +1,125 @@
+package FS::prospect_contact;
+use base qw( FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::prospect_contact - Object methods for prospect_contact records
+
+=head1 SYNOPSIS
+
+  use FS::prospect_contact;
+
+  $record = new FS::prospect_contact \%hash;
+  $record = new FS::prospect_contact { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::prospect_contact object represents a contact's attachment to a specific
+prospect.  FS::prospect_contact inherits from FS::Record.  The following fields
+are currently supported:
+
+=over 4
+
+=item prospectcontactnum
+
+primary key
+
+=item prospectnum
+
+prospectnum
+
+=item contactnum
+
+contactnum
+
+=item classnum
+
+classnum
+
+=item comment
+
+comment
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record.  To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'prospect_contact'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('prospectcontactnum')
+    || $self->ut_number('prospectnum')
+    || $self->ut_number('contactnum')
+    || $self->ut_numbern('classnum')
+    || $self->ut_textn('comment')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::contact>, L<FS::prospect_main>, L<FS::Record>
+
+=cut
+
+1;
+
index b160343..81f71a9 100644 (file)
@@ -269,7 +269,7 @@ sub name {
   my $self = shift;
   return $self->company if $self->company;
 
-  my $contact = ($self->contact)[0]; #first contact?  good enough for now
+  my $contact = ($self->prospect_contact)[0]->contact; #first contact?  good enough for now
   return $contact->line if $contact;
 
   'Prospect #'. $self->prospectnum;
@@ -314,7 +314,7 @@ sub convert_cust_main {
   my @cust_location = $self->cust_location;
   #the interface only allows one, so we're just gonna go with that for now
 
-  my @contact = $self->contact;
+  my @contact = map $_->contact, $self->prospect_contact;
 
   #XXX define one contact type as "billing", then we could pick just that one
   my @invoicing_list = map $_->emailaddress, map $_->contact_email, @contact;
index 6e36c33..e5e29b4 100644 (file)
@@ -834,3 +834,7 @@ FS/svc_circuit.pm
 t/svc_circuit.t
 FS/cust_credit_source_bill_pkg.pm
 t/cust_credit_source_bill_pkg.t
+FS/prospect_contact.pm
+t/prospect_contact.t
+FS/cust_contact.pm
+t/cust_contact.t
index 45d5878..a3c67f9 100644 (file)
@@ -120,10 +120,10 @@ while (1) {
 sub _shouldrun {
 
   my $extra_sql =
-    ' AND 0 < ( SELECT COUNT(*) FROM cust_pkg
-                  WHERE cust_pkg.pkgpart = part_pkg.pkgpart
-                    AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
-              )
+    ' AND EXISTS ( SELECT 1 FROM cust_pkg
+                     WHERE cust_pkg.pkgpart = part_pkg.pkgpart
+                       AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
+                 )
     ';
 
   my @part_pkg =
diff --git a/FS/t/cust_contact.t b/FS/t/cust_contact.t
new file mode 100644 (file)
index 0000000..0e9ea71
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_contact;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/prospect_contact.t b/FS/t/prospect_contact.t
new file mode 100644 (file)
index 0000000..dbb12e5
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::prospect_contact;
+$loaded=1;
+print "ok 1\n";
index c4d5169..a9a899c 100755 (executable)
@@ -193,16 +193,17 @@ foreach my $cust_bill ( @cust_bill ) {
   my $cur_cr = 0;
   $cur_cr += $_->amount foreach $cust_bill->cust_credited;
   $cur_cr = '' if $cur_cr == 0;
+
+  next if $cur_cr > 0 && $opt_k;
+
   if ( $opt_p ) {
     #print $cust_bill->invnum. ','. $cust_bill->custnum. ",$tax,$credit,$cr_percent%\n";
+#    print $cust_bill->invnum. ','. $cust_bill->custnum. ',"'.
+#          $cust_bill->cust_main->name. '",'. "$tax,$credit,$cur_cr\n";
     print $cust_bill->invnum. ','. $cust_bill->custnum. ',"'.
-          $cust_bill->cust_main->name. '",'. "$tax,$credit,$cur_cr\n";
+          $cust_bill->cust_main->name. '",'. "$tax,$credit\n";
   }
 
-  next if $cur_cr > 0 && $opt_k;
-
-#COMMENTING OUT ALL DANGEROUS STUFF
-#
 #  if ( $opt_m && ! $opt_r ) {
 #
 #    my $msg_template = qsearchs('msg_template', { 'msgnum' => $opt_m } )
@@ -216,28 +217,28 @@ foreach my $cust_bill ( @cust_bill ) {
 #           " custnum ". $cust_bill->custnum. ": $error\n";
 #    }
 #  }
-#
-#  if ( $opt_c ) {
-#    my $cust_credit = new FS::cust_credit {
-#      'custnum'   => $cust_main->custnum,
-#      'amount'    => $credit,
-#      'reasonnum' => $opt_c,
-#    };
-#    my $error = $cust_credit->insert;
-#    if ( $error ) {
-#      warn "error inserting credit: $error\n";
-#    }
-#    my $cust_credit_bill = new FS::cust_credit_bill {
-#      'crednum' => $cust_credit->crednum,
-#      'invnum'  => $cust_bill->invnum,
-#      'amount'  => $credit,
-#    };
-#    my $aerror = $cust_credit_bill->insert;
-#    if ( $aerror ) {
-#      warn "error applying credit to invnum ". $cust_bill->invnum. ": $aerror\n";
-#    }
-#  }
-#
+
+  if ( $opt_c ) {
+    my $cust_credit = new FS::cust_credit {
+      'custnum'   => $cust_main->custnum,
+      'amount'    => $credit,
+      'reasonnum' => $opt_c,
+    };
+    my $error = $cust_credit->insert;
+    if ( $error ) {
+      warn "error inserting credit: $error\n";
+    }
+    my $cust_credit_bill = new FS::cust_credit_bill {
+      'crednum' => $cust_credit->crednum,
+      'invnum'  => $cust_bill->invnum,
+      'amount'  => $credit,
+    };
+    my $aerror = $cust_credit_bill->insert;
+    if ( $aerror ) {
+      warn "error applying credit to invnum ". $cust_bill->invnum. ": $aerror\n";
+    }
+  }
+
 #  if ( $opt_e && ! $opt_r ) {
 #    eval { $cust_bill->email };
 #    if ( $@ ) {
index 0a6f851..686bef6 100644 (file)
@@ -1,7 +1,7 @@
 package FS::table_name;
+use base qw( FS::Record );
 
 use strict;
-use base qw( FS::Record );
 use FS::Record qw( qsearch qsearchs );
 
 =head1 NAME
index bedb5ec..4e0f495 100755 (executable)
@@ -11,7 +11,8 @@ perl Makefile.PL && make && make install
 cd ..
 
 #( cd ..; make deploy; cd fs_selfservice )
-( cd ..; make clean; make configure-rt; make install-perl-modules; /etc/init.d/freeside restart; cd fs_selfservice )
+#( cd ..; make clean; make configure-rt; make install-perl-modules; /etc/init.d/freeside restart; cd fs_selfservice )
+( cd ..; make clean; make configure-rt; make install-perl-modules; make deploy; cd fs_selfservice )
 
 #cp /home/ivan/freeside/fs_selfservice/FS-SelfService/cgi/* /var/www/MyAccount
 #chown freeside /var/www/MyAccount/*.cgi
index f54a157..3aa60a0 100644 (file)
@@ -30,6 +30,7 @@ $socket .= '.'.$tag if defined $tag && length($tag);
   'login'                     => 'MyAccount/login',
   'logout'                    => 'MyAccount/logout',
   'switch_acct'               => 'MyAccount/switch_acct',
+  'switch_cust'               => 'MyAccount/switch_cust',
   'customer_info'             => 'MyAccount/customer_info',
   'customer_info_short'       => 'MyAccount/customer_info_short',
   'billing_history'           => 'MyAccount/billing_history',
diff --git a/fs_selfservice/FS-SelfService/cgi/select_cust.html b/fs_selfservice/FS-SelfService/cgi/select_cust.html
new file mode 100644 (file)
index 0000000..7ab55db
--- /dev/null
@@ -0,0 +1,38 @@
+<HTML>
+  <HEAD>
+    <TITLE>Select customer</TITLE>
+    <%= $head %>
+  </HEAD>
+  <BODY BGCOLOR="<%= $body_bgcolor || '#eeeeee' %>">
+  <%= $body_header %>
+
+<FONT SIZE=5>Select customer</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+
+<%= $selfurl =~ s/\?.*//; ''; %>
+<FORM ACTION="<%= $selfurl %>" METHOD=POST>
+<INPUT TYPE="hidden" NAME="action" VALUE="switch_cust">
+<INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
+
+<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=2 CELLPADDING=0>
+
+  <TR>
+    <TH ALIGN="right">Customer </TH>
+    <TD>
+      <SELECT NAME="custnum">
+        <OPTION VALUE="">Select a customer
+<%=     $OUT .= qq(<OPTION VALUE="$_">). encode_entities( $customers{$_} )
+          foreach keys %customers;
+%>
+      </SELECT>
+    </TD>
+  </TR>
+
+  <TR>
+    <TD COLSPAN=2 ALIGN="center"><INPUT TYPE="submit" VALUE="Select customer"></TD>
+  </TR>
+
+</TABLE>
+</FORM>
+
+<%= $body_footer %>
index 9443a7d..2337fb5 100755 (executable)
@@ -81,6 +81,7 @@ my @actions = ( qw(
   process_change_password
   customer_suspend_pkg
   process_suspend_pkg
+  switch_cust
 ));
 
 my @nologin_actions = (qw(
@@ -204,6 +205,12 @@ unless ( $nologin_actions{$action} ) {
 
   # at this point $session_id is a real session
 
+  if ( ! $login_rv->{'custnum'} && ! $login_rv->{'svcnum'} && $login_rv->{'customers'} ) {
+    #select a customer if we're a multi-contact customer
+    do_template('select_cust', { %$login_rv } );
+    exit;
+  }
+
 }
 
 warn "calling $action sub\n"
@@ -212,6 +219,7 @@ $FS::SelfService::DEBUG = $DEBUG;
 my $result = eval "&$action();";
 die $@ if $@;
 
+use Data::Dumper;
 warn Dumper($result) if $DEBUG;
 
 if ( $result->{error} && ( $result->{error} eq "Can't resume session"
@@ -237,7 +245,13 @@ do_template($action, {
 
 #--
 
-use Data::Dumper;
+sub switch_cust {
+  $action = 'myaccount';
+  FS::SelfService::switch_cust( 'session_id' => $session_id,
+                                'custnum'    => scalar($cgi->param('custnum')),
+                              );
+}
+
 sub myaccount { 
   customer_info( 'session_id' => $session_id ); 
 }
index d3cf873..9b2298a 100644 (file)
@@ -8,8 +8,9 @@
                  'count_query' => 'SELECT COUNT(*) FROM discount',
                  'disableable' => 1,
                  'disabled_statuspos' => 1,
-                 'header'      => [ 'Name', 'Class', 'Discount', ],
+                 'header'      => [ 'Name', 'Comment', 'Class', 'Discount', ],
                  'fields'      => [ 'name',
+                                    'comment',
                                     'classname',
                                     'description',
                                   ],
index 80d9488..0f173f2 100644 (file)
@@ -56,7 +56,7 @@ GNU <b>Affero</b> General Public License.<BR>
 
 % unless ( $agentnum ) {
   <CENTER>
-  <FONT SIZE="-3">"" - R. Hunter</FONT>
+  <FONT SIZE="-3">"Half the world's a desert / Cannibals eat human brains for dessert" - D. Zero</FONT>
   </CENTER>
 % }
 
index 9f06546..3b7eb07 100644 (file)
@@ -11,6 +11,7 @@
        { 'field'             => 'contactnum',
          'type'              => 'contact',
          'colspan'           => 6,
+         'custnum'           => $custnum,
          'm2m_method'        => 'cust_contact',
          'm2m_dstcol'        => 'contactnum',   
          'm2_label'          => ' ', #'Contact',
index 353ae17..da87bfc 100755 (executable)
@@ -325,8 +325,8 @@ if ( $cgi->param('error') ) {
     $cust_main->company(  $prospect_main->company  );
 
     #first contact? -> name
-    my @contacts = $prospect_main->contact;
-    my $contact = $contacts[0];
+    my @prospect_contacts = $prospect_main->prospect_contact;
+    my $contact = $prospect_contacts[0]->contact;
     $cust_main->first( $contact->first );
     $cust_main->set( 'last', $contact->get('last') );
     #contact phone numbers?
index 9e506a7..4d5beee 100644 (file)
@@ -334,6 +334,10 @@ Example:
 %     #any?
 %     'colspan'       => $f->{'colspan'},
 %     'required'      => $f->{'required'},
+%
+%     #contact
+%     'custnum'     => $f->{'custnum'},
+%     'prospectnum' => $f->{'prospectnum'},
 %   );
 %
 %   $include_common{$_} = $f->{$_} foreach grep exists($f->{$_}),
index 979c26b..ef74481 100644 (file)
@@ -9,7 +9,7 @@
           <SELECT NAME="<%$name%>_classnum" <% $onchange %>>
             <OPTION VALUE="">
 %           my $classnum = scalar($cgi->param($name.'_classnum'))
-%                            || $contact->classnum;
+%                            || $X_contact->classnum;
 %           foreach my $contact_class (@contact_class) {
               <OPTION VALUE="<% $contact_class->classnum %>"
                  <% ($contact_class->classnum == $classnum) ? 'SELECTED' : '' %>
@@ -40,6 +40,8 @@
 %         }
 %       } elsif ( $field eq 'emailaddress' ) {
 %         $value = join(', ', map $_->emailaddress, $contact->contact_email);
+%       } elsif ( $field eq 'selfservice_access' || $field eq 'comment' ) {
+%         $value = $X_contact->get($field);
 %       } else {
 %         $value = $contact->get($field);
 %       }
@@ -100,10 +102,25 @@ if ( $opt{'onchange'} ) {
 my @contact_class = qsearch('contact_class', { 'disabled' => '' });
 
 my $contact;
+my $X_contact;
 if ( $curr_value ) {
   $contact = qsearchs('contact', { 'contactnum' => $curr_value } );
+  if ( $opt{'custnum'} ) {
+    $X_contact = qsearchs('cust_contact', {
+                            'contactnum' => $curr_value,
+                            'custnum'    => $opt{'custnum'},
+                 });
+  } elsif ( $opt{'prospectnum'} ) {
+    $X_contact = qsearchs('prospect_contact', {
+                   'contactnum'  => $curr_value,
+                   'prospectnum' => $opt{'prospectnum'},
+                 });
+  } else {
+    die 'neither custnum nor prospectnum specified';
+  }
 } else {
   $contact = new FS::contact {};
+  $X_contact = new FS::cust_contact; #arbitrary, it could be prospect_contact
 }
 
 my %size = ( 'title' => 12 );
index e5f8c61..2b6b187 100644 (file)
@@ -2,9 +2,9 @@
 
 Example:
 
-  include('/elements/init_overlib.html')
+  <& /elements/init_overlib.html &>
 
-  include( '/elements/popup_link.html', { #hashref or a list, either way is fine
+  <& /elements/popup_link.html', { #hashref or a list, either way is fine
 
     #required
     'action'         => 'content.html', # uri for content of popup
@@ -23,7 +23,8 @@ Example:
     'aname'          => "target", # link NAME= value, useful for #targets
     'target'         => '_parent',
     'style'          => 'css-attribute:value',
-  } )
+  }
+  &>
 
 </%doc>
 % if ($params->{'action'} && $label) {
index 6904e3b..373c0ab 100644 (file)
@@ -1,6 +1,6 @@
 <% include('tr-td-label.html', @_ ) %>
 
-  <TD BGCOLOR="#dddddd" <% $style %>><% $value %></TD>
+  <TD BGCOLOR="#dddddd" <% $style %> <% $colspan %>><% $value %></TD>
 
 </TR>
 
@@ -10,7 +10,9 @@
 
 my %opt = @_;
 
-my $style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+my $style = $opt{'cell_style'} ? ' STYLE="'. $opt{'cell_style'}. '" ' : '';
+
+my $colspan = $opt{'colspan'} ? ' COLSPAN="'. $opt{'colspan'}. '" ' : '';
 
 my $value = $opt{'formatted_value'} || $opt{'curr_value'} || $opt{'value'};
 $value = $opt{'prefix'} . $value if defined($opt{'prefix'});
index e37d26d..0bfa893 100644 (file)
@@ -138,8 +138,8 @@ if ( $cgi->param('error') ) {
   if ( length($opt{'curr_value'}) ) {
     $contactnum = $opt{'curr_value'};
   } elsif ($prospect_main) {
-    my @cust_contact = $prospect_main->contact;
-    $contactnum = $cust_contact[0]->contactnum if scalar(@cust_contact)==1;
+    my @prospect_contact = $prospect_main->prospect_contact;
+    $contactnum = $prospect_contact[0]->contactnum if scalar(@cust_contact)==1;
   } else { #$cust_main
     $cgi->param('contactnum') =~ /^(\-?\d*)$/ or die "illegal contactnum";
     $contactnum = $1;
@@ -176,8 +176,10 @@ my $contact_sort = sub {
 };
 
 my @contact;
-push @contact, $cust_main->cust_contact if $cust_main;
-push @contact, $prospect_main->contact if $prospect_main;
+push @contact, map $_->contact, $cust_main->cust_contact
+  if $cust_main;
+push @contact, map $_->contact, $prospect_main->prospect_contact
+  if $prospect_main;
 push @contact, $contact
   if !$cust_main && $contact && $contact->contactnum > 0
   && ! grep { $_->contactnum == $contact->contactnum } @contact;
index abaaa5b..7a5b43b 100644 (file)
@@ -287,6 +287,8 @@ if ( $locationnum && $locationnum > 0 ) {
 $cust_location->coord_auto('Y');
 
 my $location_sort = sub {
+  #enabled w/label_prefix _location #    $a->locationname cmp $b->locationname
+                                    # or 
         $a->country   cmp $b->country
   or lc($a->city)     cmp lc($b->city)
   or lc($a->address1) cmp lc($b->address1)
index b93b80b..64e3691 100644 (file)
 %   }
 % }
 
-% my @contact = $quotation->custnum ? $quotation->cust_main->cust_contact
-%                                   : $quotation->prospect_main->contact;
-% foreach my $contact ( @contact ) {
+% my @X_contact = $quotation->custnum
+%                   ? $quotation->cust_main->cust_contact
+%                   : $quotation->prospect_main->prospect_contact;
+% foreach my $X_contact ( @X_contact ) {
+%    my $contact = $X_contact->contact;
 %    foreach my $contact_email ( $contact->contact_email ) {
 %      $emails++;
        <& .emailrow, $contact_email->emailaddress, $contact->firstlast &>
index 1933493..c3667df 100644 (file)
@@ -1,13 +1,13 @@
 <& elements/search.html,
   title         => 'Contacts',
   name_singular => 'contact',
-  query         => { select    => $select,
+  query         => { select    => join(', ', @select),
                      table     => 'contact',
                      addl_from => $addl_from,
                      hashref   => \%hash,
                      extra_sql => $extra_sql,
                    },
-  count_query   => "SELECT COUNT(*) FROM contact $extra_sql", #XXX
+  count_query   => "SELECT COUNT(*) FROM contact $addl_from $extra_sql", #XXX
   header        => \@header,
   fields        => \@fields,
   links         => \@links,
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('List contacts');
 
-my $select = 'contact.*';
+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 = '';
 
-my @header = ( 'First', 'Last', 'Title', );
-my @fields = ( 'first', 'last', 'title', );
-my @links = ( '', '', '' );
+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';
+  }
+  $X_contact->contact_classname;
+};
+
+my @header = ( 'First', 'Last', 'Title', 'Type' );
+my @fields = ( 'first', 'last', 'title', $contact_classname_sub );
+my @links = ( '', '', '', '', );
 
 my $company_link = '';
 
@@ -32,22 +48,30 @@ if ( $cgi->param('selfservice_access') eq 'Y' ) {
 }
 
 my $extra_sql = '';
-if ( $cgi->param('link') ) {
+$link = $cgi->param('link');
+if ( $link ) {
 
-  my $coalesce = ', COALESCE( cust_main.company,';
   my $as       = ') AS prospect_or_customer';
 
-  if ( $cgi->param('link') eq 'cust_main' ) {
+  if ( $link eq 'cust_main' ) {
     push @header, 'Customer';
-    $select .= "$coalesce cust_main.first||' '||cust_main.last $as";
-    $addl_from = ' LEFT JOIN cust_main USING ( custnum )';
-    $extra_sql = ' custnum IS NOT NULL ';
+    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 ';
     $company_link  = [ $p.'view/cust_main.cgi?', 'custnum' ];
-  } elsif ( $cgi->param('link') eq 'prospect_main' ) {
+  } elsif ( $link eq 'prospect_main' ) {
     push @header, 'Prospect';
-    $select .= "$coalesce contact.first||'  '||contact.last $as";
-    $addl_from = ' LEFT JOIN prospect_main USING ( prospectnum )';
-    $extra_sql = ' prospectnum IS NOT NULL ';
+    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 ';
     $company_link  = [ $p.'view/prospect_main.html?', 'prospectnum' ];
   } else {
     die "don't know how to report on contacts linked to specified table";
@@ -62,6 +86,9 @@ if ( $cgi->param('link') ) {
 push @header, 'Self-service';
 push @fields, 'selfservice_access';
 
+push @header, 'Comment';
+push @fields, 'comment';
+
 $extra_sql = (keys(%hash) ? ' AND ' : ' WHERE '). $extra_sql
  if $extra_sql;
 
index 486c7b0..d5b865c 100644 (file)
@@ -47,7 +47,7 @@
                           ],
        'html_init'     => $html_init,
        'really_disable_download' => 1,
-       @_
+       @_ #why?
 &>
 <%init>
 #hmm...
@@ -71,7 +71,7 @@ if ( $cgi->param('msgtype') =~ /^(\w+)$/ ) {
   push @where, "msgtype = '$1'";
 }
 if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
-  push @where, "custnum = $1";
+  push @where, "cust_msg.custnum = $1";
 }
 my ($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, '');
 push @where, "(_date >= $beginning AND _date <= $ending)";
index 4798f58..241918b 100644 (file)
@@ -12,9 +12,9 @@
                                   sub {
                                     my $pm = shift;
                                     [ map {
-                                            [ { 'data' => $_->line, }, ];
+                                            [ { 'data'=>$_->contact->line, }, ];
                                           }
-                                          $pm->contact
+                                          $pm->prospect_contact
                                     ];
                                   },
                                 ],
index f73483a..f0bc0b8 100644 (file)
@@ -6,26 +6,31 @@
 % my $bgcolor1 = '#eeeeee';
 %     my $bgcolor2 = '#ffffff';
 %     my $bgcolor = $bgcolor2;
+% my $th = '<TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">';
 <TR>
-  <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Type</TH>
-  <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Contact</TH>
-  <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Email</TH>
-  <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Self-service</TH>
+  <%$th%>Type</TH>
+  <%$th%>Contact</TH>
+  <%$th%>Email</TH>
+  <%$th%>Self-service</TH>
 % foreach my $phone_type (@phone_type) {
-    <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc"><% $phone_type->typename |h %> phone</TD>
+    <%$th%><% $phone_type->typename |h %></TH>
 % }
+  <%$th%>Comment</TH>
 </TR>
 
-%   foreach my $contact ( @contacts ) {
+%   foreach my $cust_contact ( @cust_contacts ) {
+%     my $contact = $cust_contact->contact;
+%     my $td = qq(<TD CLASS="grid" BGCOLOR="$bgcolor">);
+
       <TR>
-        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $contact->contact_classname |h %></TD>
-        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $contact->line |h %></TD>
+        <%$td%><% $cust_contact->contact_classname |h %></TD>
+        <%$td%><% $contact->line |h %></TD>
 
 %       my @contact_email = $contact->contact_email;
-        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% join(', ', map $_->emailaddress, @contact_email) %></TD>
+        <%$td%><% join(', ', map $_->emailaddress, @contact_email) %></TD>
 
-        <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-%         if ( $contact->selfservice_access ) {
+        <%$td%>
+%         if ( $cust_contact->selfservice_access ) {
             Enabled
 %#            <FONT SIZE="-1"><A HREF="XXX">disable</A>
 %#                            <A HREF="XXX">re-email</A></FONT>
 %                      'contactnum'   => $contact->contactnum,
 %                      'phonetypenum' => $phone_type->phonetypenum,
 %                   });
-          <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $contact_phone ? $contact_phone->phonenum_pretty : '' |h %></TD>
+          <%$td%><% $contact_phone ? $contact_phone->phonenum_pretty : '' |h %></TD>
 %       }
 
+        <%$td%><% $cust_contact->comment |h %></TD>
+
       </TR>
 
 %     if ( $bgcolor eq $bgcolor1 ) {
@@ -63,6 +70,6 @@ my @phone_type = qsearch({table=>'phone_type', order_by=>'weight'});
 my( $cust_main ) = @_;
 #my $conf = new FS::Conf;
 
-my @contacts = $cust_main->cust_contact;
+my @cust_contacts = $cust_main->cust_contact;
 
 </%init>
index 66abffc..a1f14a3 100644 (file)
   </TR>
 % }
 
-% foreach my $contact ( $prospect_main->contact ) {
+% foreach my $prospect_contact ( $prospect_main->prospect_contact ) {
+%   my $contact = $prospect_contact->contact;
     <TR>
-      <TD ALIGN="right"><% $contact->contact_classname %> Contact</TD>
+      <TD ALIGN="right"><% $prospect_contact->contact_classname %> Contact</TD>
       <TD BGCOLOR="#FFFFFF"><% $contact->line %></TD>
     </TR>
 %}