X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=b278e9b14777cf36b1e2152edfbcda853b12b1ab;hb=7aad2eb29c444625fd1130f4ed37d89a7da2c027;hp=2957579d7bcc4461c5a8d022b7f221592bbe1b2a;hpb=1b8a2cc3b3697f3921e26a31691acfabacc1efd6;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 2957579d7..b278e9b14 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -1571,6 +1571,13 @@ sub check { unless ! $self->referral_custnum || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } ); + if ( $self->censustract ne '' ) { + $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/ + or return "Illegal census tract: ". $self->censustract; + + $self->censustract("$1.$2"); + } + if ( $self->ss eq '' ) { $self->ss(''); } else { @@ -1862,7 +1869,7 @@ sub check { $self->payname($1); } - foreach my $flag (qw( tax spool_cdr squelch_cdr archived )) { + foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) { $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag(); $self->$flag($1); } @@ -2481,7 +2488,6 @@ sub bill { $options{'not_pkgpart'} ||= {}; - #put below somehow? local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -2495,6 +2501,17 @@ sub bill { $self->select_for_update; #mutex + my $error = $self->do_cust_event( + 'debug' => ( $options{'debug'} || 0 ), + 'time' => $invoice_time, + 'check_freq' => $options{'check_freq'}, + 'stage' => 'pre-bill', + ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + my @cust_bill_pkg = (); ### @@ -2535,6 +2552,7 @@ sub bill { 'recur' => \$total_recur, 'tax_matrix' => \%taxlisthash, 'time' => $time, + 'real_pkgpart' => $real_pkgpart, 'options' => \%options, ); if ($error) { @@ -2566,6 +2584,7 @@ sub bill { } elsif ( $postal_pkg ) { + my $real_pkgpart = $postal_pkg->pkgpart; foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) { my %postal_options = %options; delete $postal_options{cancel}; @@ -2578,6 +2597,7 @@ sub bill { 'recur' => \$total_recur, 'tax_matrix' => \%taxlisthash, 'time' => $time, + 'real_pkgpart' => $real_pkgpart, 'options' => \%postal_options, ); if ($error) { @@ -2745,7 +2765,7 @@ sub bill { '_date' => ( $invoice_time ), 'charged' => $charged, } ); - my $error = $cust_bill->insert; + $error = $cust_bill->insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; return "can't create invoice for customer #". $self->custnum. ": $error"; @@ -2790,7 +2810,7 @@ sub _make_lines { my (%options) = %{$params{options}}; my $dbh = dbh; - my $real_pkgpart = $cust_pkg->pkgpart; + my $real_pkgpart = $params{real_pkgpart}; my %hash = $cust_pkg->hash; my $old_cust_pkg = new FS::cust_pkg \%hash; @@ -2847,9 +2867,10 @@ sub _make_lines { my $recur = 0; my $unitrecur = 0; my $sdate; - if ( ! $cust_pkg->getfield('susp') and - ( $part_pkg->getfield('freq') ne '0' && - ( $cust_pkg->getfield('bill') || 0 ) <= $time + if ( ! $cust_pkg->get('susp') + and ! $cust_pkg->get('start_date') + and ( $part_pkg->getfield('freq') ne '0' + && ( $cust_pkg->getfield('bill') || 0 ) <= $time ) || ( $part_pkg->plan eq 'voip_cdr' && $part_pkg->option('bill_every_call') @@ -2958,6 +2979,7 @@ sub _make_lines { 'unitrecur' => $unitrecur, 'quantity' => $cust_pkg->quantity, 'details' => \@details, + 'hidden' => $part_pkg->hidden, }; if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) { @@ -2981,7 +3003,7 @@ sub _make_lines { ### my $error = - $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}); + $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart); return $error if $error; push @$cust_bill_pkgs, $cust_bill_pkg; @@ -3001,6 +3023,7 @@ sub _handle_taxes { my $cust_bill_pkg = shift; my $cust_pkg = shift; my $invoice_time = shift; + my $real_pkgpart = shift; my %cust_bill_pkg = (); my %taxes = (); @@ -3091,20 +3114,29 @@ sub _handle_taxes { } my @display = (); - if ( $conf->exists('separate_usage') ) { + if ( $conf->exists('separate_usage') || $cust_bill_pkg->hidden ) { + + my $temp_pkg = new FS::cust_pkg { pkgpart => $real_pkgpart }; + my %hash = $cust_bill_pkg->hidden # maybe for all bill linked? + ? ( 'section' => $temp_pkg->part_pkg->categoryname ) + : (); + my $section = $cust_pkg->part_pkg->option('usage_section', 'Hush!'); my $summary = $cust_pkg->part_pkg->option('summarize_usage', 'Hush!'); - push @display, new FS::cust_bill_pkg_display { type => 'S' }; - push @display, new FS::cust_bill_pkg_display { type => 'R' }; - push @display, new FS::cust_bill_pkg_display { type => 'U', - section => $section - }; + push @display, new FS::cust_bill_pkg_display { type => 'S', %hash }; + push @display, new FS::cust_bill_pkg_display { type => 'R', %hash }; + if ($section && $summary) { - $display[2]->post_total('Y'); push @display, new FS::cust_bill_pkg_display { type => 'U', summary => 'Y', - } + %hash, + }; + $hash{post_total} = 'Y'; } + + $hash{section} = $section if $conf->exists('separate_usage'); + push @display, new FS::cust_bill_pkg_display { type => 'U', %hash }; + } $cust_bill_pkg->set('display', \@display); @@ -3200,7 +3232,7 @@ sub _gather_taxes { } -=item collect OPTIONS +=item collect [ HASHREF | OPTION => VALUE ... ] (Attempt to) collect money for this customer's outstanding invoices (see L). Usually used after the bill method. @@ -3225,25 +3257,24 @@ Use this time when deciding when to print invoices and late notices on those inv Retry card/echeck/LEC transactions even when not scheduled by invoice events. -=item quiet - -set true to surpress email card/ACH decline notices. - =item check_freq "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq) -=item payby +=item quiet -allows for one time override of normal customer billing method +set true to surpress email card/ACH decline notices. =item debug 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) - =back +# =item payby +# +# allows for one time override of normal customer billing method + =cut sub collect { @@ -3281,12 +3312,107 @@ sub collect { } } + my $error = $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; + ''; + +} + +=item do_cust_event [ HASHREF | OPTION => VALUE ... ] + +Runs billing events; see L and the billing events web +interface. + +If there is an error, returns the error, otherwise returns false. + +Options are passed as name-value pairs. + +Currently available options are: + +=over 4 + +=item time + +Use this time when deciding when to print invoices and late notices on those invoices. The default is now. It is specified as a UNIX timestamp; see L). Also see L and L for conversion functions. + +=item check_freq + +"1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq) + +=item stage + +"collect" (the default) or "pre-bill" + +=item quiet + +set true to surpress email card/ACH decline notices. + +=item debug + +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) + +=cut + +# =item payby +# +# allows for one time override of normal customer billing method + +# =item retry +# +# Retry card/echeck/LEC transactions even when not scheduled by invoice events. + +sub do_cust_event { + my( $self, %options ) = @_; + my $time = $options{'time'} || time; + + #put below somehow? + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + $self->select_for_update; #mutex + + if ( $DEBUG ) { + my $balance = $self->balance; + warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n" + } + +# if ( exists($options{'retry_card'}) ) { +# carp 'retry_card option passed to collect is deprecated; use retry'; +# $options{'retry'} ||= $options{'retry_card'}; +# } +# if ( exists($options{'retry'}) && $options{'retry'} ) { +# my $error = $self->retry_realtime; +# if ( $error ) { +# $dbh->rollback if $oldAutoCommit; +# return $error; +# } +# } + # false laziness w/pay_batch::import_results my $due_cust_event = $self->due_cust_event( 'debug' => ( $options{'debug'} || 0 ), - 'time' => $invoice_time, + 'time' => $time, 'check_freq' => $options{'check_freq'}, + 'stage' => ( $options{'stage'} || 'collect' ), ); unless( ref($due_cust_event) ) { $dbh->rollback if $oldAutoCommit; @@ -3298,7 +3424,7 @@ sub collect { #XXX lock event #re-eval event conditions (a previous event could have changed things) - unless ( $cust_event->test_conditions( 'time' => $invoice_time ) ) { + unless ( $cust_event->test_conditions( 'time' => $time ) ) { #don't leave stray "new/locked" records around my $error = $cust_event->delete; if ( $error ) { @@ -3351,6 +3477,10 @@ options are: Search only for events of this check frequency (how often events of this type are checked); currently "1d" (daily, the default) and "1m" (monthly) are recognized. +=item stage + +"collect" (the default) or "pre-bill" + =item time "Current time" for the events. @@ -3406,7 +3536,7 @@ sub due_cust_event { unless $opt{testonly}; ### - # 1: find possible events (initial search) + # find possible events (initial search) ### my @cust_event = (); @@ -3497,8 +3627,20 @@ sub due_cust_event { " total possible cust events found in initial search\n" if $DEBUG; # > 1; + + ## + # test stage + ## + + $opt{stage} ||= 'collect'; + @cust_event = + grep { my $stage = $_->part_event->event_stage; + $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' ) + } + @cust_event; + ## - # 2: test conditions + # test conditions ## my %unsat = (); @@ -3515,7 +3657,7 @@ sub due_cust_event { if $DEBUG; # > 1; ## - # 3: insert + # insert ## unless( $opt{testonly} ) { @@ -3533,7 +3675,7 @@ sub due_cust_event { $dbh->commit or die $dbh->errstr if $oldAutoCommit; ## - # 4: return + # return ## warn " returning events: ". Dumper(@cust_event). "\n" @@ -3940,6 +4082,7 @@ sub realtime_bop { 'payinfo' => $payinfo, 'paydate' => $paydate, 'recurring_billing' => $content{recurring_billing}, + 'pkgnum' => $options{'pkgnum'}, 'status' => 'new', 'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ), }; @@ -4095,6 +4238,7 @@ sub realtime_bop { 'payinfo' => $payinfo, 'paybatch' => $paybatch, 'paydate' => $paydate, + 'pkgnum' => $options{'pkgnum'}, } ); #doesn't hurt to know, even though the dup check is in cust_pay_pending now $cust_pay->payunique( $options{payunique} ) @@ -4197,7 +4341,13 @@ sub realtime_bop { $template->compile() or return "($perror) can't compile template: $Text::Template::ERROR"; - my $templ_hash = { error => $transaction->error_message }; + 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 ), @@ -4608,7 +4758,7 @@ On failure returns an error message. Returns false or a hashref upon success. The hashref contains keys popup_url reference, and collectitems. The first is a URL to which a browser should be redirected for completion of collection. The second is a reference id for the transaction suitable for the end user. The collectitems is a reference to a list of name value pairs suitable for assigning to a html form and posted to popup_url. -Available options are: I, I, I, I, I, I, I, I +Available options are: I, I, I, I, I, I, I, I, I I is one of: I, I and I. If none is specified then it is deduced from the customer record. @@ -4981,6 +5131,7 @@ sub _new_realtime_bop { 'payinfo' => $options{payinfo}, 'paydate' => $paydate, 'recurring_billing' => $content{recurring_billing}, + 'pkgnum' => $options{'pkgnum'}, 'status' => 'new', 'gatewaynum' => $payment_gateway->gatewaynum || '', 'session_id' => $options{session_id} || '', @@ -5227,6 +5378,7 @@ sub _realtime_bop_result { #'payinfo' => $payinfo, 'paybatch' => $paybatch, 'paydate' => $cust_pay_pending->paydate, + 'pkgnum' => $cust_pay_pending->pkgnum, } ); #doesn't hurt to know, even though the dup check is in cust_pay_pending now $cust_pay->payunique( $options{payunique} ) @@ -5375,7 +5527,13 @@ sub _realtime_bop_result { $template->compile() or return "($perror) can't compile template: $Text::Template::ERROR"; - my $templ_hash = { error => $transaction->error_message }; + 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 ), @@ -6089,32 +6247,52 @@ sub apply_credits { @invoices = sort { $b->_date <=> $a->_date } @invoices if defined($opt{'order'}) && $opt{'order'} eq 'newest'; + if ( $conf->exists('pkg-balances') ) { + # limit @credits to those w/ a pkgnum grepped from $self + my %pkgnums = (); + foreach my $i (@invoices) { + foreach my $li ( $i->cust_bill_pkg ) { + $pkgnums{$li->pkgnum} = 1; + } + } + @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits; + } + my $credit; + foreach my $cust_bill ( @invoices ) { - my $amount; if ( !defined($credit) || $credit->credited == 0) { $credit = pop @credits or last; } - if ($cust_bill->owed >= $credit->credited) { - $amount=$credit->credited; - }else{ - $amount=$cust_bill->owed; + my $owed; + if ( $conf->exists('pkg-balances') && $credit->pkgnum ) { + $owed = $cust_bill->owed_pkgnum($credit->pkgnum); + } else { + $owed = $cust_bill->owed; } + unless ( $owed > 0 ) { + push @credits, $credit; + next; + } + + my $amount = min( $credit->credited, $owed ); my $cust_credit_bill = new FS::cust_credit_bill ( { 'crednum' => $credit->crednum, 'invnum' => $cust_bill->invnum, 'amount' => $amount, } ); + $cust_credit_bill->pkgnum( $credit->pkgnum ) + if $conf->exists('pkg-balances') && $credit->pkgnum; my $error = $cust_credit_bill->insert; if ( $error ) { $dbh->rollback or die $dbh->errstr if $oldAutoCommit; die $error; } - redo if ($cust_bill->owed > 0); + redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances'); } @@ -6162,33 +6340,52 @@ sub apply_payments { grep { $_->owed > 0 } $self->cust_bill; + if ( $conf->exists('pkg-balances') ) { + # limit @payments to those w/ a pkgnum grepped from $self + my %pkgnums = (); + foreach my $i (@invoices) { + foreach my $li ( $i->cust_bill_pkg ) { + $pkgnums{$li->pkgnum} = 1; + } + } + @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments; + } + my $payment; foreach my $cust_bill ( @invoices ) { - my $amount; if ( !defined($payment) || $payment->unapplied == 0 ) { $payment = pop @payments or last; } - if ( $cust_bill->owed >= $payment->unapplied ) { - $amount = $payment->unapplied; + my $owed; + if ( $conf->exists('pkg-balances') && $payment->pkgnum ) { + $owed = $cust_bill->owed_pkgnum($payment->pkgnum); } else { - $amount = $cust_bill->owed; + $owed = $cust_bill->owed; } + unless ( $owed > 0 ) { + push @payments, $payment; + next; + } + + my $amount = min( $payment->unapplied, $owed ); my $cust_bill_pay = new FS::cust_bill_pay ( { 'paynum' => $payment->paynum, 'invnum' => $cust_bill->invnum, 'amount' => $amount, } ); + $cust_bill_pay->pkgnum( $payment->pkgnum ) + if $conf->exists('pkg-balances') && $payment->pkgnum; my $error = $cust_bill_pay->insert; if ( $error ) { $dbh->rollback or die $dbh->errstr if $oldAutoCommit; die $error; } - redo if ( $cust_bill->owed > 0); + redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances'); } @@ -6249,6 +6446,41 @@ sub total_owed_date { } +=item total_owed_pkgnum PKGNUM + +Returns the total owed on all invoices for this customer's specific package +when using experimental package balances (see L). + +=cut + +sub total_owed_pkgnum { + my( $self, $pkgnum ) = @_; + $self->total_owed_date_pkgnum(2145859200, $pkgnum); #12/31/2037 +} + +=item total_owed_date_pkgnum TIME PKGNUM + +Returns the total owed for this customer's specific package when using +experimental package balances on all invoices with date earlier than +TIME. TIME is specified as a UNIX timestamp; see L). Also +see L and L for conversion functions. + +=cut + +sub total_owed_date_pkgnum { + my( $self, $time, $pkgnum ) = @_; + + my $total_bill = 0; + foreach my $cust_bill ( + grep { $_->_date <= $time } + qsearch('cust_bill', { 'custnum' => $self->custnum, } ) + ) { + $total_bill += $cust_bill->owed_pkgnum($pkgnum); + } + sprintf( "%.2f", $total_bill ); + +} + =item total_paid Returns the total amount of all payments. @@ -6285,6 +6517,21 @@ sub total_unapplied_credits { sprintf( "%.2f", $total_credit ); } +=item total_unapplied_credits_pkgnum PKGNUM + +Returns the total outstanding credit (see L) for this +customer. See L. + +=cut + +sub total_unapplied_credits_pkgnum { + my( $self, $pkgnum ) = @_; + my $total_credit = 0; + $total_credit += $_->credited foreach $self->cust_credit_pkgnum($pkgnum); + sprintf( "%.2f", $total_credit ); +} + + =item total_unapplied_payments Returns the total unapplied payments (see L) for this customer. @@ -6299,6 +6546,22 @@ sub total_unapplied_payments { sprintf( "%.2f", $total_unapplied ); } +=item total_unapplied_payments_pkgnum PKGNUM + +Returns the total unapplied payments (see L) for this customer's +specific package when using experimental package balances. See +L. + +=cut + +sub total_unapplied_payments_pkgnum { + my( $self, $pkgnum ) = @_; + my $total_unapplied = 0; + $total_unapplied += $_->unapplied foreach $self->cust_pay_pkgnum($pkgnum); + sprintf( "%.2f", $total_unapplied ); +} + + =item total_unapplied_refunds Returns the total unrefunded refunds (see L) for this @@ -6351,6 +6614,26 @@ sub balance_date { ); } +=item balance_pkgnum PKGNUM + +Returns the balance for this customer's specific package when using +experimental package balances (total_owed plus total_unrefunded, minus +total_unapplied_credits minus total_unapplied_payments) + +=cut + +sub balance_pkgnum { + my( $self, $pkgnum ) = @_; + + sprintf( "%.2f", + $self->total_owed_pkgnum($pkgnum) +# n/a - refunds aren't part of pkg-balances since they don't apply to invoices +# + $self->total_unapplied_refunds_pkgnum($pkgnum) + - $self->total_unapplied_credits_pkgnum($pkgnum) + - $self->total_unapplied_payments_pkgnum($pkgnum) + ); +} + =item in_transit_payments Returns the total of requests for payments for this customer pending in @@ -6980,6 +7263,22 @@ sub cust_credit { qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) } +=item cust_credit_pkgnum + +Returns all the credits (see L) for this customer's specific +package when using experimental package balances. + +=cut + +sub cust_credit_pkgnum { + my( $self, $pkgnum ) = @_; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_credit', { 'custnum' => $self->custnum, + 'pkgnum' => $pkgnum, + } + ); +} + =item cust_pay Returns all the payments (see L) for this customer. @@ -6992,6 +7291,22 @@ sub cust_pay { qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) } +=item cust_pay_pkgnum + +Returns all the payments (see L) for this customer's specific +package when using experimental package balances. + +=cut + +sub cust_pay_pkgnum { + my( $self, $pkgnum ) = @_; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_pay', { 'custnum' => $self->custnum, + 'pkgnum' => $pkgnum, + } + ); +} + =item cust_pay_void Returns all voided payments (see L) for this customer. @@ -7357,6 +7672,19 @@ sub support_services { } +# Return a list of latitude/longitude for one of the services (if any) +sub service_coordinates { + my $self = shift; + + my @svc_X = + grep { $_->latitude && $_->longitude } + map { $_->svc_x } + map { $_->cust_svc } + $self->ncancelled_pkgs; + + scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : () +} + =back =head1 CLASS METHODS