beginning of prospect/CRM/contact work
authorivan <ivan>
Mon, 28 Dec 2009 19:20:25 +0000 (19:20 +0000)
committerivan <ivan>
Mon, 28 Dec 2009 19:20:25 +0000 (19:20 +0000)
34 files changed:
FS/FS.pm
FS/FS/AccessRight.pm
FS/FS/Mason.pm
FS/FS/Schema.pm
FS/FS/Setup.pm
FS/FS/Upgrade.pm
FS/FS/contact.pm [new file with mode: 0644]
FS/FS/contact_email.pm [new file with mode: 0644]
FS/FS/contact_phone.pm [new file with mode: 0644]
FS/FS/cust_location.pm
FS/FS/o2m_Common.pm [new file with mode: 0644]
FS/FS/phone_type.pm [new file with mode: 0644]
FS/FS/prospect_main.pm [new file with mode: 0644]
FS/MANIFEST
FS/t/contact.t [new file with mode: 0644]
FS/t/contact_email.t [new file with mode: 0644]
FS/t/contact_phone.t [new file with mode: 0644]
FS/t/phone_type.t [new file with mode: 0644]
FS/t/prospect_main.t [new file with mode: 0644]
httemplate/edit/elements/edit.html
httemplate/edit/process/elements/process.html
httemplate/edit/process/prospect_main.html [new file with mode: 0644]
httemplate/edit/prospect_main.html [new file with mode: 0644]
httemplate/elements/city.html
httemplate/elements/contact.html [new file with mode: 0644]
httemplate/elements/header.html
httemplate/elements/menu.html
httemplate/elements/tr-contact.html [new file with mode: 0644]
httemplate/elements/tr-select-cust_location.html
httemplate/misc/location.cgi
httemplate/search/prospect_main.html [new file with mode: 0644]
httemplate/search/report_prospect_main.html [new file with mode: 0644]
httemplate/view/cust_main/packages/location.html
httemplate/view/prospect_main.html [new file with mode: 0644]

index c6f9ece..13dc04f 100644 (file)
--- a/FS/FS.pm
+++ b/FS/FS.pm
@@ -232,9 +232,19 @@ L<FS::reason> - Reason class
 
 L<FS::cust_pkg_reason> - Package reason class
 
+L<FS::contact> - Contact class
+
+L<FS::contact_phone> - Contact phone class
+
+L<FS::phone_type> - Phone type class
+
+L<FS::contact_email> - Contact email class
+
+L<FS::prospect_main> - Prospect class
+
 L<FS::cust_main> - Customer class
 
-L<FS::cust_main_location> - Customer location class
+L<FS::cust_location> - Customer location class
 
 L<FS::cust_main_Mixin> - Mixin class for records that contain fields from cust_main
 
index 44235b1..8e771a9 100644 (file)
@@ -85,6 +85,20 @@ assigned to users and/or groups.
 
 #well, this is what we have for now.  getting better.
 tie my %rights, 'Tie::IxHash',
+
+  ###
+  # contact rights
+  ###
+  'Contact and Prospect rights' => [
+    'New prospect',
+    'View prospect',
+    'Edit prospect',
+    'List prospects',
+    'Edit contact', #!
+    #'New contact',
+    #'View customer contacts',
+    #'List contacts',
+  ],
   
   ###
   # basic customer rights
index bef0058..cc15cd4 100644 (file)
@@ -224,6 +224,8 @@ if ( -e $addl_handler_use_file ) {
   use FS::cust_statement;
   use FS::cust_class;
   use FS::cust_category;
+  use FS::prospect_main;
+  use FS::contact;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
index 7c6548a..a039003 100644 (file)
@@ -800,12 +800,84 @@ sub tables_hashref {
       'index' => [],
     },
 
+    #eventually for cust_main too
+    'contact' => {
+      'columns' => [
+        'contactnum', 'serial',     '',      '', '', '',
+        'prospectnum',   'int', 'NULL',      '', '', '',
+        'custnum',       'int', 'NULL',      '', '', '',
+        'locationnum',   'int', 'NULL',      '', '', '', #not yet
+#        'titlenum',      'int', 'NULL',      '', '', '', #eg Mr. Mrs. Dr. Rev.
+        'last',      'varchar',     '', $char_d, '', '', 
+#        'middle',    'varchar', 'NULL', $char_d, '', '', 
+        'first',     'varchar',     '', $char_d, '', '', 
+        'title',     'varchar',     '', $char_d, '', '', #eg Head Bottle Washer
+        'comment',   'varchar',     '', $char_d, '', '', 
+        'disabled',     'char', 'NULL',       1, '', '', 
+      ],
+      'primary_key' => 'contactnum',
+      'unique'      => [],
+      'index'       => [ [ 'prospectnum' ], [ 'custnum' ], [ 'locationnum' ],
+                         [ 'last' ], [ 'first' ],
+                       ],
+    },
+
+    'contact_phone' => {
+      'columns' => [
+        'contactphonenum', 'serial', '', '', '', '',
+        'contactnum',         'int', '', '', '', '',
+        'phonetypenum',       'int', '', '', '', '',
+        'countrycode',    'varchar', '',  3, '', '', 
+        'phonenum',       'varchar', '', 14, '', '', 
+        'extension',      'varchar', '',  7, '', '',
+        #?#'comment',   'varchar',     '', $char_d, '', '', 
+      ],
+      'primary_key' => 'contactphonenum',
+      'unique'      => [],
+      'index'       => [],
+    },
+
+    'phone_type' => {
+      'columns' => [
+        'phonetypenum',  'serial', '',      '', '', '',
+        'typename',     'varchar', '', $char_d, '', '',
+        'weight',           'int', '',      '', '', '', 
+      ],
+      'primary_key' => 'phonetypenum',
+      'unique'      => [ [ 'typename' ], ],
+      'index'       => [],
+    },
+
+    'contact_email' => {
+      'columns' => [
+        'contactemailnum', 'serial', '',      '', '', '',
+        'contactnum',         'int', '',      '', '', '',
+        'emailaddress',   'varchar', '', $char_d, '', '',
+      ],
+      'primary_key' => 'contactemailnum',
+      'unique'      => [ [ 'emailaddress' ], ],
+      'index'       => [],
+    },
+
+    'prospect_main' => {
+      'columns' => [
+        'prospectnum',  'serial',     '',      '', '', '',
+        'agentnum',        'int',     '',      '', '', '',
+        'company',     'varchar',     '', $char_d, '', '',
+        #'disabled',     'char', 'NULL',       1, '', '', 
+      ],
+      'primary_key' => 'prospectnum',
+      'unique'      => [],
+      'index'       => [ [ 'company' ], [ 'agentnum' ], ],
+    },
+
     #eventually use for billing & ship from cust_main too
     #for now, just cust_pkg locations
-    'cust_location' => {
+    'cust_location' => { #'location' now that its prospects too, but...
       'columns' => [
         'locationnum',  'serial',     '',      '', '', '',
-        'custnum',         'int',     '',      '', '', '',
+        'prospectnum',     'int', 'NULL',      '', '', '',
+        'custnum',         'int', 'NULL',      '', '', '',
         'address1',    'varchar',     '', $char_d, '', '', 
         'address2',    'varchar', 'NULL', $char_d, '', '', 
         'city',        'varchar',     '', $char_d, '', '', 
@@ -817,7 +889,7 @@ sub tables_hashref {
       ],
       'primary_key' => 'locationnum',
       'unique'      => [],
-      'index'       => [ [ 'custnum' ],
+      'index'       => [ [ 'prospectnum' ], [ 'custnum' ],
                          [ 'county' ], [ 'state' ], [ 'country' ], [ 'zip' ],
                        ],
     },
index edfe912..f8c59c5 100644 (file)
@@ -348,11 +348,12 @@ sub initial_data {
 
     #not yet....
 
-  #)
-
     #usage classes
     'usage_class' => [],
 
+    #phone types
+    'phone_type' => [],
+
   ;
 
   \%hash;
index c39680e..8cc5c61 100644 (file)
@@ -126,6 +126,9 @@ sub upgrade_data {
     #usage_classes if we have none
     'usage_class' => [],
 
+    #phone_type if we have none
+    'phone_type' => [],
+
     #fixup access rights
     'access_right' => [],
 
diff --git a/FS/FS/contact.pm b/FS/FS/contact.pm
new file mode 100644 (file)
index 0000000..d3ab411
--- /dev/null
@@ -0,0 +1,177 @@
+package FS::contact;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use FS::prospect_main;
+use FS::cust_main;
+use FS::cust_location;
+
+=head1 NAME
+
+FS::contact - Object methods for contact records
+
+=head1 SYNOPSIS
+
+  use FS::contact;
+
+  $record = new FS::contact \%hash;
+  $record = new FS::contact { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::contact object represents an example.  FS::contact inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item contactnum
+
+primary key
+
+=item prospectnum
+
+prospectnum
+
+=item custnum
+
+custnum
+
+=item locationnum
+
+locationnum
+
+=item last
+
+last
+
+=item first
+
+first
+
+=item title
+
+title
+
+=item comment
+
+comment
+
+=item disabled
+
+disabled
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example 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 { 'contact'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=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.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  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('contactnum')
+    || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
+    || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
+    || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
+    || $self->ut_textn('last')
+    || $self->ut_textn('first')
+    || $self->ut_textn('title')
+    || $self->ut_textn('comment')
+    || $self->ut_enum('disabled', [ '', 'Y' ])
+  ;
+  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"
+    if ! grep $self->$_(), qw( first last title);
+
+  $self->SUPER::check;
+}
+
+sub line {
+  my $self = shift;
+  my $data = $self->first. ' '. $self->last;
+  $data .= ', '. $self->title
+    if $self->title;
+  $data .= ' ('. $self->comment. ')'
+    if $self->comment;
+  $data;
+}
+
+=back
+
+=head1 BUGS
+
+The author forgot to customize this manpage.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/contact_email.pm b/FS/FS/contact_email.pm
new file mode 100644 (file)
index 0000000..1276d8d
--- /dev/null
@@ -0,0 +1,128 @@
+package FS::contact_email;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::contact_email - Object methods for contact_email records
+
+=head1 SYNOPSIS
+
+  use FS::contact_email;
+
+  $record = new FS::contact_email \%hash;
+  $record = new FS::contact_email { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::contact_email object represents an example.  FS::contact_email inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item contactemailnum
+
+primary key
+
+=item contactnum
+
+contactnum
+
+=item emailaddress
+
+emailaddress
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example 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 { 'contact_email'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=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.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  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('contactemailnum')
+    || $self->ut_number('contactnum')
+    || $self->ut_text('emailaddress')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+The author forgot to customize this manpage.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/contact_phone.pm b/FS/FS/contact_phone.pm
new file mode 100644 (file)
index 0000000..bb9cf03
--- /dev/null
@@ -0,0 +1,143 @@
+package FS::contact_phone;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::contact_phone - Object methods for contact_phone records
+
+=head1 SYNOPSIS
+
+  use FS::contact_phone;
+
+  $record = new FS::contact_phone \%hash;
+  $record = new FS::contact_phone { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::contact_phone object represents an example.  FS::contact_phone inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item contactphonenum
+
+primary key
+
+=item contactnum
+
+contactnum
+
+=item phonetypenum
+
+phonetypenum
+
+=item countrycode
+
+countrycode
+
+=item phonenum
+
+phonenum
+
+=item extension
+
+extension
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new example.  To add the example 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 { 'contact_phone'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=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.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  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('contactphonenum')
+    || $self->ut_number('contactnum')
+    || $self->ut_number('phonetypenum')
+    || $self->ut_text('countrycode')
+    || $self->ut_text('phonenum')
+    || $self->ut_text('extension')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+The author forgot to customize this manpage.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 87c6c3e..da586f0 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use base qw( FS::Record );
 use Locale::Country;
 use FS::Record qw( qsearch ); #qsearchs );
+use FS::prospect_main;
 use FS::cust_main;
 use FS::cust_main_county;
 
@@ -119,7 +120,8 @@ sub check {
 
   my $error = 
     $self->ut_numbern('locationnum')
-    || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
+    || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
+    || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
     || $self->ut_text('address1')
     || $self->ut_textn('address2')
     || $self->ut_text('city')
@@ -131,6 +133,9 @@ 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;
+
   unless ( qsearch('cust_main_county', {
     'country' => $self->country,
     'state'   => '',
@@ -187,8 +192,10 @@ sub location_label {
 
   my $separator = $opt{join_string} || ', ';
   my $escape = $opt{escape_function} || sub{ shift };
+  my $ds = $opt{double_space} || '  ';
   my $line = '';
-  my $cydefault = FS::conf->new->config('countrydefault') || 'US';
+  my $cydefault =
+    $opt{'countrydefault'} || FS::Conf->new->config('countrydefault') || 'US';
   my $prefix = '';
 
   my $notfirst = 0;
@@ -202,11 +209,13 @@ sub location_label {
   foreach (qw ( city county state zip ) ) {
     my $method = "$prefix$_";
     if ( $self->$method ) {
-      $line .= ' (' if $method eq 'county';
-      $line .= ($notfirst ? ' ' : $separator). &$escape($self->$method);
-      $line .= ' )' if $method eq 'county';
+      $line .= ($notfirst ? ($method eq 'zip' ? $ds : ' ') : $separator);
+      $line .= '(' if $method eq 'county';
+      $line .= &$escape($self->$method);
+      $line .= ')' if $method eq 'county';
       $notfirst++;
     }
+    $line .= ',' if $method eq 'county';
   }
   $line .= $separator. &$escape(code2country($self->country))
     if $self->country ne $cydefault;
diff --git a/FS/FS/o2m_Common.pm b/FS/FS/o2m_Common.pm
new file mode 100644 (file)
index 0000000..0e03b52
--- /dev/null
@@ -0,0 +1,152 @@
+package FS::o2m_Common;
+
+use strict;
+use vars qw( $DEBUG $me );
+use Carp;
+use FS::Schema qw( dbdef );
+use FS::Record qw( qsearch qsearchs dbh );
+
+$DEBUG = 0;
+
+$me = '[FS::o2m_Common]';
+
+=head1 NAME
+
+FS::o2m_Common - Mixin class for tables with a related table
+
+=head1 SYNOPSIS
+
+use FS::o2m_Common;
+
+@ISA = qw( FS::o2m_Common FS::Record );
+
+=head1 DESCRIPTION
+
+FS::o2m_Common is intended as a mixin class for classes which have a
+related table.
+
+=head1 METHODS
+
+=over 4
+
+=item process_o2m OPTION => VALUE, ...
+
+Available options:
+
+table (required) - Table into which the records are inserted.
+
+num_col (optional) - Column in table which links to the primary key of the base table.  If not specified, it is assumed this has the same name.
+
+params (required) - Hashref of keys and values, often passed as C<scalar($cgi->Vars)> from a form.
+
+fields (required) - Arrayref of field names for each record in table.  Pulled from params as "pkeyNN_field" where pkey is table's primary key and NN is the entry's numeric identifier.
+
+=cut
+
+#a little more false laziness w/m2m_Common.pm than m2_name_Common.pm
+# still, far from the worse of it.  at least we're a reuable mixin!
+sub process_o2m {
+  my( $self, %opt ) = @_;
+
+  my $self_pkey = $self->dbdef_table->primary_key;
+  my $link_sourcekey = $opt{'num_col'} || $self_pkey;
+
+  my $hashref = {}; #$opt{'hashref'} || {};
+  $hashref->{$link_sourcekey} = $self->$self_pkey();
+
+  my $table = $self->_load_table($opt{'table'});
+  my $table_pkey = dbdef->table($table)->primary_key;
+
+#  my $link_static = $opt{'link_static'} || {};
+
+  warn "$me processing o2m from ". $self->table. ".$link_sourcekey".
+       " to $table\n"
+    if $DEBUG;
+
+  #if ( ref($opt{'params'}) eq 'ARRAY' ) {
+  #  $opt{'params'} = { map { $_=>1 } @{$opt{'params'}} };
+  #}
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my @fields = grep { /^$table_pkey\d+$/ }
+               keys %{ $opt{'params'} };
+
+  my %edits = map  { $opt{'params'}->{$_} => $_ }
+              grep { $opt{'params'}->{$_} }
+              @fields;
+
+  foreach my $del_obj (
+    grep { ! $edits{$_->$table_pkey()} }
+         qsearch( $table, $hashref )
+  ) {
+    my $error = $del_obj->delete;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  foreach my $pkey_value ( keys %edits ) {
+    my $old_obj = qsearchs( $table, { %$hashref, $table_pkey => $pkey_value } ),
+    my $add_param = $edits{$pkey_value};
+    my %hash = ( $table_pkey => $pkey_value,
+                 map { $_ => $opt{'params'}->{$add_param."_$_"} }
+                     @{ $opt{'fields'} }
+               );
+    #next unless grep { $_ =~ /\S/ } values %hash;
+
+    my $new_obj = "FS::$table"->new( { %$hashref, %hash } );
+    my $error = $new_obj->replace($old_obj);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  foreach my $add_param ( grep { ! $opt{'params'}->{$_} } @fields ) {
+
+    my %hash = map { $_ => $opt{'params'}->{$add_param."_$_"} }
+               @{ $opt{'fields'} };
+    next unless grep { $_ =~ /\S/ } values %hash;
+
+    my $add_obj = "FS::$table"->new( { %$hashref, %hash } );
+    my $error = $add_obj->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+}
+
+sub _load_table {
+  my( $self, $table ) = @_;
+  eval "use FS::$table";
+  die $@ if $@;
+  $table;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/phone_type.pm b/FS/FS/phone_type.pm
new file mode 100644 (file)
index 0000000..d2ef465
--- /dev/null
@@ -0,0 +1,137 @@
+package FS::phone_type;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch ); # qsearchs );
+
+=head1 NAME
+
+FS::phone_type - Object methods for phone_type records
+
+=head1 SYNOPSIS
+
+  use FS::phone_type;
+
+  $record = new FS::phone_type \%hash;
+  $record = new FS::phone_type { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::phone_type object represents an phone number type (for example: Work,
+Home, Mobile, Fax).  FS::phone_type inherits from FS::Record.  The following
+fields are currently supported:
+
+=over 4
+
+=item phonetypenum
+
+Primary key
+
+=item typename
+
+Type name
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new type.  To add the type 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
+
+sub table { 'phone_type'; }
+
+=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 type.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('phonetypenum')
+    || $self->ut_number('weight')
+    || $self->ut_text('typename')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+# Used by FS::Setup to initialize a new database.
+sub _populate_initial_data {
+  my ($class, %opts) = @_;
+
+  my $weight = 10;
+
+  foreach ("Work", "Home", "Mobile", "Fax") {
+    my $object = $class->new({ 'typename' => $_,
+                               'weight'   => $weight,
+                            });
+    my $error = $object->insert;
+    die "error inserting $class into database: $error\n"
+      if $error;
+
+    $weight += 10;
+  }
+
+  '';
+
+}
+
+# Used by FS::Upgrade to migrate to a new database.
+sub _upgrade_data {
+  my $class = shift;
+
+  return $class->_populate_initial_data(@_)
+    unless scalar( qsearch( 'phone_type', {} ) );
+
+  '';
+
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::contact_phone>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/prospect_main.pm b/FS/FS/prospect_main.pm
new file mode 100644 (file)
index 0000000..369029b
--- /dev/null
@@ -0,0 +1,292 @@
+package FS::prospect_main;
+
+use strict;
+use base qw( FS::o2m_Common FS::Record );
+use vars qw( $DEBUG );
+use Scalar::Util qw( blessed );
+use FS::Record qw( dbh qsearch ); #qsearchs );
+use FS::agent;
+use FS::cust_location;
+use FS::contact;
+
+$DEBUG = 0;
+
+=head1 NAME
+
+FS::prospect_main - Object methods for prospect_main records
+
+=head1 SYNOPSIS
+
+  use FS::prospect_main;
+
+  $record = new FS::prospect_main \%hash;
+  $record = new FS::prospect_main { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::prospect_main object represents a prospect.  FS::prospect_main inherits
+from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item prospectnum
+
+primary key
+
+=item company
+
+company
+
+=item locationnum
+
+locationnum
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new prospect.  To add the prospect 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
+
+sub table { 'prospect_main'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub insert {
+  my $self = shift;
+  my %options = @_;
+  warn "FS::prospect_main::insert called on $self with options ".
+       join(', ', map "$_=>$options{$_}", keys %options)
+    if $DEBUG;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  warn "  inserting prospect_main record" if $DEBUG;
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $options{'cust_location'} ) {
+    warn "  inserting cust_location record" if $DEBUG;
+    my $cust_location = $options{'cust_location'};
+    $cust_location->prospectnum($self->prospectnum);
+    $error = $cust_location->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  warn "  commiting transaction" if $DEBUG;
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+#delete dangling locations?
+
+=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.
+
+=cut
+
+sub replace {
+  my $new = shift;
+
+  my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
+              ? shift
+              : $new->replace_old;
+
+  my %options = @_;
+
+  warn "FS::prospect_main::replace called on $new to replace $old with options".
+       " ". join(', ', map "$_ => ". $options{$_}, keys %options)
+    if $DEBUG;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  warn "  replacing prospect_main record" if $DEBUG;
+  my $error = $new->SUPER::replace($old);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $options{'cust_location'} ) {
+    my $cust_location = $options{'cust_location'};
+    $cust_location->prospectnum($new->prospectnum);
+    my $method = $cust_location->locationnum ? 'replace' : 'insert';
+    warn "  ${method}ing cust_location record" if $DEBUG;
+    $error = $cust_location->$method();
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  } elsif ( exists($options{'cust_location'}) ) {
+    foreach my $cust_location (
+      qsearch('cust_location', { 'prospectnum' => $new->prospectnum } )
+    ) {
+      $error = $cust_location->delete();
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+  }
+
+  warn "  commiting transaction" if $DEBUG;
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+}
+
+=item check
+
+Checks all fields to make sure this is a valid prospect.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('prospectnum')
+    || $self->ut_foreign_key('agentnum', 'agent', 'agentnum' )
+    || $self->ut_text('company')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item contact
+
+Returns the contacts (see L<FS::contact>) associated with this prospect.
+
+=cut
+
+sub contact {
+  my $self = shift;
+  qsearch( 'contact', { 'prospectnum' => $self->prospectnum } );
+}
+
+=item search HASHREF
+
+(Class method)
+
+Returns a qsearch hash expression to search for the parameters specified in
+HASHREF.  Valid parameters are:
+
+=over 4
+
+=item agentnum
+
+=back
+
+=cut
+
+sub search {
+  my( $class, $params ) = @_;
+
+  my @where = ();
+  my $orderby;
+
+  ##
+  # parse agent
+  ##
+
+  if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
+    push @where,
+      "prospect_main.agentnum = $1";
+  }
+
+  ##
+  # setup queries, subs, etc. for the search
+  ##
+
+  $orderby ||= 'ORDER BY prospectnum';
+
+  # here is the agent virtualization
+  push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+  my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
+
+  my $count_query = "SELECT COUNT(*) FROM prospect_main $extra_sql";
+  
+  my $sql_query = {
+    'table'         => 'prospect_main',
+    #'select'        => $select,
+    'hashref'       => {},
+    'extra_sql'     => $extra_sql,
+    'order_by'      => $orderby,
+    'count_query'   => $count_query,
+    #'extra_headers' => \@extra_headers,
+    #'extra_fields'  => \@extra_fields,
+  };
+
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 8a02a13..74b06a8 100644 (file)
@@ -463,3 +463,14 @@ FS/class_Common.pm
 t/class_Common.t
 FS/category_Common.pm
 t/category_Common.t
+FS/contact.pm
+t/contact.t
+FS/contact_phone.pm
+t/contact_phone.t
+FS/phone_type.pm
+t/phone_type.t
+FS/contact_email.pm
+t/contact_email.t
+FS/prospect_main.pm
+t/prospect_main.t
+FS/o2m_Common.pm
diff --git a/FS/t/contact.t b/FS/t/contact.t
new file mode 100644 (file)
index 0000000..bf22bd1
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::contact;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/contact_email.t b/FS/t/contact_email.t
new file mode 100644 (file)
index 0000000..1cd13b6
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::contact_email;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/contact_phone.t b/FS/t/contact_phone.t
new file mode 100644 (file)
index 0000000..493ced7
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::contact_phone;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/phone_type.t b/FS/t/phone_type.t
new file mode 100644 (file)
index 0000000..b6bc793
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::phone_type;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/prospect_main.t b/FS/t/prospect_main.t
new file mode 100644 (file)
index 0000000..045c2f0
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::prospect_main;
+$loaded=1;
+print "ok 1\n";
index fd73e03..4935ddc 100644 (file)
@@ -409,7 +409,7 @@ Example:
 %       $label[0] = '/elements/tr-td-label.html';
 
         <% include( @label ) %>
-        <TD>
+        <TD COLSPAN="<% $f->{'colspan'} || 1 %>">
         <% include( @existing ) %>
         </TD>
 
@@ -465,7 +465,7 @@ Example:
 %     $label[0] = '/elements/tr-td-label.html';
 
       <% include( @label ) %>
-      <TD>
+      <TD COLSPAN="<% $f->{'colspan'} || 1 %>">
       <% include( @include ) %>
       </TD>
 
@@ -503,7 +503,7 @@ Example:
 
           // only spawn if we're the last element... return if not
 
-          var field_regex = /(\d+)$/;
+          var field_regex = /(\d+)(_[a-z]+)?$/;
           var match = field_regex.exec(what.name);
           if ( !match ) {
             alert(what.name + " didn't match?!");
@@ -574,6 +574,7 @@ Example:
 
           widget_cell.style.borderTop = "1px solid black";
           widget_cell.style.paddingTop = "3px";
+          widget_cell.colSpan = "<% $f->{'colspan'} || 1 %>"
 
           widget_cell.innerHTML = newrow;
 
index 5befdd3..e24f3f6 100644 (file)
@@ -56,13 +56,17 @@ Example:
 
 
                        },
+   'process_o2m' => { 'table' => table_name',
+                      'num_col' => 'column', #if column name is different in
+                                             #link_table than source_table 
+                    },
 
    #checks CGI params and whatever else before much else runs
    #return an error string or empty for no error
    'precheck_callback' => sub { my( $cgi ) = @_; },
 
    #supplies arguments to insert() and replace()
-   # for use with tables that are FS::option_Common
+   # for use with tables that are FS::option_Common (among other things)
    'args_callback' => sub { my( $cgi, $object ) = @_; },
 
    'debug' => 1, #turns on debugging output
@@ -255,6 +259,28 @@ if ( !$error && $opt{'process_m2name'} ) {
 
 }
 
+if ( !$error && $opt{'process_o2m'} ) {
+
+  my @process_o2m = ref($opt{'process_o2m'}) eq 'ARRAY'
+                         ? @{ $opt{'process_o2m'} }
+                         :  ( $opt{'process_o2m'} );
+
+
+  foreach my $process_o2m (@process_o2m) {
+
+    if ( $opt{'debug'} ) {
+      warn "$me processing o2m:\n". Dumper( %{ $process_o2m },
+                                               'params' => scalar($cgi->Vars),
+                                             );
+    }
+
+    $error = $new->process_o2m( %{ $process_o2m },
+                                   'params' => scalar($cgi->Vars),
+                                 );
+  }
+
+}
+
 
 if ( $error ) {
   $cgi->param('error', $error);
diff --git a/httemplate/edit/process/prospect_main.html b/httemplate/edit/process/prospect_main.html
new file mode 100644 (file)
index 0000000..34d2642
--- /dev/null
@@ -0,0 +1,34 @@
+<% include('elements/process.html',
+     'table'          => 'prospect_main',
+     'args_callback'  => $args_callback,
+     'agent_virt'     => 1,
+     'process_o2m' => {
+       'table'  => 'contact',
+       'fields' => [qw( first last title comment )],
+     },
+     'redirect' => popurl(3). 'view/prospect_main.html?',
+   )
+%>
+<%init>
+
+my $args_callback = sub {
+  my( $cgi, $object ) = @_;
+
+  $cgi->param('locationnum') =~ /^(\-?\d*)$/
+    or die 'illegal locationnum '. $cgi->param('locationnum');
+  my $locationnum = $1;
+
+  return ( 'cust_location' => '' ) unless $locationnum;
+
+  my $cust_location = new FS::cust_location {
+    map { $_ => scalar($cgi->param($_)) }
+        qw( address1 address2 city county state zip country )
+  };
+
+  $cust_location->locationnum($locationnum) unless $locationnum == -1;
+
+  ( 'cust_location' => $cust_location );
+
+};
+
+</%init>
diff --git a/httemplate/edit/prospect_main.html b/httemplate/edit/prospect_main.html
new file mode 100644 (file)
index 0000000..c4123a0
--- /dev/null
@@ -0,0 +1,83 @@
+<% include('elements/edit.html',
+     'name_singular'   => 'prospect',
+     'table'           => 'prospect_main',
+     'labels'          => { 'prospectnum' => 'Prospect',
+                            'agentnum'    => 'Agent',
+                            'company'     => 'Company',
+                            'contactnum'  => 'Contact',
+                          },
+     'fields'          => [
+       { 'field'       => 'agentnum',
+         'type'        => 'select-agent',
+         'empty_label' => 'Select agent',
+       },
+       { 'field'   => 'company',
+         'type'   => 'text',
+         'size'    => 50,
+       },
+       { 'field'             => 'contactnum',
+         'type'              => 'contact',
+         'colspan'           => 6,
+         #actually o2m, but this seems to be working for edit so far
+         'm2name_table'      => 'contact',
+         'm2name_namecol'    => 'contactnum',
+         'm2_label'          => 'Contact',
+         'm2_error_callback' => sub { my($cgi, $object) = @_; (); }, #XXX
+       },
+       { 'field'       => 'locationnum',
+         'type'        => 'select-cust_location',
+         'empty_label' => 'No address',
+       },
+     ],
+     'edit_callback'   => $edit_callback,
+     'error_callbacck' => $error_callback,
+     'agent_virt'      => 1,
+   )
+%>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+my $prospectnum;
+if ( $cgi->param('error') ) {
+  $prospectnum = scalar($cgi->param('prospectnum'));
+
+  die "access denied"
+    unless $curuser->access_right(($prospectnum ? 'Edit' : 'New'). ' prospect');
+
+} elsif ( $cgi->keywords ) { #editing
+
+  die "access denied"
+    unless $curuser->access_right('Edit prospect');
+
+} else { #new prospect 
+
+  die "access denied"
+    unless $curuser->access_right('New prospect');
+
+}
+
+my $edit_callback = sub {
+  #my( $cgi, $prospect_main, $fields_listref, $opt_hashref ) = @_;
+  my( $cgi, $prospect_main ) = @_;
+  my @cust_location =
+    qsearch('cust_location', { 'prospectnum' => $prospect_main->prospectnum } );
+  die 'multiple locations for prospect '. $prospect_main->prospectnum
+    if scalar(@cust_location) > 1;
+  $prospect_main->set('locationnum', $cust_location[0]->locationnum)
+    if scalar(@cust_location);
+  #warn 'prospect_main.locationnum '.$prospect_main->get('locationnum');
+};
+
+my $error_callback = sub {
+  #my( $cgi, $prospect_main, $fields_listref, $opt_hashref ) = @_;
+  my( $cgi, $prospect_main ) = @_;
+  $cgi->param('locationnum') =~ /^(\-?\d*)$/
+    or die 'illegal locationnum '. $cgi->param('locationnum');
+  my $locationnum = $1;
+  $prospect_main->set('locationnum', $locationnum);
+};
+
+my @agentnums = $FS::CurrentUser::CurrentUser->agentnums;
+
+</%init>
index 47e5c37..61d0578 100644 (file)
@@ -34,7 +34,7 @@ Example:
     what.options[length] = optionName;
   }
 
-  var saved_<%$pre%>city= '';
+  var saved_<%$pre%>city= '<% $saved_city |h %>';
 
   function <% $pre %>county_changed(what, callback) {
 
@@ -124,10 +124,12 @@ my $text_style   = $opt{'style'} ? [ @{ $opt{'style'} } ] : [];
 my $select_style = $opt{'style'} ? [ @{ $opt{'style'} } ] : [];
 
 my @cities = cities( $opt{'county'}, $opt{'state'}, $opt{'country'} );
+my $saved_city = '';
 if ( scalar(@cities) > 1 || $cities[0] ) {
   push @$text_style, 'display:none';
 } else {
   push @$select_style, 'display:none';
+  $saved_city = $opt{'city'};
 }
 
 $text_style =
diff --git a/httemplate/elements/contact.html b/httemplate/elements/contact.html
new file mode 100644 (file)
index 0000000..38703bf
--- /dev/null
@@ -0,0 +1,72 @@
+% unless ( $opt{'js_only'} ) {
+
+  <INPUT TYPE="hidden" NAME="<%$name%>" ID="<%$id%>" VALUE="<% $curr_value %>">
+
+  <TABLE>
+    <TR>
+      <TD>
+        <INPUT TYPE = "text"
+               NAME = "<%$name%>_first"
+               ID   = "<%$id%>_id"
+               VALUE = "<% $contact->first |h %>"
+               <% $onchange %>
+        ><BR>
+        <FONT SIZE="-2">First name</FONT>
+      </TD>
+      <TD>
+        <INPUT TYPE = "text"
+               NAME = "<%$name%>_last"
+               ID   = "<%$id%>_id"
+               VALUE = "<% $contact->get('last') |h %>"
+               <% $onchange %>
+        ><BR>
+        <FONT SIZE="-2">Last name</FONT>
+      </TD>
+      <TD>
+        <INPUT TYPE = "text"
+               NAME = "<%$name%>_title"
+               ID   = "<%$id%>_id"
+               VALUE = "<% $contact->title |h %>"
+               <% $onchange %>
+        ><BR>
+        <FONT SIZE="-2">Title/Position</FONT>
+      </TD>
+      <TD>
+        <INPUT TYPE = "text"
+               NAME = "<%$name%>_comment"
+               ID   = "<%$id%>_id"
+               VALUE = "<% $contact->comment |h %>"
+               <% $onchange %>
+        ><BR>
+        <FONT SIZE="-2">Comment</FONT>
+      </TD>
+    </TR>
+  </TABLE>
+
+% }
+<%init>
+
+my( %opt ) = @_;
+
+my $name = $opt{'element_name'} || $opt{'field'} || 'contactnum';
+my $id = $opt{'id'} || 'contactnum';
+
+my $curr_value = $opt{'curr_value'} || $opt{'value'};
+
+my $onchange = '';
+if ( $opt{'onchange'} ) {
+  $onchange = $opt{'onchange'};
+  $onchange .= '(this)' unless $onchange =~ /\(\w*\);?$/;
+  $onchange =~ s/\(what\);/\(this\);/g; #ugh, terrible hack.  all onchange
+                                        #callbacks should act the same
+  $onchange = 'onChange="'. $onchange. '"';
+}
+
+my $contact;
+if ( $curr_value ) {
+  $contact = qsearchs('contact', { 'contactnum' => $curr_value } );
+} else {
+  $contact = new FS::contact {};
+}
+
+</%init>
index 22e872e..495923c 100644 (file)
@@ -39,6 +39,12 @@ Example:
     <% include('init_overlib.html') |n %>
 
     <SCRIPT TYPE="text/javascript">
+
+      function clearhint_search_prospect (what) {
+        if ( what.value == '<% $prospect_label |n %>' )
+          what.value = '';
+      }
+
       function clearhint_search_cust (what) {
         if ( what.value == '<% $cust_label |n %>' )
           what.value = '';
@@ -63,6 +69,7 @@ Example:
         if ( what.value == '<% $ticketing_label |n %>' )
           what.value = '';
       }
+
     </SCRIPT>
 
     <% $head |n %>
@@ -140,6 +147,7 @@ input.fstext {
          vertical-align:bottom;
          text-align:right;
          font-family: Arial,Verdana,Helvetica,sans-serif;
+         font-size: 13px;
          padding-left: 0px;
          padding-right: 0px;
          padding-top: 0px;
@@ -154,14 +162,14 @@ input.fstext {
 
     <TABLE WIDTH="100%" CELLSPACING=0 CELLPADDING=0>
       <TR>
-        <TD COLSPAN=6 WIDTH="100%" STYLE="padding:0"><IMG BORDER=0 ALT="" SRC="<%$fsurl%>images/black-gradient.png" HEIGHT="13" WIDTH="100%"></TD>
+        <TD COLSPAN="7" WIDTH="100%" STYLE="padding:0"><IMG BORDER=0 ALT="" SRC="<%$fsurl%>images/black-gradient.png" HEIGHT="13" WIDTH="100%"></TD>
       </TR>
 
 % if ( $menu_position eq 'top' ) {
 
       <TR>
 
-        <TD COLSPAN="4" WIDTH="100%" STYLE="padding:0" BGCOLOR="#000000">
+        <TD COLSPAN="5" WIDTH="100%" STYLE="padding:0" BGCOLOR="#000000">
           <SCRIPT TYPE="text/javascript">
             document.write(myBar);
           </SCRIPT>
@@ -180,12 +188,12 @@ input.fstext {
       </TR>
 
       <TR>
-        <TD COLSPAN="6" WIDTH="100%" HEIGHT="2px" STYLE="padding:0" BGCOLOR="#000000">
+        <TD COLSPAN="7" WIDTH="100%" HEIGHT="2px" STYLE="padding:0" BGCOLOR="#000000">
         </TD>
       </TR>
       
       <TR>
-        <TD COLSPAN="6" WIDTH="100%" HEIGHT="4px" STYLE="padding:0" BGCOLOR="#000000">
+        <TD COLSPAN="7" WIDTH="100%" HEIGHT="4px" STYLE="padding:0" BGCOLOR="#000000">
         </TD>
       </TR>
 
@@ -193,7 +201,17 @@ input.fstext {
 
       <TR>
 
-        <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
+        <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right" STYLE="padding-left:2px">
+% if ( $curuser->access_right('List prospects') ) {
+          <FORM ACTION="<%$fsurl%>search/prospect_main.html" METHOD="GET" STYLE="margin:0">
+            <INPUT NAME="search_prospect" TYPE="text" VALUE="<% $prospect_label |n %>" STYLE="width:155px" onFocus="clearhint_search_prospect(this);" onClick="clearhint_search_prospect(this);" CLASS="fstext"><BR>
+            <A HREF="<%$fsurl%>search/report_prospect_main.html" STYLE="color: #ffffff; font-size: 11px">Adv</A>
+            <INPUT TYPE="submit" VALUE="Search prospects" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:11px;padding-left:1px;padding-right:1px"">
+          </FORM>
+% }
+        </TD>
+
+        <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right" STYLE="padding-left:2px">
 % if ( $curuser->access_right('List customers') ) {
           <FORM ACTION="<%$fsurl%>search/cust_main.cgi" METHOD="GET" STYLE="margin:0">
             <INPUT NAME="search_cust" TYPE="text" VALUE="<% $cust_label |n %>" STYLE="width:<%$cust_width%>px" onFocus="clearhint_search_cust(this);" onClick="clearhint_search_cust(this);" CLASS="fstext"><BR>
@@ -209,7 +227,7 @@ input.fstext {
               <INPUT TYPE="hidden" NAME="address2_on" VALUE="1">
               <INPUT NAME="address2_text" TYPE="text" VALUE="<% $address2_label |n %>" STYLE="width:67px" onFocus="clearhint_search_address2(this);" onClick="clearhint_search_address2(this);" CLASS="fstext">
               <BR>
-              <INPUT TYPE="submit" VALUE="Search units" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:11px;padding-left:2px;padding-right:2px">
+              <INPUT TYPE="submit" VALUE="Search units" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:11px;padding-left:1px;padding-right:1px;margin-top:3px">
             </FORM>
 % } 
         </TD>
@@ -217,30 +235,29 @@ input.fstext {
         <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
 % if ( $curuser->access_right('View invoices') ) { 
             <FORM ACTION="<%$fsurl%>search/cust_bill.html" METHOD="GET" STYLE="margin:0;display:inline">
-              <INPUT NAME="invnum" TYPE="text" VALUE="<% $inv_label |n %>" STYLE="width:64px" onFocus="clearhint_search_invoice(this);" onClick="clearhint_search_invoice(this);" CLASS="fstext">
+              <INPUT NAME="invnum" TYPE="text" VALUE="<% $inv_label |n %>" STYLE="width:56px" onFocus="clearhint_search_invoice(this);" onClick="clearhint_search_invoice(this);" CLASS="fstext">
 %   if ( $curuser->access_right('List invoices') ) { 
               <A HREF="<%$fsurl%>search/report_cust_bill.html" STYLE="color: #ffffff; font-size: 11px">Adv</A>\
 %   } 
-<BR>
-              <INPUT TYPE="submit" VALUE="Search invoices" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:11px;padding-left:2px;padding-right:2px">
+<BR><INPUT TYPE="submit" VALUE="Search invoices" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:11px;padding-left:1px;padding-right:1px;margin-top:3px">
             </FORM>
 % } 
         </TD>
 
-        <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
+        <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right" STYLE="padding-left:2px">
 % if ( $curuser->access_right('View customer services') ) {
           <FORM ACTION="<%$fsurl%>search/cust_svc.html" METHOD="GET" STYLE="margin:0">
-            <INPUT NAME="search_svc" TYPE="text" VALUE="<% $svc_label |n %>" STYLE="width:324px" onFocus="clearhint_search_svc(this);" onClick="clearhint_search_svc(this);" CLASS="fstext"><BR>
+            <INPUT NAME="search_svc" TYPE="text" VALUE="<% $svc_label |n %>" STYLE="width:271px" onFocus="clearhint_search_svc(this);" onClick="clearhint_search_svc(this);" CLASS="fstext"><BR>
             <A NOTYET="<%$fsurl%>search/svc_Smarter.html" STYLE="color: #000000; font-size:11px">Advanced</A>
             <INPUT TYPE="submit" VALUE="Search services" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:11px">
           </FORM>
 % }
         </TD>
 
-        <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right" STYLE="padding-left:4px;padding-right:4px">
+        <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right" STYLE="padding-left:2px;padding-right:2px">
 % if ( $conf->config("ticket_system") ) { 
           <FORM ACTION="<% FS::TicketSystem->baseurl %>index.html" METHOD="GET" STYLE="margin:0">
-            <INPUT NAME="q" TYPE="text" VALUE="<% $ticketing_label |n %>" STYLE="width:256px" onFocus="clearhint_search_ticket(this);" onClick="clearhint_search_ticket(this);" CLASS="fstext"><BR>
+            <INPUT NAME="q" TYPE="text" VALUE="<% $ticketing_label |n %>" STYLE="width:223px" onFocus="clearhint_search_ticket(this);" onClick="clearhint_search_ticket(this);" CLASS="fstext"><BR>
             <A HREF="<% FS::TicketSystem->baseurl %>Search/Build.html" STYLE="color: #ffffff; font-size:11px">Advanced</A>
             <INPUT TYPE="submit" VALUE="Search tickets" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:11px">
           </FORM>
@@ -334,11 +351,13 @@ if ( scalar(@agentnums) == 1 ) {
   $company_name = $conf->config('company_name');
 }
 
-my $cust_width = 288; #251 #ok for IE, slightly bigger for windows firefox
+my $prospect_label = '(name, company or phone)';
+
+my $cust_width = 246;
 my $cust_label = '(cust #, name, company';
 if ( $conf->exists('address1-search') ) {
   $cust_label .= ', address';
-  $cust_width += 64;
+  $cust_width += 56;
 }
 $cust_label .= ' or contact phone)';
 
index caf2274..ce0278f 100644 (file)
@@ -476,6 +476,8 @@ if ( $conf->config('ticket_system') ) {
       'Ticketing start page',
     ],
 }
+$menu{'New prospect'} = [ $fsurl.'edit/prospect_main.html', 'Add a new prospect' ]
+  if $curuser->access_right('New prospect');
 $menu{'New customer'} = [ $fsurl.'edit/cust_main.cgi', 'Add a new customer' ]
   if $curuser->access_right('New customer');
 $menu{'Reports'} = [ \%report_menu, 'Lists, reporting and graphing' ]
diff --git a/httemplate/elements/tr-contact.html b/httemplate/elements/tr-contact.html
new file mode 100644 (file)
index 0000000..ee0e6e8
--- /dev/null
@@ -0,0 +1,24 @@
+%   unless ( $opt{'js_only'} ) {
+
+      <% include('tr-td-label.html', %opt) %>
+        <TD <% $cell_style %>>
+
+%   }
+%
+            <% include( '/elements/contact.html', %opt ) %>
+%
+%   unless ( $opt{'js_only'} ) {
+
+        </TD>
+      </TR>
+
+%   }
+<%init>
+
+my( %opt ) = @_;
+
+my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+
+$opt{'label'} ||= 'Contact';
+
+</%init>
index ab043ee..5e938b5 100644 (file)
@@ -4,7 +4,13 @@ Example:
 
   include('/elements/tr-select-cust_location.html',
             'cgi'       => $cgi,
-            'cust_main' => $cust_main,
+
+            'cust_main'     => $cust_main,
+            #or
+            'prospect_main' => $prospect_main,
+
+            #optional
+            'empty_label'   => '(default service address)',
          )
 
 </%doc>
@@ -42,6 +48,7 @@ Example:
     } else {
 
       if ( locationnum == 0 ) {
+%       if ( $cust_main ) {
         what.form.address1.value = <% $cust_main->get($prefix.'address1') |js_string %>;
         what.form.address2.value = <% $cust_main->get($prefix.'address2') |js_string %>;
         what.form.city.value = <% $cust_main->get($prefix.'city') |js_string %>;
@@ -54,16 +61,33 @@ Example:
                                             <% $cust_main->get($prefix.'county') | js_string %>
                                           )
                        );
+%       }
 
       } else {
         get_location( locationnum, update_location );
       } 
 
+%     if ( $editable ) {
+        if ( locationnum == 0 ) {
+%     }
+
 %#sleep/wait until dropdowns are updated?
-%     for (@location_fields, 'city_select') { 
-        what.form.<%$_%>.disabled = true;
-        what.form.<%$_%>.style.backgroundColor = '#dddddd';
-%     } 
+%         for (@location_fields, 'city_select') { 
+            what.form.<%$_%>.disabled = true;
+            what.form.<%$_%>.style.backgroundColor = '#dddddd';
+%         } 
+
+%     if ( $editable ) {
+        } else {
+
+%#sleep/wait until dropdowns are updated?
+%         for (@location_fields, 'city_select') { 
+            what.form.<%$_%>.disabled = false;
+            what.form.<%$_%>.style.backgroundColor = '#ffffff';
+%         } 
+
+        }
+%     }
 
     }
   }
@@ -122,15 +146,20 @@ Example:
   <TH ALIGN="right">Service&nbsp;location</TH>
   <TD COLSPAN=7>
     <SELECT NAME="locationnum" onChange="locationnum_changed(this);">
-      <OPTION VALUE="">(default service address)
-%     foreach my $loc ( $cust_main->cust_location ) {
+      <OPTION VALUE=""><% $opt{'empty_label'} || '(default service address)' |h %>
+%     my @locations = $cust_main ? $cust_main->cust_location : ();
+%     push @locations, $cust_location
+%       if !$cust_main && $cust_location && $cust_location->locationnum>0;
+%     foreach my $loc ( @locations ) {
         <OPTION VALUE="<% $loc->locationnum %>"
                 <% $locationnum == $loc->locationnum ? 'SELECTED' : '' %>
         ><% $loc->line |h %>
 %     }
-      <OPTION VALUE="-1"
-              <% $locationnum == -1 ? 'SELECTED' : '' %>
-      >Add new location
+%     if ( $addnew ) {
+        <OPTION VALUE="-1"
+                <% $locationnum == -1 ? 'SELECTED' : '' %>
+        >Add new location
+%     }
     </SELECT>
   </TD>
 </TR>
@@ -138,7 +167,7 @@ Example:
 <% include('/elements/location.html',
              'object'       => $cust_location,
              #'onchange' ?  probably not
-             'disabled'     => ( $locationnum == -1 ? '' : 'DISABLED' ),
+             'disabled'     => $disabled,
              'no_asterisks' => 1,
           )
 %>
@@ -156,13 +185,25 @@ my $statedefault = $conf->config('statedefault')
                    || ($countrydefault eq 'US' ? 'CA' : '');
 
 my %opt = @_;
-my $cgi       = $opt{'cgi'};
-my $cust_main = $opt{'cust_main'};
+my $cgi           = $opt{'cgi'};
+my $cust_main     = $opt{'cust_main'};
+my $prospect_main = $opt{'prospect_main'};
+
+my $prefix = ($cust_main && length($cust_main->ship_last)) ? 'ship_' : '';
 
-my $prefix = length($cust_main->ship_last) ? 'ship_' : '';
+my $locationnum;
+if ( length($opt{'curr_value'}) ) {
+  $locationnum = $opt{'curr_value'};
+} else {
+  $cgi->param('locationnum') =~ /^(\-?\d*)$/ or die "illegal locationnum";
+  $locationnum = $1;
+}
+
+#probably could use explicit controls
+# (cust_main locations not editable for tax reasons)
+my $editable = $cust_main ? 0 : 1; #could use explicit control
+my $addnew = $cust_main ? 1 : ( $locationnum>0 ? 0 : 1 );
 
-$cgi->param('locationnum') =~ /^(\-?\d*)$/ or die "illegal locationnum";
-my $locationnum = $1;
 my $cust_location;
 if ( $locationnum && $locationnum != -1 ) {
   $cust_location = qsearchs('cust_location', { 'locationnum' => $locationnum } )
@@ -171,9 +212,13 @@ if ( $locationnum && $locationnum != -1 ) {
   $cust_location = new FS::cust_location;
   if ( $locationnum == -1 ) {
     $cust_location->$_( $cgi->param($_) ) foreach @location_fields;
-  } else {
+  } elsif ( $cust_main ) {
     $cust_location->$_( $cust_main->get($prefix.$_) ) foreach @location_fields;
   }
 }
 
+my $disabled = ( $locationnum == -1 || ($editable && $locationnum) )
+                 ? ''
+                 : 'DISABLED';
+
 </%init>
index 419c59f..82ad636 100644 (file)
@@ -3,12 +3,23 @@
 
 my $locationnum = $cgi->param('arg');
 
+my $curuser = $FS::CurrentUser::CurrentUser;
+
 my $cust_location = qsearchs({
   'select'    => 'cust_location.*',
   'table'     => 'cust_location',
   'hashref'   => { 'locationnum' => $locationnum },
-  'addl_from' => 'LEFT JOIN cust_main USING ( custnum )',
-  'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+  'addl_from' => ' LEFT JOIN cust_main     USING ( custnum     ) ',
+                 ' LEFT JOIN prospect_main USING ( prospectnum ) ',
+  'extra_sql' => ' AND ( '.
+                       ' ( custnum IS NOT NULL AND '.
+                           $curuser->agentnums_sql( table=>'cust_main' ).
+                       ' ) '.
+                       ' OR '.
+                       ' ( prospectnum IS NOT NULL AND '.
+                           $curuser->agentnums_sql( table=>'prospect_main' ).
+                       ' ) '.
+                     ' )',
 });
 
 my %hash = ();
diff --git a/httemplate/search/prospect_main.html b/httemplate/search/prospect_main.html
new file mode 100644 (file)
index 0000000..12e3e18
--- /dev/null
@@ -0,0 +1,74 @@
+<% include('elements/search.html',
+             'title'         => 'Prospect Search Results',
+             'name_singular' => 'prospect',
+             'query'         => $query,
+             'count_query'   => $count_query,
+             'header'        => [ '#',
+                                  'Prospect',
+                                  'Contact(s)',
+                                ],
+             'fields'        => [ 'prospectnum',
+                                  'company',
+                                  sub {
+                                    my $pm = shift;
+                                    [ map {
+                                            [ { 'data' => $_->line, }, ];
+                                          }
+                                          $pm->contact
+                                    ];
+                                  },
+                                ],
+             'links'         => [ '',
+                                  $link,
+                                  '', #link to contact edit???
+                                ],
+             'agent_virt'    => 1,
+          )
+%>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('List prospects');
+
+my %search_hash = ();
+
+#$search_hash{'query'} = $cgi->keywords;
+
+#scalars
+my @scalars = qw (
+  agentnum 
+);
+
+for my $param ( @scalars ) {
+  $search_hash{$param} = scalar( $cgi->param($param) )
+    if $cgi->param($param);
+}
+
+#lists
+#for my $param () {
+#  $search_hash{$param} = [ $cgi->param($param) ];
+#}
+
+# parse dates
+#foreach my $field (qw( signupdate )) {
+#
+#  my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field);
+#
+#  next if $beginning == 0 && $ending == 4294967295;
+#       #or $disable{$cgi->param('status')}->{$field};
+#
+#  $search_hash{$field} = [ $beginning, $ending ];
+#
+#}
+
+my $query = FS::prospect_main->search(\%search_hash);
+my $count_query   = delete($query->{'count_query'});
+#my @extra_headers = @{ delete($query->{'extra_headers'}) };
+#my @extra_fields  = @{ delete($query->{'extra_fields'})  };
+
+my $link = sub {
+  my $prospect_main = shift;
+  [ "${p}view/prospect_main.html?", 'prospectnum' ];
+};
+
+</%init>
diff --git a/httemplate/search/report_prospect_main.html b/httemplate/search/report_prospect_main.html
new file mode 100644 (file)
index 0000000..5e38343
--- /dev/null
@@ -0,0 +1,32 @@
+<% include('/elements/header.html', 'Prospect Report' ) %>
+
+<FORM ACTION="prospect_main.html" METHOD="GET">
+
+  <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
+
+    <TR>
+      <TH BGCOLOR="#e8e8e8" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH>
+    </TR>
+
+    <% include( '/elements/tr-select-agent.html',
+                  'curr_value'    => scalar($cgi->param('agentnum')),
+                  'disable_empty' => 0,
+               )
+    %>
+
+  </TABLE>
+
+<BR>
+<INPUT TYPE="submit" VALUE="Get Report">
+
+</FORM>
+
+<% include('/elements/footer.html') %>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('List prospects');
+
+my $conf = new FS::Conf;
+
+</%init>
index 9f348e5..41155cb 100644 (file)
@@ -4,12 +4,16 @@
   <I><FONT SIZE=-1>(default service address)</FONT><BR>
 % }
 
-  <% $loc->location_label( 'join_string' => '<BR>',
+  <% $loc->location_label( 'join_string'     => '<BR>',
+                           'double_space'    => ' &nbsp; ',
                            'escape_function' => \&encode_entities,
+                           'countrydefault'  => $countrydefault,
                          )
   %>
 
+% unless ( $cust_pkg->locationnum ) {
   </I>
+% }
 
 % if ( ! $cust_pkg->get('cancel')
 %      && $FS::CurrentUser::CurrentUser->access_right('Change customer package')
diff --git a/httemplate/view/prospect_main.html b/httemplate/view/prospect_main.html
new file mode 100644 (file)
index 0000000..de446a9
--- /dev/null
@@ -0,0 +1,92 @@
+<% include('/elements/header.html',
+             'Prospect View: '. $prospect_main->company
+          )
+%>
+
+% if ( $curuser->access_right('Edit prospect') ) { 
+  <A HREF="<% $p %>edit/prospect_main.html?<% $prospectnum %>">Edit this prospect</A>
+% } 
+
+<% ntable("#cccccc",2) %>
+
+<TR>
+  <TD ALIGN="right">Prospect #</TD>
+  <TD BGCOLOR="#FFFFFF"><B><% $prospectnum %></B></TD>
+</TR>
+
+%unless ( scalar(@agentnums) == 1 ) {
+%  my $agent = qsearchs('agent',{ 'agentnum' => $prospect_main->agentnum } );
+   <TR>
+     <TD ALIGN="right">Agent</TD>
+     <TD BGCOLOR="#ffffff"><% $agent->agentnum %>: <% $agent->agent %></TD>
+   </TR>
+%}
+
+<TR>
+  <TD ALIGN="right">Company</TD>
+  <TD BGCOLOR="#FFFFFF"><B><% $prospect_main->company |h %></B></TD>
+</TR>
+
+% foreach my $contact ( $prospect_main->contact ) {
+    <TR>
+      <TD ALIGN="right">Contact</TD>
+      <TD BGCOLOR="#FFFFFF"><% $contact->line %></TD>
+    </TR>
+%}
+
+% my @cust_location =
+%   qsearch('cust_location', { 'prospectnum' => $prospectnum } );
+% #but only one, for now
+% foreach my $cust_location (@cust_location) {
+    <TR>
+      <TD ALIGN="right">Address</TD>
+      <TD BGCOLOR="#FFFFFF">
+        <% $cust_location->location_label(
+             'join_string'     => '<BR>',
+             'double_space'    => ' &nbsp; ',
+             'escape_function' => \&encode_entities,
+           )
+        %>
+      </TD>
+  </TR>
+% }
+
+</TABLE>
+
+<BR>
+
+<% ntable("#cccccc") %>
+
+<TR>
+  <TH BGCOLOR="#e8e8e8" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Tickets</FONT></TH>
+</TR>
+
+</TABLE>
+
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right('View prospect');
+
+my $prospectnum;
+if ( $cgi->param('prospectnum') =~ /^(\d+)$/ ) {
+  $prospectnum = $1;
+} else {
+  die "No prospect specified (bad URL)!" unless $cgi->keywords;
+  my($query) = $cgi->keywords; # needs parens with my, ->keywords returns array
+  $query =~ /^(\d+)$/;
+  $prospectnum = $1;
+}
+
+my $prospect_main = qsearchs( {
+  'table'     => 'prospect_main',
+  'hashref'   => { 'prospectnum' => $prospectnum },
+  'extra_sql' => ' AND '. $curuser->agentnums_sql,
+});
+die "Prospect not found!" unless $prospect_main;
+
+my @agentnums = $curuser->agentnums;
+
+</%init>