creating address-less free customers, RT#24968
[freeside.git] / FS / FS / cust_main.pm
index 7c7c9e2..a1d7d87 100644 (file)
@@ -9,7 +9,7 @@ use base qw( FS::cust_main::Packages FS::cust_main::Status
              FS::cust_main::Billing_ThirdParty
              FS::cust_main::Location
              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
            );
@@ -17,6 +17,7 @@ use vars qw( $DEBUG $me $conf
              @encrypted_fields
              $import
              $ignore_expired_card $ignore_banned_card $ignore_illegal_zip
+             $ignore_invalid_card
              $skip_fuzzyfiles
              @paytypes
            );
@@ -35,6 +36,7 @@ use Business::CreditCard 0.28;
 use Locale::Country;
 use FS::UID qw( dbh driver_name );
 use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
+use FS::Cursor;
 use FS::Misc qw( generate_email send_email generate_ps do_print );
 use FS::Msgcat qw(gettext);
 use FS::CurrentUser;
@@ -76,6 +78,8 @@ use FS::cust_attachment;
 use FS::contact;
 use FS::Locales;
 use FS::upgrade_journal;
+use FS::sales;
+use FS::cust_payby;
 
 # 1 is mostly method/subroutine entry and options
 # 2 traces progress of some operations
@@ -86,6 +90,7 @@ $me = '[FS::cust_main]';
 $import = 0;
 $ignore_expired_card = 0;
 $ignore_banned_card = 0;
+$ignore_invalid_card = 0;
 
 $skip_fuzzyfiles = 0;
 
@@ -99,6 +104,7 @@ sub nohistory_fields { ('payinfo', 'paycvv'); }
 install_callback FS::UID sub { 
   $conf = new FS::Conf;
   #yes, need it for stuff below (prolly should be cached)
+  $ignore_invalid_card = $conf->exists('allow_invalid_cards');
 };
 
 sub _cache {
@@ -405,7 +411,9 @@ sub insert {
     my $loc = delete $self->hashref->{$l};
     # XXX if we're moving a prospect's locations, do that here
     if ( !$loc ) {
-      return "$l not set";
+      #return "$l not set";
+      #location-less customer records are now permitted
+      next;
     }
     
     if ( !$loc->locationnum ) {
@@ -455,7 +463,7 @@ sub insert {
   foreach my $l (qw(bill_location ship_location)) {
     warn "  setting $l.custnum\n"
       if $DEBUG > 1;
-    my $loc = $self->$l;
+    my $loc = $self->$l or next;
     unless ( $loc->custnum ) {
       $loc->set(custnum => $self->custnum);
       $error ||= $loc->replace;
@@ -1237,13 +1245,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 ) {
@@ -1655,13 +1664,26 @@ sub queue_fuzzyfiles_update {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  my @locations = $self->bill_location;
-  push @locations, $self->ship_location if $self->has_ship_address;
+  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 = ();
+  push @locations, $self->bill_location if $self->bill_locationnum;
+  push @locations, $self->ship_location if @locations && $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;
@@ -1693,9 +1715,10 @@ sub check {
     || $self->ut_number('agentnum')
     || $self->ut_textn('agent_custid')
     || $self->ut_number('refnum')
-    || $self->ut_foreign_key('bill_locationnum', 'cust_location','locationnum')
-    || $self->ut_foreign_key('ship_locationnum', 'cust_location','locationnum')
+    || $self->ut_foreign_keyn('bill_locationnum', 'cust_location','locationnum')
+    || $self->ut_foreign_keyn('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')
@@ -1704,6 +1727,7 @@ sub check {
     || $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')
@@ -1721,11 +1745,13 @@ sub check {
     || $self->ut_currencyn('currency')
   ;
 
-  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."
@@ -1791,11 +1817,16 @@ sub check {
   
   }
 
-  #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
-  #  or return "Illegal payby: ". $self->payby;
-  #$self->payby($1);
-  FS::payby->can_payby($self->table, $self->payby)
-    or return "Illegal payby: ". $self->payby;
+  ### start of stuff moved to cust_payby
+  # then mostly kept here to support upgrades (can remove in 5.x)
+  #  but modified to allow everything to be empty
+
+  if ( $self->payby ) {
+    FS::payby->can_payby($self->table, $self->payby)
+      or return "Illegal payby: ". $self->payby;
+  } else {
+    $self->payby('');
+  }
 
   $error =    $self->ut_numbern('paystart_month')
            || $self->ut_numbern('paystart_year')
@@ -1817,7 +1848,8 @@ sub check {
 
   # Need some kind of global flag to accept invalid cards, for testing
   # on scrubbed data.
-  if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
+  if ( !$import && !$ignore_invalid_card && $check_payinfo && 
+    $self->payby =~ /^(CARD|DCRD)$/ ) {
 
     my $payinfo = $self->payinfo;
     $payinfo =~ s/\D//g;
@@ -1889,7 +1921,8 @@ sub check {
       $self->payissue('');
     }
 
-  } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
+  } elsif ( !$ignore_invalid_card && $check_payinfo && 
+    $self->payby =~ /^(CHEK|DCHK)$/ ) {
 
     my $payinfo = $self->payinfo;
     $payinfo =~ s/[^\d\@\.]//g;
@@ -1967,7 +2000,8 @@ sub check {
   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
     return "Expiration date required"
       # shouldn't payinfo_check do this?
-      unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/;
+      unless ! $self->payby
+            || $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD|PPAL)$/;
     $self->paydate('');
   } else {
     my( $m, $y );
@@ -1995,11 +2029,13 @@ sub check {
   ) {
     $self->payname( $self->first. " ". $self->getfield('last') );
   } else {
-    $self->payname =~ /^([\w \,\.\-\'\&]+)$/
+    $self->payname =~ /^([\w \,\.\-\'\&]*)$/
       or return gettext('illegal_name'). " payname: ". $self->payname;
     $self->payname($1);
   }
 
+  ### end of stuff moved to cust_payby
+
   return "Please select an invoicing locale"
     if ! $self->locale
     && ! $self->custnum
@@ -2080,6 +2116,21 @@ sub cust_contact {
   qsearch('contact', { 'custnum' => $self->custnum } );
 }
 
+=item cust_payby
+
+Returns all payment methods (see L<FS::cust_payby>) for this customer.
+
+=cut
+
+sub cust_payby {
+  my $self = shift;
+  qsearch({
+    'table'    => 'cust_payby',
+    'hashref'  => { 'custnum' => $self->custnum },
+    'order_by' => 'ORDER BY weight ASC',
+  });
+}
+
 =item unsuspend
 
 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
@@ -3294,6 +3345,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.
 
@@ -3319,10 +3372,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);
 
@@ -3698,6 +3751,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.
@@ -4042,6 +4108,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.
@@ -4881,9 +4957,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;
 }
 
@@ -5004,6 +5080,7 @@ sub process_bill_and_collect {
 # JRNL seq scan of cust_main on paydate... index on substrings?  maybe set an
 # JRNL seq scan of cust_main on payinfo.. certainly not going toi ndex that...
 # JRNL leading/trailing spaces in first, last, company
+# JRNL migrate to cust_payby
 # - otaker upgrade?  journal and call it good?  (double check to make sure
 #    we're not still setting otaker here)
 #
@@ -5080,6 +5157,44 @@ sub _upgrade_data { #class method
 
   }
 
+  unless ( FS::upgrade_journal->is_done('cust_main__cust_payby') ) {
+
+    #we don't want to decrypt them, just stuff them as-is into cust_payby
+    local(@encrypted_fields) = ();
+
+    local($FS::cust_payby::ignore_expired_card) = 1;
+    local($FS::cust_payby::ignore_banned_card) = 1;
+
+    my @payfields = qw( payby payinfo paycvv paymask
+                        paydate paystart_month paystart_year payissue
+                        payname paystate paytype payip
+                      );
+
+    my $search = new FS::Cursor {
+      'table'     => 'cust_main',
+      'extra_sql' => " WHERE ( payby IS NOT NULL AND payby != '' ) ",
+    };
+
+    while (my $cust_main = $search->fetch) {
+
+      my $cust_payby = new FS::cust_payby {
+        'custnum' => $cust_main->custnum,
+        'weight'  => 1,
+        map { $_ => $cust_main->$_(); } @payfields
+      };
+
+      my $error = $cust_payby->insert;
+      die $error if $error;
+
+      $cust_main->setfield($_, '') foreach @payfields;
+      $error = $cust_main->replace;
+      die $error if $error;
+
+    };
+
+    FS::upgrade_journal->set_done('cust_main__cust_payby');
+  }
+
   $class->_upgrade_otaker(%opts);
 
 }