X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main.pm;h=638036a15beb2b77f8ac4991715e5455ca9593ae;hb=357b4e26965895666685590d59f72de331fecb08;hp=b8657895b69731be596fc6cb8559a1e40b1eb9d8;hpb=54538d4a7445f5315e27cb8ff44b5ddc4160ee88;p=freeside.git diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index b8657895b..638036a15 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -1845,6 +1845,8 @@ sub check { my( $m, $y ); if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) { ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" ); + } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) { + ( $m, $y ) = ( $2, "19$1" ); } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) { ( $m, $y ) = ( $3, "20$2" ); } else { @@ -2488,7 +2490,6 @@ sub bill { $options{'not_pkgpart'} ||= {}; - #put below somehow? local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -2502,6 +2503,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 = (); ### @@ -2705,6 +2717,23 @@ sub bill { $tax = sprintf('%.2f', $tax ); $total_setup = sprintf('%.2f', $total_setup+$tax ); + my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname, + 'disabled' => '', + }, + ); + + my @display = (); + if ( $pkg_category and + $conf->config('invoice_latexsummary') || + $conf->config('invoice_htmlsummary') + ) + { + + my %hash = ( 'section' => $pkg_category->categoryname ); + push @display, new FS::cust_bill_pkg_display { type => 'S', %hash }; + + } + push @cust_bill_pkg, new FS::cust_bill_pkg { 'pkgnum' => 0, 'setup' => $tax, @@ -2712,6 +2741,7 @@ sub bill { 'sdate' => '', 'edate' => '', 'itemdesc' => $taxname, + 'display' => \@display, 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location, 'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location, }; @@ -2749,13 +2779,26 @@ sub bill { my $charged = sprintf('%.2f', $total_setup + $total_recur ); + my @cust_bill = $self->cust_bill; + my $balance = $self->balance; + my $previous_balance = scalar(@cust_bill) + ? $cust_bill[$#cust_bill]->billing_balance + : 0; + + $previous_balance += $cust_bill[$#cust_bill]->charged + if scalar(@cust_bill); + #my $balance_adjustments = + # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged); + #create the new invoice my $cust_bill = new FS::cust_bill ( { - 'custnum' => $self->custnum, - '_date' => ( $invoice_time ), - 'charged' => $charged, + 'custnum' => $self->custnum, + '_date' => ( $invoice_time ), + 'charged' => $charged, + 'billing_balance' => $balance, + 'previous_balance' => $previous_balance, } ); - 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"; @@ -2993,7 +3036,7 @@ sub _make_lines { ### my $error = - $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart); + $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options); return $error if $error; push @$cust_bill_pkgs, $cust_bill_pkg; @@ -3014,6 +3057,7 @@ sub _handle_taxes { my $cust_pkg = shift; my $invoice_time = shift; my $real_pkgpart = shift; + my $options = shift; my %cust_bill_pkg = (); my %taxes = (); @@ -3021,8 +3065,8 @@ sub _handle_taxes { my @classes; #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U'; push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage; - push @classes, 'setup' if $cust_bill_pkg->setup; - push @classes, 'recur' if $cust_bill_pkg->recur; + push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel}); + push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel}); if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) { @@ -3104,7 +3148,9 @@ sub _handle_taxes { } my @display = (); - if ( $conf->exists('separate_usage') || $cust_bill_pkg->hidden ) { + my $separate = $conf->exists('separate_usage'); + my $usage_mandate = $cust_pkg->part_pkg->option('usage_mandate', 'Hush!'); + if ( $separate || $cust_bill_pkg->hidden || $usage_mandate ) { my $temp_pkg = new FS::cust_pkg { pkgpart => $real_pkgpart }; my %hash = $cust_bill_pkg->hidden # maybe for all bill linked? @@ -3113,18 +3159,28 @@ sub _handle_taxes { 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', %hash }; - push @display, new FS::cust_bill_pkg_display { type => 'R', %hash }; + if ( $separate ) { + push @display, new FS::cust_bill_pkg_display { type => 'S', %hash }; + push @display, new FS::cust_bill_pkg_display { type => 'R', %hash }; + } else { + push @display, new FS::cust_bill_pkg_display + { type => '', + %hash, + ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ), + }; + } - if ($section && $summary) { + if ($separate && $section && $summary) { push @display, new FS::cust_bill_pkg_display { type => 'U', summary => 'Y', %hash, }; + } + if ($usage_mandate || $section && $summary) { $hash{post_total} = 'Y'; } - $hash{section} = $section if $conf->exists('separate_usage'); + $hash{section} = $section if ($separate || $usage_mandate); push @display, new FS::cust_bill_pkg_display { type => 'U', %hash }; } @@ -3222,7 +3278,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. @@ -3247,25 +3303,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 { @@ -3303,12 +3358,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; @@ -3320,7 +3470,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 ) { @@ -3373,6 +3523,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. @@ -3428,7 +3582,7 @@ sub due_cust_event { unless $opt{testonly}; ### - # 1: find possible events (initial search) + # find possible events (initial search) ### my @cust_event = (); @@ -3519,8 +3673,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 = (); @@ -3537,7 +3703,7 @@ sub due_cust_event { if $DEBUG; # > 1; ## - # 3: insert + # insert ## unless( $opt{testonly} ) { @@ -3555,7 +3721,7 @@ sub due_cust_event { $dbh->commit or die $dbh->errstr if $oldAutoCommit; ## - # 4: return + # return ## warn " returning events: ". Dumper(@cust_event). "\n" @@ -6046,19 +6212,23 @@ sub batch_card { ''; } -=item apply_payments_and_credits +=item apply_payments_and_credits [ OPTION => VALUE ... ] Applies unapplied payments and credits. In most cases, this new method should be used in place of sequential apply_payments and apply_credits methods. +A hash of optional arguments may be passed. Currently "manual" is supported. +If true, a payment receipt is sent instead of a statement when +'payment_receipt_email' configuration option is set. + If there is an error, returns the error, otherwise returns false. =cut sub apply_payments_and_credits { - my $self = shift; + my( $self, %options ) = @_; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -6074,7 +6244,7 @@ sub apply_payments_and_credits { $self->select_for_update; #mutex foreach my $cust_bill ( $self->open_cust_bill ) { - my $error = $cust_bill->apply_payments_and_credits; + my $error = $cust_bill->apply_payments_and_credits(%options); if ( $error ) { $dbh->rollback if $oldAutoCommit; return "Error applying: $error"; @@ -6183,19 +6353,24 @@ sub apply_credits { return $total_unapplied_credits; } -=item apply_payments +=item apply_payments [ OPTION => VALUE ... ] Applies (see L) unapplied payments (see L) to outstanding invoice balances in chronological order. #and returns the value of any remaining unapplied payments. +A hash of optional arguments may be passed. Currently "manual" is supported. +If true, a payment receipt is sent instead of a statement when +'payment_receipt_email' configuration option is set. + + Dies if there is an error. =cut sub apply_payments { - my $self = shift; + my( $self, %options ) = @_; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -6259,7 +6434,7 @@ sub apply_payments { } ); $cust_bill_pay->pkgnum( $payment->pkgnum ) if $conf->exists('pkg-balances') && $payment->pkgnum; - my $error = $cust_bill_pay->insert; + my $error = $cust_bill_pay->insert(%options); if ( $error ) { $dbh->rollback or die $dbh->errstr if $oldAutoCommit; die $error; @@ -6829,6 +7004,24 @@ sub invoicing_list_emailonly_scalar { join(', ', $self->invoicing_list_emailonly); } +=item referral_custnum_cust_main + +Returns the customer who referred this customer (or the empty string, if +this customer was not referred). + +Note the difference with referral_cust_main method: This method, +referral_custnum_cust_main returns the single customer (if any) who referred +this customer, while referral_cust_main returns an array of customers referred +BY this customer. + +=cut + +sub referral_custnum_cust_main { + my $self = shift; + return '' unless $self->referral_custnum; + qsearchs('cust_main', { 'custnum' => $self->referral_custnum } ); +} + =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ] Returns an array of customers referred by this customer (referral_custnum set @@ -6836,6 +7029,11 @@ to this custnum). If DEPTH is given, recurses up to the given depth, returning customers referred by customers referred by this customer and so on, inclusive. The default behavior is DEPTH 1 (no recursion). +Note the difference with referral_custnum_cust_main method: This method, +referral_cust_main, returns an array of customers referred BY this customer, +while referral_custnum_cust_main returns the single customer (if any) who +referred this customer. + =cut sub referral_cust_main { @@ -7131,6 +7329,18 @@ sub open_cust_bill { } +=item cust_statements + +Returns all the statements (see L) for this customer. + +=cut + +sub cust_statement { + my $self = shift; + sort { $a->_date <=> $b->_date } + qsearch('cust_statement', { 'custnum' => $self->custnum, } ) +} + =item cust_credit Returns all the credits (see L) for this customer. @@ -7167,10 +7377,26 @@ Returns all the payments (see L) for this customer. sub cust_pay { my $self = shift; + return $self->num_cust_pay unless wantarray; sort { $a->_date <=> $b->_date } qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) } +=item num_cust_pay + +Returns the number of payments (see L) for this customer. Also +called automatically when the cust_pay method is used in a scalar context. + +=cut + +sub num_cust_pay { + my $self = shift; + my $sql = "SELECT COUNT(*) FROM cust_pay WHERE custnum = ?"; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute($self->custnum) or die $sth->errstr; + $sth->fetchrow_arrayref->[0]; +} + =item cust_pay_pkgnum Returns all the payments (see L) for this customer's specific @@ -8069,6 +8295,9 @@ sub email_search_sql { my $job = delete $params->{'job'}; + $params->{'payby'} = [ split(/\0/, $params->{'payby'}) ] + unless ref($params->{'payby'}); + my $sql_query = $class->search_sql($params); my $count_query = delete($sql_query->{'count_query'}); @@ -8130,6 +8359,9 @@ sub process_email_search_sql { $param->{'job'} = $job; + $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ] + unless ref($param->{'payby'}); + my $error = FS::cust_main->email_search_sql( $param ); die $error if $error;