tax engine refactoring for Avalara and Billsoft tax vendors, #25718
[freeside.git] / FS / FS / cust_main_Mixin.pm
index e8e243f..4b151e7 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
@@ -388,6 +393,7 @@ in the job fail, the entire job will abort and return an error.
 use Storable qw(thaw);
 use MIME::Base64;
 use Data::Dumper qw(Dumper);
+use Digest::SHA qw(sha1); # for duplicate checking
 
 sub email_search_result {
   my($class, $param) = @_;
@@ -409,6 +415,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)
@@ -438,32 +445,23 @@ sub email_search_result {
     }
 
     my $cust_main = $obj->cust_main;
-    my @message;
+    tie my %message, 'Tie::IxHash';
     if ( !$cust_main ) { 
       next; # unlinked object; nothing else we can do
     }
 
-    if( $sent_to{$cust_main->custnum} ) {
-      # avoid duplicates
-      $dups++;
-      next;
-    }
-
-    $sent_to{$cust_main->custnum} = 1;
-    
     if ( $msg_template ) {
-      # XXX add support for other context objects?
-      # If we do that, handling of "duplicates" will 
-      # have to be smarter.  Currently we limit to 
-      # one message per custnum because they'd all
-      # be identical.
-      @message = $msg_template->prepare( 'cust_main' => $cust_main );
+      # Now supports other context objects.
+      %message = $msg_template->prepare(
+        'cust_main' => $cust_main,
+        'object'    => $obj,
+      );
     }
     else {
       my @to = $cust_main->invoicing_list_emailonly;
       next if !@to;
 
-      @message = (
+      %message = (
         'from'      => $from,
         'to'        => \@to,
         'subject'   => $subject,
@@ -473,7 +471,19 @@ sub email_search_result {
       );
     } #if $msg_template
 
-    $error = send_email( generate_email( @message ) );
+    # For non-cust_main searches, we avoid duplicates based on message
+    # body text.  
+    my $unique = $cust_main->custnum;
+    $unique .= sha1($message{'text_body'}) if $class ne 'FS::cust_main';
+    if( $sent_to{$unique} ) {
+      # avoid duplicates
+      $dups++;
+      next;
+    }
+
+    $sent_to{$unique} = 1;
+    
+    $error = send_email( generate_email( %message ) );
 
     if($error) {
       # queue the sending of this message so that the user can see what we
@@ -484,7 +494,7 @@ sub email_search_result {
         'status'     => 'failed',
         'statustext' => $error,
       };
-      $queue->insert(@message);
+      $queue->insert(%message);
       push @retry_jobs, $queue;
     }
     else {
@@ -516,7 +526,7 @@ sub process_email_search_result {
   my $job = shift;
   #warn "$me process_re_X $method for job $job\n" if $DEBUG;
 
-  my $param = thaw(decode_base64(shift));
+  my $param = shift;
   warn Dumper($param) if $DEBUG;
 
   $param->{'job'} = $job;
@@ -534,10 +544,107 @@ 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;
 
 }
 
+=item conf
+
+Returns a configuration handle (L<FS::Conf>) set to the customer's locale, 
+if they have one.  If not, returns an FS::Conf with no locale.
+
+=cut
+
+sub conf {
+  my $self = shift;
+  return $self->{_conf} if (ref $self and $self->{_conf});
+  my $cust_main = $self->cust_main;
+  my $conf = new FS::Conf { 
+    'locale' => ($cust_main ? $cust_main->locale : '')
+  };
+  $self->{_conf} = $conf if ref $self;
+  return $conf;
+}
+
+=item mt TEXT [, ARGS ]
+
+Localizes a text string (see L<Locale::Maketext>) for the customer's locale,
+if they have one.
+
+=cut
+
+sub mt {
+  my $self = shift;
+  return $self->{_lh}->maketext(@_) if (ref $self and $self->{_lh});
+  my $cust_main = $self->cust_main;
+  my $locale = $cust_main ? $cust_main->locale : '';
+  my $lh = FS::L10N->get_handle($locale);
+  $self->{_lh} = $lh if ref $self;
+  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;
+}
+
 =back
 
 =head1 BUGS