utf-8, RT#12514
[freeside.git] / FS / FS / cust_main.pm
index 8a043ae..22c16fb 100644 (file)
@@ -3,8 +3,9 @@ package FS::cust_main;
 require 5.006;
 use strict;
              #FS::cust_main:_Marketgear when they're ready to move to 2.1
-use base qw( FS::cust_main::Packages
+use base qw( FS::cust_main::Packages FS::cust_main::Status
              FS::cust_main::Billing FS::cust_main::Billing_Realtime
+             FS::cust_main::Billing_Discount
              FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
              FS::geocode_Mixin
              FS::Record
@@ -34,6 +35,7 @@ use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
 use FS::Misc qw( generate_email send_email generate_ps do_print );
 use FS::Msgcat qw(gettext);
 use FS::CurrentUser;
+use FS::TicketSystem;
 use FS::payby;
 use FS::cust_pkg;
 use FS::cust_svc;
@@ -63,7 +65,9 @@ use FS::type_pkgs;
 use FS::payment_gateway;
 use FS::agent_payment_gateway;
 use FS::banned_pay;
-use FS::TicketSystem;
+use FS::cust_main_note;
+use FS::cust_attachment;
+use FS::contact;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -365,7 +369,8 @@ invoicing_list destination to the newly-created svc_acct.  Here's an example:
 
   $cust_main->insert( {}, [ $email, 'POST' ] );
 
-Currently available options are: I<depend_jobnum>, I<noexport> and I<tax_exemption>.
+Currently available options are: I<depend_jobnum>, I<noexport>,
+I<tax_exemption> and I<prospectnum>.
 
 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
 on the supplied jobnum (they will not run until the specific job completes).
@@ -379,6 +384,8 @@ the B<reexport> method.)
 The I<tax_exemption> option can be set to an arrayref of tax names.
 FS::cust_main_exemption records will be created and inserted.
 
+If I<prospectnum> is set, moves contacts and locations from that prospect.
+
 =cut
 
 sub insert {
@@ -477,16 +484,41 @@ sub insert {
     }
   }
 
-  if ( $invoicing_list ) {
-    $error = $self->check_invoicing_list( $invoicing_list );
+  my $prospectnum = delete $options{'prospectnum'};
+  if ( $prospectnum ) {
+
+    warn "  moving contacts and locations from prospect $prospectnum\n"
+      if $DEBUG > 1;
+
+    my $prospect_main =
+      qsearchs('prospect_main', { 'prospectnum' => $prospectnum } );
+    unless ( $prospect_main ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Unknown prospectnum $prospectnum";
+    }
+    $prospect_main->custnum($self->custnum);
+    $prospect_main->disabled('Y');
+    my $error = $prospect_main->replace;
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
-      #return "checking invoicing_list (transaction rolled back): $error";
       return $error;
     }
-    $self->invoicing_list( $invoicing_list );
-  }
 
+    my @contact = $prospect_main->contact;
+    my @cust_location = $prospect_main->cust_location;
+    my @qual = $prospect_main->qual;
+
+    foreach my $r ( @contact, @cust_location, @qual ) {
+      $r->prospectnum('');
+      $r->custnum($self->custnum);
+      my $error = $r->replace;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
 
   warn "  setting cust_main_exemption\n"
     if $DEBUG > 1;
@@ -1246,6 +1278,8 @@ sub merge {
 
   }
 
+  my $name = $self->ship_name;
+
   my $locationnum = '';
   foreach my $cust_pkg ( $self->all_pkgs ) {
     $cust_pkg->custnum($new_custnum);
@@ -1271,6 +1305,22 @@ sub merge {
       $dbh->rollback if $oldAutoCommit;
       return $error;
     }
+
+    # add customer (ship) name to svc_phone.phone_name if blank
+    my @cust_svc = $cust_pkg->cust_svc;
+    foreach my $cust_svc (@cust_svc) {
+      my($label, $value, $svcdb) = $cust_svc->label;
+      next unless $svcdb eq 'svc_phone';
+      my $svc_phone = $cust_svc->svc_x;
+      next if $svc_phone->phone_name;
+      $svc_phone->phone_name($name);
+      my $error = $svc_phone->replace;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
   }
 
   #not considered:
@@ -1403,6 +1453,17 @@ sub replace {
     return "You are not permitted to create complimentary accounts.";
   }
 
+  if ( $old->get('geocode') && $old->get('geocode') eq $self->get('geocode')
+       && $conf->exists('enable_taxproducts')
+     )
+  {
+    my $pre = ($conf->exists('tax-ship_address') && $self->ship_zip)
+                ? 'ship_' : '';
+    $self->set('geocode', '')
+      if $old->get($pre.'zip') ne $self->get($pre.'zip')
+      && length($self->get($pre.'zip')) >= 10;
+  }
+
   local($ignore_expired_card) = 1
     if $old->payby  =~ /^(CARD|DCRD)$/
     && $self->payby =~ /^(CARD|DCRD)$/
@@ -2001,6 +2062,18 @@ sub cust_location {
   qsearch('cust_location', { 'custnum' => $self->custnum } );
 }
 
+=item cust_contact
+
+Returns all contacts (see L<FS::contact>) for this customer.
+
+=cut
+
+#already used :/ sub contact {
+sub cust_contact {
+  my $self = shift;
+  qsearch('contact', { 'custnum' => $self->custnum } );
+}
+
 =item unsuspend
 
 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
@@ -2179,13 +2252,14 @@ Returns all notes (see L<FS::cust_main_note>) for this customer.
 =cut
 
 sub notes {
-  my $self = shift;
-  #order by?
+  my($self,$orderby_classnum) = (shift,shift);
+  my $orderby = "_DATE DESC";
+  $orderby = "CLASSNUM ASC, $orderby" if $orderby_classnum;
   qsearch( 'cust_main_note',
            { 'custnum' => $self->custnum },
-          '',
-          'ORDER BY _DATE DESC'
-        );
+           '',
+           "ORDER BY $orderby",
+         );
 }
 
 =item agent
@@ -3778,6 +3852,9 @@ Returns a status string for this customer, currently:
 
 =back
 
+Behavior of inactive vs. cancelled edge cases can be adjusted with the
+cust_main-status_module configuration option.
+
 =cut
 
 sub status { shift->cust_status(@_); }
@@ -3815,21 +3892,11 @@ Returns a hex triplet color string for this customer's status.
 
 =cut
 
-use vars qw(%statuscolor);
-tie %statuscolor, 'Tie::IxHash',
-  'prospect'  => '7e0079', #'000000', #black?  naw, purple
-  'active'    => '00CC00', #green
-  'ordered'   => '009999', #teal? cyan?
-  'suspended' => 'FF9900', #yellow
-  'cancelled' => 'FF0000', #red
-  'inactive'  => '0000CC', #blue
-;
-
 sub statuscolor { shift->cust_statuscolor(@_); }
 
 sub cust_statuscolor {
   my $self = shift;
-  $statuscolor{$self->cust_status};
+  __PACKAGE__->statuscolors->{$self->cust_status};
 }
 
 =item tickets
@@ -3925,8 +3992,8 @@ Class method that returns the list of possible status strings for customers
 =cut
 
 sub statuses {
-  #my $self = shift; #could be class...
-  keys %statuscolor;
+  my $self = shift;
+  keys %{ $self->statuscolors };
 }
 
 =item cust_status_sql
@@ -3970,13 +4037,14 @@ sub prospect_sql {
 =item ordered_sql
 
 Returns an SQL expression identifying ordered cust_main records (customers with
-recurring packages not yet setup).
+no active packages, but recurring packages not yet setup or one time charges
+not yet billed).
 
 =cut
 
 sub ordered_sql {
   FS::cust_main->none_active_sql.
-  " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->ordered_sql. " ) ";
+  " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->not_yet_billed_sql. " ) ";
 }
 
 =item active_sql
@@ -4035,22 +4103,7 @@ Returns an SQL expression identifying cancelled cust_main records.
 
 =cut
 
-sub cancelled_sql { cancel_sql(@_); }
-sub cancel_sql {
-
-  my $recurring_sql = FS::cust_pkg->recurring_sql;
-  my $cancelled_sql = FS::cust_pkg->cancelled_sql;
-
-  "
-        0 < ( $select_count_pkgs )
-    AND 0 < ( $select_count_pkgs AND $recurring_sql AND $cancelled_sql   )
-    AND 0 = ( $select_count_pkgs AND $recurring_sql
-                  AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
-            )
-  ";
-#    AND 0 = (  $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
-
-}
+sub cancel_sql { shift->cancelled_sql(@_); }
 
 =item uncancel_sql
 =item uncancelled_sql
@@ -4684,7 +4737,7 @@ sub _agent_plandata {
         " ORDER BY
            CASE WHEN part_event_condition_option.optionname IS NULL
            THEN -1
-          ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
+           ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
         " END
           , part_event.weight".
         " LIMIT 1"