internal doc updates, #30003
[freeside.git] / FS / FS / cust_main.pm
index 68c2bd7..9a83099 100644 (file)
@@ -6,9 +6,11 @@ use base qw( FS::cust_main::Packages FS::cust_main::Status
              FS::cust_main::NationalID
              FS::cust_main::Billing FS::cust_main::Billing_Realtime
              FS::cust_main::Billing_Discount
+             FS::cust_main::Billing_ThirdParty
              FS::cust_main::Location
+             FS::cust_main::Credit_Limit
              FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
-             FS::geocode_Mixin FS::Quotable_Mixin
+             FS::geocode_Mixin FS::Quotable_Mixin FS::Sales_Mixin
              FS::o2m_Common
              FS::Record
            );
@@ -390,7 +392,7 @@ sub insert {
 
     $payby = 'PREP' if $amount;
 
-  } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
+  } elsif ( $self->payby =~ /^(CASH|WEST|MCRD|PPAL)$/ ) {
 
     $payby = $1;
     $self->payby('BILL');
@@ -1276,13 +1278,14 @@ sub merge {
   }
 
   tie my %financial_tables, 'Tie::IxHash',
-    'cust_bill'      => 'invoices',
-    'cust_bill_void' => 'voided invoices',
-    'cust_statement' => 'statements',
-    'cust_credit'    => 'credits',
-    'cust_pay'       => 'payments',
-    'cust_pay_void'  => 'voided payments',
-    'cust_refund'    => 'refunds',
+    'cust_bill'         => 'invoices',
+    'cust_bill_void'    => 'voided invoices',
+    'cust_statement'    => 'statements',
+    'cust_credit'       => 'credits',
+    'cust_credit_void'  => 'voided credits',
+    'cust_pay'          => 'payments',
+    'cust_pay_void'     => 'voided payments',
+    'cust_refund'       => 'refunds',
   ;
    
   foreach my $table ( keys %financial_tables ) {
@@ -1509,43 +1512,17 @@ sub replace {
     my $old_loc = $old->$l;
     my $new_loc = $self->$l;
 
-    if ( !$new_loc->locationnum ) {
-      # changing location
-      # If the new location is all empty fields, or if it's identical to 
-      # the old location in all fields, don't replace.
-      my @nonempty = grep { $new_loc->$_ } $self->location_fields;
-      next if !@nonempty;
-      my @unlike = grep { $new_loc->$_ ne $old_loc->$_ } $self->location_fields;
-
-      if ( @unlike or $old_loc->disabled ) {
-        warn "  changed $l fields: ".join(',',@unlike)."\n"
-          if $DEBUG;
-        $new_loc->set(custnum => $self->custnum);
-
-        # insert it--the old location will be disabled later
-        my $error = $new_loc->insert;
-        if ( $error ) {
-          $dbh->rollback if $oldAutoCommit;
-          return $error;
-        }
-
-      } else {
-      # no fields have changed and $old_loc isn't disabled, so don't change it
-        next;
-      }
-
-    }
-    elsif ( $new_loc->custnum ne $self->custnum or $new_loc->prospectnum ) {
+    # find the existing location if there is one
+    $new_loc->set('custnum' => $self->custnum);
+    my $error = $new_loc->find_or_insert;
+    if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
-      return "$l belongs to customer ".$new_loc->custnum;
+      return $error;
     }
-    # else the new location belongs to this customer so we're good
-
-    # set the foo_locationnum now that we have one.
     $self->set($l.'num', $new_loc->locationnum);
-
   } #for $l
 
+  # replace the customer record
   my $error = $self->SUPER::replace($old);
 
   if ( $error ) {
@@ -1720,13 +1697,25 @@ sub queue_fuzzyfiles_update {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  foreach my $field ( 'first', 'last', 'company', 'ship_company' ) {
+    my $queue = new FS::queue { 
+      'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
+    };
+    my @args = "cust_main.$field", $self->get($field);
+    my $error = $queue->insert( @args );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "queueing job (transaction rolled back): $error";
+    }
+  }
+
   my @locations = $self->bill_location;
   push @locations, $self->ship_location if $self->has_ship_address;
   foreach my $location (@locations) {
     my $queue = new FS::queue { 
-      'job' => 'FS::cust_main::Search::append_fuzzyfiles'
+      'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
     };
-    my @args = map $location->get($_), @FS::cust_main::Search::fuzzyfields;
+    my @args = 'cust_location.address1', $location->address1;
     my $error = $queue->insert( @args );
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
@@ -1761,14 +1750,18 @@ sub check {
     || $self->ut_foreign_key('bill_locationnum', 'cust_location','locationnum')
     || $self->ut_foreign_key('ship_locationnum', 'cust_location','locationnum')
     || $self->ut_foreign_keyn('classnum', 'cust_class', 'classnum')
+    || $self->ut_foreign_keyn('salesnum', 'sales', 'salesnum')
     || $self->ut_textn('custbatch')
     || $self->ut_name('last')
     || $self->ut_name('first')
     || $self->ut_snumbern('signupdate')
     || $self->ut_snumbern('birthdate')
+    || $self->ut_namen('spouse_last')
+    || $self->ut_namen('spouse_first')
     || $self->ut_snumbern('spouse_birthdate')
     || $self->ut_snumbern('anniversary_date')
     || $self->ut_textn('company')
+    || $self->ut_textn('ship_company')
     || $self->ut_anything('comments')
     || $self->ut_numbern('referral_custnum')
     || $self->ut_textn('stateid')
@@ -1778,17 +1771,20 @@ sub check {
     || $self->ut_floatn('credit_limit')
     || $self->ut_numbern('billday')
     || $self->ut_numbern('prorate_day')
-    || $self->ut_enum('edit_subject', [ '', 'Y' ] )
-    || $self->ut_enum('calling_list_exempt', [ '', 'Y' ] )
-    || $self->ut_enum('invoice_noemail', [ '', 'Y' ] )
+    || $self->ut_flag('edit_subject')
+    || $self->ut_flag('calling_list_exempt')
+    || $self->ut_flag('invoice_noemail')
+    || $self->ut_flag('message_noemail')
     || $self->ut_enum('locale', [ '', FS::Locales->locales ])
   ;
 
-  my $company = $self->company;
-  $company =~ s/^\s+//; 
-  $company =~ s/\s+$//; 
-  $company =~ s/\s+/ /g;
-  $self->company($company);
+  foreach (qw(company ship_company)) {
+    my $company = $self->get($_);
+    $company =~ s/^\s+//; 
+    $company =~ s/\s+$//; 
+    $company =~ s/\s+/ /g;
+    $self->set($_, $company);
+  }
 
   #barf.  need message catalogs.  i18n.  etc.
   $error .= "Please select an advertising source."
@@ -2020,7 +2016,8 @@ sub check {
 
   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
     return "Expiration date required"
-      unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
+      # shouldn't payinfo_check do this?
+      unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/;
     $self->paydate('');
   } else {
     my( $m, $y );
@@ -2048,9 +2045,17 @@ sub check {
   ) {
     $self->payname( $self->first. " ". $self->getfield('last') );
   } else {
-    $self->payname =~ /^([\w \,\.\-\'\&]+)$/
-      or return gettext('illegal_name'). " payname: ". $self->payname;
-    $self->payname($1);
+
+    if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
+      $self->payname =~ /^([\w \,\.\-\']*)$/
+        or return gettext('illegal_name'). " payname: ". $self->payname;
+      $self->payname($1);
+    } else {
+      $self->payname =~ /^([\w \,\.\-\'\&]*)$/
+        or return gettext('illegal_name'). " payname: ". $self->payname;
+      $self->payname($1);
+    }
+
   }
 
   return "Please select an invoicing locale"
@@ -2834,7 +2839,7 @@ UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
 L<Date::Parse> for conversion functions.  The empty string can be passed
 to disable that time constraint completely.
 
-Available options are:
+Accepts the same options as L<balance_date_sql>:
 
 =over 4
 
@@ -2842,6 +2847,12 @@ Available options are:
 
 set to true to disregard unapplied credits, payments and refunds outside the specified time period - by default the time period restriction only applies to invoices (useful for reporting, probably a bad idea for event triggering)
 
+=item cutoff
+
+An absolute cutoff time.  Payments, credits, and refunds I<applied> after this 
+time will be ignored.  Note that START_TIME and END_TIME only limit the date 
+range for invoices and I<unapplied> payments, credits, and refunds.
+
 =back
 
 =cut
@@ -3347,6 +3358,8 @@ reason, and a 'reason_type' option must be passed to indicate the
 FS::reason_type for the new reason.
 
 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
+Likewise for I<eventnum>, I<commission_agentnum>, I<commission_salesnum> and
+I<commission_pkgnum>.
 
 Any other options are passed to FS::cust_credit::insert.
 
@@ -3372,10 +3385,10 @@ sub credit {
     $cust_credit->set('reason', $reason)
   }
 
-  for (qw( addlinfo eventnum )) {
-    $cust_credit->$_( delete $options{$_} )
-      if exists($options{$_});
-  }
+  $cust_credit->$_( delete $options{$_} )
+    foreach grep exists($options{$_}),
+              qw( addlinfo eventnum ),
+              map "commission_$_", qw( agentnum salesnum pkgnum );
 
   $cust_credit->insert(%options);
 
@@ -3751,6 +3764,19 @@ sub cust_credit_pkgnum {
     );
 }
 
+=item cust_credit_void
+
+Returns all voided credits (see L<FS::cust_credit_void>) for this customer.
+
+=cut
+
+sub cust_credit_void {
+  my $self = shift;
+  map { $_ }
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_credit_void', { 'custnum' => $self->custnum } )
+}
+
 =item cust_pay
 
 Returns all the payments (see L<FS::cust_pay>) for this customer.
@@ -4095,6 +4121,16 @@ sub ship_contact_firstlast {
 #  code2country($self->country);
 #}
 
+sub bill_country_full {
+  my $self = shift;
+  code2country($self->bill_location->country);
+}
+
+sub ship_country_full {
+  my $self = shift;
+  code2country($self->ship_location->country);
+}
+
 =item county_state_county [ PREFIX ]
 
 Returns a string consisting of just the county, state and country.
@@ -4189,14 +4225,17 @@ sub cust_statuscolor {
   __PACKAGE__->statuscolors->{$self->cust_status};
 }
 
-=item tickets
+=item tickets [ STATUS ]
 
 Returns an array of hashes representing the customer's RT tickets.
 
+An optional status (or arrayref or hashref of statuses) may be specified.
+
 =cut
 
 sub tickets {
   my $self = shift;
+  my $status = ( @_ && $_[0] ) ? shift : '';
 
   my $num = $conf->config('cust_main-max_tickets') || 10;
   my @tickets = ();
@@ -4204,7 +4243,12 @@ sub tickets {
   if ( $conf->config('ticket_system') ) {
     unless ( $conf->config('ticket_system-custom_priority_field') ) {
 
-      @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) };
+      @tickets = @{ FS::TicketSystem->customer_tickets( $self->custnum,
+                                                        $num,
+                                                        undef,
+                                                        $status,
+                                                      )
+                  };
 
     } else {
 
@@ -4216,6 +4260,7 @@ sub tickets {
           @{ FS::TicketSystem->customer_tickets( $self->custnum,
                                                  $num - scalar(@tickets),
                                                  $priority,
+                                                 $status,
                                                )
            };
       }
@@ -4925,9 +4970,9 @@ sub queueable_print {
   my %opt = @_;
 
   my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
-    or die "invalid customer number: " . $opt{custvnum};
+    or die "invalid customer number: " . $opt{custnum};
 
-  my $error = $self->print( $opt{template} );
+  my $error = $self->print( { 'template' => $opt{template} } );
   die $error if $error;
 }
 
@@ -5026,6 +5071,9 @@ sub queued_bill {
   my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
   warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid
 
+  #without this errors don't get rolled back
+  $args{'fatal'} = 1; # runs from job queue, will be caught
+
   $cust_main->bill_and_collect( %args );
 }
 
@@ -5041,42 +5089,6 @@ sub process_bill_and_collect {
   $cust_main->bill_and_collect( %$param );
 }
 
-=item process_censustract_update CUSTNUM
-
-Queueable function to update the census tract to the current year (as set in 
-the 'census_year' configuration variable) and retrieve the new tract code.
-
-=cut
-
-sub process_censustract_update { 
-  eval "use FS::Misc::Geo qw(get_censustract)";
-  die $@ if $@;
-  my $custnum = shift;
-  my $cust_main = qsearchs( 'cust_main', { custnum => $custnum })
-      or die "custnum '$custnum' not found!\n";
-
-  my $new_year = $conf->config('census_year') or return;
-  my $new_tract = get_censustract({ $cust_main->location_hash }, $new_year);
-  if ( $new_tract =~ /^\d/ ) {
-    # then it's a tract code
-        $cust_main->set('censustract', $new_tract);
-    $cust_main->set('censusyear',  $new_year);
-
-    local($ignore_expired_card) = 1;
-    local($ignore_illegal_zip) = 1;
-    local($ignore_banned_card) = 1;
-    local($skip_fuzzyfiles) = 1;
-    local($import) = 1; #prevent automatic geocoding (need its own variable?)
-    my $error = $cust_main->replace;
-    die $error if $error;
-  }
-  else {
-    # it's an error message
-    die $new_tract;
-  }
-  return;
-}
-
 #starting to take quite a while for big dbs
 #   (JRNL: journaled so it only happens once per database)
 # - seq scan of h_cust_main (yuck), but not going to index paycvv, so
@@ -5139,6 +5151,8 @@ sub _upgrade_data { #class method
   local($skip_fuzzyfiles) = 1;
   local($import) = 1; #prevent automatic geocoding (need its own variable?)
 
+  FS::cust_main::Location->_upgrade_data(%opts);
+
   unless ( FS::upgrade_journal->is_done('cust_main__trimspaces') ) {
 
     foreach my $cust_main ( qsearch({
@@ -5160,8 +5174,6 @@ sub _upgrade_data { #class method
 
   $class->_upgrade_otaker(%opts);
 
-  FS::cust_main::Location->_upgrade_data(%opts);
-
 }
 
 =back