X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=6d32e00fc73e0e302d6408bea28cba06df536d24;hb=b0b00f5e3adc74e2b9f39cc7827da9403ca42be6;hp=6d879bcd406d3dc32628fa7ca82eddb2304570c7;hpb=66ec38299e2f40271fa1e66c7a4db529a5b261d4;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 6d879bcd4..6d32e00fc 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -16,6 +16,8 @@ use Exporter; use Scalar::Util qw( blessed ); use List::Util qw( min ); use Time::Local qw(timelocal); +use Storable qw(thaw); +use MIME::Base64; use Data::Dumper; use Tie::IxHash; use Digest::MD5 qw(md5_base64); @@ -55,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; @@ -62,6 +65,7 @@ use FS::queue; use FS::part_pkg; use FS::part_event; use FS::part_event_condition; +use FS::part_export; #use FS::cust_event; use FS::type_pkgs; use FS::payment_gateway; @@ -470,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; @@ -546,6 +574,45 @@ sub insert { } } + # cust_main exports! + warn " exporting\n" if $DEBUG > 1; + + my $export_args = $options{'export_args'} || []; + + my @part_export = + map qsearch( 'part_export', {exportnum=>$_} ), + $conf->config('cust_main-exports'); #, $agentnum + + foreach my $part_export ( @part_export ) { + my $error = $part_export->export_insert($self, @$export_args); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "exporting to ". $part_export->exporttype. + " (transaction rolled back): $error"; + } + } + + #foreach my $depend_jobnum ( @$depend_jobnums ) { + # warn "[$me] inserting dependancies on supplied job $depend_jobnum\n" + # if $DEBUG; + # foreach my $jobnum ( @jobnums ) { + # my $queue = qsearchs('queue', { 'jobnum' => $jobnum } ); + # warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n" + # if $DEBUG; + # my $error = $queue->depend_insert($depend_jobnum); + # if ( $error ) { + # $dbh->rollback if $oldAutoCommit; + # return "error queuing job dependancy: $error"; + # } + # } + # } + # + #} + # + #if ( exists $options{'jobnums'} ) { + # push @{ $options{'jobnums'} }, @jobnums; + #} + warn " insert complete; committing transaction\n" if $DEBUG > 1; @@ -1314,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; + } } } @@ -1340,6 +1397,23 @@ sub delete { return $error; } + # cust_main exports! + + #my $export_args = $options{'export_args'} || []; + + my @part_export = + map qsearch( 'part_export', {exportnum=>$_} ), + $conf->config('cust_main-exports'); #, $agentnum + + foreach my $part_export ( @part_export ) { + my $error = $part_export->export_delete( $self ); #, @$export_args); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "exporting to ". $part_export->exporttype. + " (transaction rolled back): $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -1419,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'}; @@ -1478,6 +1574,23 @@ sub replace { } } + # cust_main exports! + + my $export_args = $options{'export_args'} || []; + + my @part_export = + map qsearch( 'part_export', {exportnum=>$_} ), + $conf->config('cust_main-exports'); #, $agentnum + + foreach my $part_export ( @part_export ) { + my $error = $part_export->export_replace( $self, $old, @$export_args); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "exporting to ". $part_export->exporttype. + " (transaction rolled back): $error"; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -2107,6 +2220,9 @@ sub sort_packages { return 1 if !$a_num_cust_svc && $b_num_cust_svc; my @a_cust_svc = $a->cust_svc; my @b_cust_svc = $b->cust_svc; + return 0 if !scalar(@a_cust_svc) && !scalar(@b_cust_svc); + return -1 if scalar(@a_cust_svc) && !scalar(@b_cust_svc); + return 1 if !scalar(@a_cust_svc) && scalar(@b_cust_svc); $a_cust_svc[0]->svc_x->label cmp $b_cust_svc[0]->svc_x->label; } @@ -2385,6 +2501,42 @@ sub agent { qsearchs( 'agent', { 'agentnum' => $self->agentnum } ); } +=item agent_name + +Returns the agent name (see L) for this customer. + +=cut + +sub agent_name { + my $self = shift; + $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 @@ -2475,6 +2627,10 @@ Any other true value causes errors to die. Debugging level. Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries) +=item job + +Optional FS::queue entry to receive status updates. + =back Options are passed to the B and B methods verbatim, so all @@ -2491,7 +2647,9 @@ sub bill_and_collect { #pre-printing invoices $options{'actual_time'} ||= time; + my $job = $options{'job'}; + $job->update_statustext('0,cleaning expired packages') if $job; $error = $self->cancel_expired_pkgs( $options{actual_time} ); if ( $error ) { $error = "Error expiring custnum ". $self->custnum. ": $error"; @@ -2508,6 +2666,7 @@ sub bill_and_collect { else { warn $error; } } + $job->update_statustext('20,billing packages') if $job; $error = $self->bill( %options ); if ( $error ) { $error = "Error billing custnum ". $self->custnum. ": $error"; @@ -2516,6 +2675,7 @@ sub bill_and_collect { else { warn $error; } } + $job->update_statustext('50,applying payments and credits') if $job; $error = $self->apply_payments_and_credits; if ( $error ) { $error = "Error applying custnum ". $self->custnum. ": $error"; @@ -2524,6 +2684,7 @@ sub bill_and_collect { else { warn $error; } } + $job->update_statustext('70,running collection events') if $job; unless ( $conf->exists('cancelled_cust-noevents') && ! $self->num_ncancelled_pkgs ) { @@ -2535,6 +2696,7 @@ sub bill_and_collect { else { warn $error; } } } + $job->update_statustext('100,finished') if $job; ''; @@ -2684,8 +2846,14 @@ sub bill { local $FS::UID::AutoCommit = 0; my $dbh = dbh; + warn "$me acquiring lock on customer ". $self->custnum. "\n" + if $DEBUG; + $self->select_for_update; #mutex + warn "$me running pre-bill events for customer ". $self->custnum. "\n" + if $DEBUG; + my $error = $self->do_cust_event( 'debug' => ( $options{'debug'} || 0 ), 'time' => $invoice_time, @@ -2697,6 +2865,9 @@ sub bill { return $error; } + warn "$me done running pre-bill events for customer ". $self->custnum. "\n" + if $DEBUG; + #keep auto-charge and non-auto-charge line items separate my @passes = ( '', 'no_auto' ); @@ -3622,19 +3793,17 @@ sub collect { } } - my $error = $self->do_cust_event( + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + #never want to roll back an event just because it returned an error + local $FS::UID::AutoCommit = 1; #$oldAutoCommit; + + $self->do_cust_event( 'debug' => ( $options{'debug'} || 0 ), 'time' => $invoice_time, 'check_freq' => $options{'check_freq'}, 'stage' => 'collect', ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - - $dbh->commit or die $dbh->errstr if $oldAutoCommit; - ''; } @@ -3729,6 +3898,11 @@ sub do_cust_event { return $due_cust_event; } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + #never want to roll back an event just because it or a different one + # returned an error + local $FS::UID::AutoCommit = 1; #$oldAutoCommit; + foreach my $cust_event ( @$due_cust_event ) { #XXX lock event @@ -3737,11 +3911,7 @@ sub do_cust_event { unless ( $cust_event->test_conditions( 'time' => $time ) ) { #don't leave stray "new/locked" records around my $error = $cust_event->delete; - if ( $error ) { - #gah, even with transactions - $dbh->commit if $oldAutoCommit; #well. - return $error; - } + return $error if $error; next; } @@ -3750,20 +3920,16 @@ sub do_cust_event { warn " running cust_event ". $cust_event->eventnum. "\n" if $DEBUG > 1; - #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options? if ( my $error = $cust_event->do_event() ) { #XXX wtf is this? figure out a proper dealio with return value #from do_event - # gah, even with transactions. - $dbh->commit if $oldAutoCommit; #well. - return $error; - } + return $error; + } } } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } @@ -7881,8 +8047,10 @@ sub email_search_result { my $subject = delete $params->{subject}; my $html_body = delete $params->{html_body}; my $text_body = delete $params->{text_body}; + my $error = ''; - my $job = delete $params->{'job'}; + my $job = delete $params->{'job'} + or die "email_search_result must run from the job queue.\n"; $params->{'payby'} = [ split(/\0/, $params->{'payby'}) ] unless ref($params->{'payby'}); @@ -7902,43 +8070,73 @@ sub email_search_result { my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo + my @retry_jobs = (); + my $success = 0; #eventually order+limit magic to reduce memory use? foreach my $cust_main ( qsearch($sql_query) ) { + #progressbar first, so that the count is right + $num++; + if ( time - $min_sec > $last ) { + my $error = $job->update_statustext( + int( 100 * $num / $num_cust ) + ); + die $error if $error; + $last = time; + } + my $to = $cust_main->invoicing_list_emailonly_scalar; - next unless $to; - my $error = send_email( - generate_email( + if( $to ) { + my @message = ( 'from' => $from, 'to' => $to, 'subject' => $subject, 'html_body' => $html_body, 'text_body' => $text_body, - ) - ); - return $error if $error; + ); - if ( $job ) { #progressbar foo - $num++; - if ( time - $min_sec > $last ) { - my $error = $job->update_statustext( - int( 100 * $num / $num_cust ) - ); - die $error if $error; - $last = time; + $error = send_email( generate_email( @message ) ); + + if($error) { + # queue the sending of this message so that the user can see what we + # tried to do, and retry if desired + my $queue = new FS::queue { + 'job' => 'FS::Misc::process_send_email', + 'custnum' => $cust_main->custnum, + 'status' => 'failed', + 'statustext' => $error, + }; + $queue->insert(@message); + push @retry_jobs, $queue; } + else { + $success++; + } + } + + if($success == 0 and + (scalar(@retry_jobs) > 10 or $num == $num_cust) + ) { + # 10 is arbitrary, but if we have enough failures, that's + # probably a configuration or network problem, and we + # abort the batch and run away screaming. + # We NEVER do this if anything was successfully sent. + $_->delete foreach (@retry_jobs); + return "multiple failures: '$error'\n"; } + } + if(@retry_jobs) { + # fail the job, but with a status message that makes it clear + # something was sent. + return "Sent $success, failed ".scalar(@retry_jobs).". Failed attempts placed in job queue.\n"; } return ''; } -use Storable qw(thaw); -use Data::Dumper; -use MIME::Base64; sub process_email_search_result { my $job = shift; #warn "$me process_re_X $method for job $job\n" if $DEBUG; @@ -8874,6 +9072,18 @@ sub queued_bill { $cust_main->bill_and_collect( %args ); } +sub process_bill_and_collect { + my $job = shift; + my $param = thaw(decode_base64(shift)); + my $cust_main = qsearchs( 'cust_main', { custnum => $param->{'custnum'} } ) + or die "custnum '$param->{custnum}' not found!\n"; + $param->{'job'} = $job; + $param->{'fatal'} = 1; # runs from job queue, will be caught + $param->{'retry'} = 1; + + $cust_main->bill_and_collect( %$param ); +} + sub _upgrade_data { #class method my ($class, %opts) = @_; @@ -8882,6 +9092,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); }