RT# 81961 Repair broken links in POD documentation
[freeside.git] / FS / FS / cust_main_Mixin.pm
index 7fcb5b5..097111f 100644 (file)
@@ -2,11 +2,12 @@ package FS::cust_main_Mixin;
 
 use strict;
 use vars qw( $DEBUG $me );
-use Carp qw( confess );
+use Carp qw( confess carp cluck );
 use FS::UID qw(dbh);
 use FS::cust_main;
 use FS::Record qw( qsearch qsearchs );
 use FS::Misc qw( send_email generate_email );
+use HTML::Entities;
 
 $DEBUG = 0;
 $me = '[FS::cust_main_Mixin]';
@@ -37,6 +38,7 @@ sub cust_linked { $_[0]->custnum; }
 
 sub cust_main { 
   my $self = shift;
+  cluck ref($self). '->cust_main called' if $DEBUG;
   $self->cust_linked ? qsearchs('cust_main', {custnum => $self->custnum}) : '';
 }
 
@@ -131,9 +133,12 @@ linked to a customer.
 
 sub country_full {
   my $self = shift;
-  $self->cust_linked
-    ? FS::cust_main::country_full($self)
-    : $self->cust_unlinked_msg;
+  if ( $self->locationnum ) {  # cust_pkg has this
+    my $location = FS::cust_location->by_key($self->locationnum);
+    $location ? $location->country_full : '';
+  } elsif ( $self->cust_linked ) {
+    $self->cust_main->bill_country_full;
+  }
 }
 
 =item invoicing_list_emailonly
@@ -205,19 +210,9 @@ a customer.
 sub cust_status {
   my $self = shift;
   return $self->cust_unlinked_msg unless $self->cust_linked;
-
-  #FS::cust_main::status($self)
-  #false laziness w/actual cust_main::status
-  # (make sure FS::cust_main methods are called)
-  for my $status (qw( prospect active inactive suspended cancelled )) {
-    my $method = $status.'_sql';
-    my $sql = FS::cust_main->$method();;
-    my $numnum = ( $sql =~ s/cust_main\.custnum/?/g );
-    my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
-    $sth->execute( ($self->custnum) x $numnum )
-      or die "Error executing 'SELECT $sql': ". $sth->errstr;
-    return $status if $sth->fetchrow_arrayref->[0];
-  }
+  my $cust_main = $self->cust_main;
+  return $self->cust_unlinked_msg unless $cust_main;
+  return $cust_main->cust_status;
 }
 
 =item ucfirst_cust_status
@@ -253,6 +248,17 @@ sub cust_statuscolor {
     : '000000';
 }
 
+=item agent_name
+
+=cut
+
+sub agent_name {
+  my $self = shift;
+  $self->cust_linked
+    ? $self->cust_main->agent_name
+    : $self->cust_unlinked_msg;
+}
+
 =item prospect_sql
 
 =item active_sql
@@ -352,7 +358,7 @@ Queue job for status updates.  Required.
 
 =item search
 
-Hashref of params to the L<search()> method.  Required.
+Hashref of params to the L<FS::Record/search> method.  Required.
 
 =item msgnum
 
@@ -375,6 +381,12 @@ HTML body
 
 Text body
 
+=item to_contact_classnum
+
+The customer contact class (or classes, as a comma-separated list) to send
+the message to. If unspecified, will be sent to any contacts that are marked
+as invoice destinations (the equivalent of specifying 'invoice').
+
 =back
 
 Returns an error message, or false for success.
@@ -393,13 +405,21 @@ use Digest::SHA qw(sha1); # for duplicate checking
 sub email_search_result {
   my($class, $param) = @_;
 
+  my $conf = FS::Conf->new;
+  my $send_to_domain = $conf->config('email-to-voice_domain');
+
   my $msgnum = $param->{msgnum};
   my $from = delete $param->{from};
   my $subject = delete $param->{subject};
   my $html_body = delete $param->{html_body};
   my $text_body = delete $param->{text_body};
+  my $to_contact_classnum = delete $param->{to_contact_classnum};
+  my $emailtovoice_name = delete $param->{emailtovoice_contact};
+
   my $error = '';
 
+  my $to = $emailtovoice_name . '@' . $send_to_domain unless !$emailtovoice_name;
+
   my $job = delete $param->{'job'}
     or die "email_search_result must run from the job queue.\n";
   
@@ -410,6 +430,7 @@ sub email_search_result {
   }
 
   my $sql_query = $class->search($param->{'search'});
+  $sql_query->{'select'} = $sql_query->{'table'} . '.*';
 
   my $count_query   = delete($sql_query->{'count_query'});
   my $count_sth = dbh->prepare($count_query)
@@ -444,15 +465,28 @@ sub email_search_result {
       next; # unlinked object; nothing else we can do
     }
 
+    my %to = ( to => $to ) if $to;
+
     if ( $msg_template ) {
       # Now supports other context objects.
       %message = $msg_template->prepare(
         'cust_main' => $cust_main,
         'object'    => $obj,
+        'to_contact_classnum' => $to_contact_classnum,
+        %to
       );
-    }
-    else {
-      my @to = $cust_main->invoicing_list_emailonly;
+
+    } else {
+      # 3.x: false laziness with msg_template.pm; on 4.x, all email notices
+      # are generated from templates and this case goes away
+      my @classes;
+      if ( $to_contact_classnum ) {
+        @classes = ref($to_contact_classnum) ? @$to_contact_classnum : split(',', $to_contact_classnum);
+      }
+      if (!@classes) {
+        @classes = ( 'invoice' );
+      }
+      my @to = $to ? split(',', $to) : $cust_main->contact_list_email(@classes);
       next if !@to;
 
       %message = (
@@ -466,7 +500,7 @@ sub email_search_result {
     } #if $msg_template
 
     # For non-cust_main searches, we avoid duplicates based on message
-    # body text.  
+    # body text.
     my $unique = $cust_main->custnum;
     $unique .= sha1($message{'text_body'}) if $class ne 'FS::cust_main';
     if( $sent_to{$unique} ) {
@@ -538,6 +572,7 @@ sub process_email_search_result {
   die "error loading FS::$table: $@\n" if $@;
 
   my $error = "FS::$table"->email_search_result( $param );
+  dbh->commit; # save failed jobs before rethrowing the error
   die $error if $error;
 
 }
@@ -577,6 +612,120 @@ sub mt {
   return $lh->maketext(@_);
 }
 
+=item time2str_local FORMAT, TIME[, ESCAPE]
+
+Localizes a date (see L<Date::Language>) for the customer's locale.
+
+FORMAT can be a L<Date::Format> string, or one of these special words:
+
+- "short": the value of the "date_format" config setting for the customer's 
+  locale, defaulting to "%x".
+- "rdate": the same as "short" except that the default has a four-digit year.
+- "long": the value of the "date_format_long" config setting for the 
+  customer's locale, defaulting to "%b %o, %Y".
+
+ESCAPE, if specified, is one of "latex" or "html", and will escape non-ASCII
+characters and convert spaces to nonbreaking spaces.
+
+=cut
+
+sub time2str_local {
+  # renamed so that we don't have to change every single reference to 
+  # time2str everywhere
+  my $self = shift;
+  my ($format, $time, $escape) = @_;
+  return '' unless $time > 0; # work around time2str's traditional stupidity
+
+  $self->{_date_format} ||= {};
+  if (!exists($self->{_dh})) {
+    my $cust_main = $self->cust_main;
+    my $locale = $cust_main->locale  if $cust_main;
+    $locale ||= 'en_US';
+    my %info = FS::Locales->locale_info($locale);
+    my $dh = eval { Date::Language->new($info{'name'}) } ||
+             Date::Language->new(); # fall back to English
+    $self->{_dh} = $dh;
+  }
+
+  if ($format eq 'short') {
+    $format = $self->{_date_format}->{short}
+            ||= $self->conf->config('date_format') || '%x';
+  } elsif ($format eq 'rdate') {
+    $format = $self->{_date_format}->{rdate}
+            ||= $self->conf->config('date_format') || '%m/%d/%Y';
+  } elsif ($format eq 'long') {
+    $format = $self->{_date_format}->{long}
+            ||= $self->conf->config('date_format_long') || '%b %o, %Y';
+  }
+
+  # actually render the date
+  my $string = $self->{_dh}->time2str($format, $time);
+
+  if ($escape) {
+    if ($escape eq 'html') {
+      $string = encode_entities($string);
+      $string =~ s/ +/&nbsp;/g;
+    } elsif ($escape eq 'latex') { # just do nbsp's here
+      $string =~ s/ +/~/g;
+    }
+  }
+  
+  $string;
+}
+
+=item unsuspend_balance
+
+If conf I<unsuspend_balance> is set and customer's current balance is
+beneath the set threshold, unsuspends customer packages.
+
+=cut
+
+sub unsuspend_balance {
+  my $self = shift;
+  my $cust_main = $self->cust_main;
+  my $conf = $self->conf;
+  my $setting = $conf->config('unsuspend_balance');
+  my $maxbalance;
+  if ($setting eq 'Zero') {
+    $maxbalance = 0;
+
+  # kind of a pain to load/check all cust_bill instead of just open ones,
+  # but if for some reason payment gets applied to later bills before
+  # earlier ones, we still want to consider the later ones as allowable balance
+  } elsif ($setting eq 'Latest invoice charges') {
+    my @cust_bill = $cust_main->cust_bill();
+    my $cust_bill = $cust_bill[-1]; #always want the most recent one
+    if ($cust_bill) {
+      $maxbalance = $cust_bill->charged || 0;
+    } else {
+      $maxbalance = 0;
+    }
+  } elsif ($setting eq 'Charges not past due') {
+    my $now = time;
+    $maxbalance = 0;
+    foreach my $cust_bill ($cust_main->cust_bill()) {
+      next unless $now <= ($cust_bill->due_date || $cust_bill->_date);
+      $maxbalance += $cust_bill->charged || 0;
+    }
+  } elsif (length($setting)) {
+    warn "Unrecognized unsuspend_balance setting $setting";
+    return;
+  } else {
+    return;
+  }
+  my $balance = $cust_main->balance || 0;
+  if ($balance <= $maxbalance) {
+    my @errors = $cust_main->unsuspend(
+                  'reason_type' => $conf->config('unsuspend_reason_type')
+                );
+    # side-fx with nested transactions?  upstack rolls back?
+    warn "WARNING:Errors unsuspending customer ". $cust_main->custnum. ": ".
+         join(' / ', @errors)
+      if @errors;
+  }
+  return;
+}
+
 =back
 
 =head1 BUGS