add logo_file support to welcome_letter and fix leaving temp files around for invoice...
[freeside.git] / FS / FS / cust_main.pm
index d8f525e..e107e6c 100644 (file)
@@ -57,6 +57,7 @@ use FS::cust_tax_location;
 use FS::part_pkg_taxrate;
 use FS::agent;
 use FS::cust_main_invoice;
+use FS::cust_tag;
 use FS::cust_credit_bill;
 use FS::cust_bill_pay;
 use FS::prepay_credit;
@@ -473,6 +474,30 @@ sub insert {
     $self->invoicing_list( $invoicing_list );
   }
 
+  warn "  setting customer tags\n"
+    if $DEBUG > 1;
+
+  foreach my $tagnum ( @{ $self->tagnum || [] } ) {
+    my $cust_tag = new FS::cust_tag { 'tagnum'  => $tagnum,
+                                      'custnum' => $self->custnum };
+    my $error = $cust_tag->insert;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  if ( $invoicing_list ) {
+    $error = $self->check_invoicing_list( $invoicing_list );
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      #return "checking invoicing_list (transaction rolled back): $error";
+      return $error;
+    }
+    $self->invoicing_list( $invoicing_list );
+  }
+
+
   warn "  setting cust_main_exemption\n"
     if $DEBUG > 1;
 
@@ -1356,23 +1381,13 @@ sub delete {
     }
   }
 
-  foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
-    qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
-  ) {
-    my $error = $cust_main_invoice->delete;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
-    }
-  }
-
-  foreach my $cust_main_exemption (
-    qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } )
-  ) {
-    my $error = $cust_main_exemption->delete;
-    if ( $error ) {
-      $dbh->rollback if $oldAutoCommit;
-      return $error;
+  foreach my $table (qw( cust_main_invoice cust_main_exemption cust_tag )) {
+    foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
+      my $error = $record->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
     }
   }
 
@@ -1478,6 +1493,28 @@ sub replace {
     $self->invoicing_list( $invoicing_list );
   }
 
+  if ( $self->exists('tagnum') ) { #so we don't delete these on edit by accident
+
+    #this could be more efficient than deleting and re-inserting, if it matters
+    foreach my $cust_tag (qsearch('cust_tag', {'custnum'=>$self->custnum} )) {
+      my $error = $cust_tag->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+    foreach my $tagnum ( @{ $self->tagnum || [] } ) {
+      my $cust_tag = new FS::cust_tag { 'tagnum'  => $tagnum,
+                                        'custnum' => $self->custnum };
+      my $error = $cust_tag->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
+
   my %options = @param;
 
   my $tax_exemption = delete $options{'tax_exemption'};
@@ -2475,6 +2512,31 @@ sub agent_name {
   $self->agent->agent;
 }
 
+=item cust_tag
+
+Returns any tags associated with this customer, as FS::cust_tag objects,
+or an empty list if there are no tags.
+
+=cut
+
+sub cust_tag {
+  my $self = shift;
+  qsearch('cust_tag', { 'custnum' => $self->custnum } );
+}
+
+=item part_tag
+
+Returns any tags associated with this customer, as FS::part_tag objects,
+or an empty list if there are no tags.
+
+=cut
+
+sub part_tag {
+  my $self = shift;
+  map $_->part_tag, $self->cust_tag; 
+}
+
+
 =item cust_class
 
 Returns the customer class, as an FS::cust_class object, or the empty string
@@ -5049,28 +5111,42 @@ sub _realtime_bop_result {
          && ! grep { $transaction->error_message =~ /$_/ }
                    $conf->config('emaildecline-exclude')
     ) {
-      my @templ = $conf->config('declinetemplate');
-      my $template = new Text::Template (
-        TYPE   => 'ARRAY',
-        SOURCE => [ map "$_\n", @templ ],
-      ) or return "($perror) can't create template: $Text::Template::ERROR";
-      $template->compile()
-        or return "($perror) can't compile template: $Text::Template::ERROR";
-
-      my $templ_hash = {
-        'company_name'    =>
-          scalar( $conf->config('company_name', $self->agentnum ) ),
-        'company_address' =>
-          join("\n", $conf->config('company_address', $self->agentnum ) ),
-        'error'           => $transaction->error_message,
-      };
 
-      my $error = send_email(
-        'from'    => $conf->config('invoice_from', $self->agentnum ),
-        'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
-        'subject' => 'Your payment could not be processed',
-        'body'    => [ $template->fill_in(HASH => $templ_hash) ],
-      );
+      # Send a decline alert to the customer.
+      my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
+      my $error = '';
+      if ( $msgnum ) {
+        # include the raw error message in the transaction state
+        $cust_pay_pending->setfield('error', $transaction->error_message);
+        my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
+        $error = $msg_template->send( 'cust_main' => $self,
+                                      'object'    => $cust_pay_pending );
+      }
+      else { #!$msgnum
+
+        my @templ = $conf->config('declinetemplate');
+        my $template = new Text::Template (
+          TYPE   => 'ARRAY',
+          SOURCE => [ map "$_\n", @templ ],
+        ) or return "($perror) can't create template: $Text::Template::ERROR";
+        $template->compile()
+          or return "($perror) can't compile template: $Text::Template::ERROR";
+
+        my $templ_hash = {
+          'company_name'    =>
+            scalar( $conf->config('company_name', $self->agentnum ) ),
+          'company_address' =>
+            join("\n", $conf->config('company_address', $self->agentnum ) ),
+          'error'           => $transaction->error_message,
+        };
+
+        my $error = send_email(
+          'from'    => $conf->config('invoice_from', $self->agentnum ),
+          'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
+          'subject' => 'Your payment could not be processed',
+          'body'    => [ $template->fill_in(HASH => $templ_hash) ],
+        );
+      }
 
       $perror .= " (also received error sending decline notification: $error)"
         if $error;
@@ -6199,7 +6275,7 @@ sub balance_date_range {
   my $self = shift;
   my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
             ') FROM cust_main WHERE custnum='. $self->custnum;
-  sprintf( "%.2f", $self->scalar_sql($sql) );
+  sprintf( '%.2f', $self->scalar_sql($sql) );
 }
 
 =item balance_pkgnum PKGNUM
@@ -7024,6 +7100,26 @@ sub cust_pay_pending {
            );
 }
 
+=item cust_pay_pending_attempt
+
+Returns all payment attempts / declined payments for this customer, as pending
+payments objects (see L<FS::cust_pay_pending>), with status "done" but without
+a corresponding payment (see L<FS::cust_pay>).
+
+=cut
+
+sub cust_pay_pending_attempt {
+  my $self = shift;
+  return $self->num_cust_pay_pending_attempt unless wantarray;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_pay_pending', {
+                                   'custnum' => $self->custnum,
+                                   'status'  => 'done',
+                                   'paynum'  => '',
+                                 },
+           );
+}
+
 =item num_cust_pay_pending
 
 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
@@ -7034,11 +7130,28 @@ cust_pay_pending method is used in a scalar context.
 
 sub num_cust_pay_pending {
   my $self = shift;
-  my $sql = " SELECT COUNT(*) FROM cust_pay_pending ".
-            "   WHERE custnum = ? AND status != 'done' ";
-  my $sth = dbh->prepare($sql) or die dbh->errstr;
-  $sth->execute($self->custnum) or die $sth->errstr;
-  $sth->fetchrow_arrayref->[0];
+  $self->scalar_sql(
+    " SELECT COUNT(*) FROM cust_pay_pending ".
+      " WHERE custnum = ? AND status != 'done' ",
+    $self->custnum
+  );
+}
+
+=item num_cust_pay_pending_attempt
+
+Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
+customer, with status "done" but without a corresp.  Also called automatically when the
+cust_pay_pending method is used in a scalar context.
+
+=cut
+
+sub num_cust_pay_pending_attempt {
+  my $self = shift;
+  $self->scalar_sql(
+    " SELECT COUNT(*) FROM cust_pay_pending ".
+      " WHERE custnum = ? AND status = 'done' AND paynum IS NULL",
+    $self->custnum
+  );
 }
 
 =item cust_refund
@@ -7415,7 +7528,8 @@ recurring packages not yet setup).
 =cut
 
 sub ordered_sql {
-  " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->ordered_sql. " ) ";
+  FS::cust_main->none_active_sql.
+  " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->ordered_sql. " ) ";
 }
 
 =item active_sql
@@ -7429,6 +7543,18 @@ sub active_sql {
   " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
 }
 
+=item none_active_sql
+
+Returns an SQL expression identifying cust_main records with no active
+recurring packages.  This includes customers of status prospect, ordered,
+inactive, and suspended.
+
+=cut
+
+sub none_active_sql {
+  " 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
+}
+
 =item inactive_sql
 
 Returns an SQL expression identifying inactive cust_main records (customers with
@@ -7436,11 +7562,10 @@ no active recurring packages, but otherwise unsuspended/uncancelled).
 
 =cut
 
-sub inactive_sql { "
-  0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
-  AND
-  0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
-"; }
+sub inactive_sql {
+  FS::cust_main->none_active_sql.
+  " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " ) ";
+}
 
 =item susp_sql
 =item suspended_sql
@@ -7451,11 +7576,10 @@ Returns an SQL expression identifying suspended cust_main records.
 
 
 sub suspended_sql { susp_sql(@_); }
-sub susp_sql { "
-    0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " )
-    AND
-    0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
-"; }
+sub susp_sql {
+  FS::cust_main->none_active_sql.
+  " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " ) ";
+}
 
 =item cancel_sql
 =item cancelled_sql
@@ -8697,6 +8821,9 @@ sub batch_charge {
 
 =item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
 
+Deprecated.  Use event notification and message templates 
+(L<FS::msg_template>) instead.
+
 Sends a templated email notification to the customer (see L<Text::Template>).
 
 OPTIONS is a hash and may include
@@ -8810,6 +8937,7 @@ I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or
 
 =cut
 
+# a lot like cust_bill::print_latex
 sub generate_letter {
   my ($self, $template, %options) = @_;
 
@@ -8873,6 +9001,17 @@ sub generate_letter {
   $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
 
   my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
+
+  my $lh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
+                           DIR      => $dir,
+                           SUFFIX   => '.eps',
+                           UNLINK   => 0,
+                         ) or die "can't open temp file: $!\n";
+  print $lh $conf->config_binary('logo.eps', $self->agentnum)
+    or die "can't write temp file: $!\n";
+  close $lh;
+  $letter_data{'logo_file'} = $lh->filename;
+
   my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
                            DIR      => $dir,
                            SUFFIX   => '.tex',
@@ -8882,7 +9021,8 @@ sub generate_letter {
   $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
   close $fh;
   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
-  return $1;
+  return ($1, $letter_data{'logo_file'});
+
 }
 
 =item print_ps TEMPLATE 
@@ -8893,8 +9033,12 @@ Returns an postscript letter filled in from TEMPLATE, as a scalar.
 
 sub print_ps {
   my $self = shift;
-  my $file = $self->generate_letter(@_);
-  FS::Misc::generate_ps($file);
+  my($file, $lfile) = $self->generate_letter(@_);
+  my $ps = FS::Misc::generate_ps($file);
+  unlink($file.'.tex');
+  unlink($lfile);
+
+  $ps;
 }
 
 =item print TEMPLATE
@@ -9030,6 +9174,7 @@ sub _upgrade_data { #class method
   $sth->execute or die $sth->errstr;
 
   local($ignore_expired_card) = 1;
+  local($skip_fuzzyfiles) = 1;
   $class->_upgrade_otaker(%opts);
 
 }