From 4396080ed2829ae0595f1fd777f39d090c9bcd7c Mon Sep 17 00:00:00 2001 From: ivan Date: Thu, 30 Jul 2009 06:42:33 +0000 Subject: [PATCH] experimental package balances, RT#4339 --- FS/FS/Conf.pm | 7 + FS/FS/Schema.pm | 6 + FS/FS/cust_bill.pm | 104 ++++++++++-- FS/FS/cust_bill_ApplicationCommon.pm | 4 + FS/FS/cust_bill_pay.pm | 2 + FS/FS/cust_credit.pm | 6 + FS/FS/cust_credit_bill.pm | 2 + FS/FS/cust_main.pm | 179 +++++++++++++++++++-- FS/FS/cust_pay.pm | 50 ++++-- FS/FS/cust_pay_pending.pm | 6 + FS/FS/cust_pay_void.pm | 38 ++++- FS/FS/cust_pkg.pm | 57 +++++++ httemplate/edit/cust_credit.cgi | 13 ++ httemplate/edit/cust_pay.cgi | 14 +- httemplate/edit/process/cust_pay.cgi | 4 +- httemplate/elements/select-cust_pkg-balances.html | 30 ++++ .../elements/tr-select-cust_pkg-balances.html | 31 ++++ httemplate/view/cust_bill.cgi | 1 + httemplate/view/cust_main/packages.html | 3 + httemplate/view/cust_main/packages/status.html | 43 +++-- httemplate/view/cust_main/payment_history.html | 13 +- .../view/cust_main/payment_history/credit.html | 14 +- .../view/cust_main/payment_history/payment.html | 14 +- .../cust_main/payment_history/voided_payment.html | 5 + httemplate/view/cust_pay.html | 9 ++ 25 files changed, 586 insertions(+), 69 deletions(-) create mode 100644 httemplate/elements/select-cust_pkg-balances.html create mode 100644 httemplate/elements/tr-select-cust_pkg-balances.html diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 40fbaf6c5..c335c0140 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -3009,6 +3009,13 @@ worry that config_items is freeside-specific and icky. 'type' => 'checkbox', }, + { + 'key' => 'pkg-balances', + 'section' => 'billing', + 'description' => 'Enable experimental package balances. Not recommended for general use.', + 'type' => 'checkbox', + }, + { key => "apacheroot", section => "deprecated", description => "DEPRECATED", type => "text" }, { key => "apachemachine", section => "deprecated", description => "DEPRECATED", type => "text" }, { key => "apachemachines", section => "deprecated", description => "DEPRECATED", type => "text" }, diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 649e0aa49..15adba3ed 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -598,6 +598,7 @@ sub tables_hashref { 'reasonnum', 'int', 'NULL', '', '', '', 'addlinfo', 'text', 'NULL', '', '', '', 'closed', 'char', 'NULL', 1, '', '', + 'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances ], 'primary_key' => 'crednum', 'unique' => [], @@ -611,6 +612,7 @@ sub tables_hashref { 'invnum', 'int', '', '', '', '', '_date', @date_type, '', '', 'amount', @money_type, '', '', + 'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances ], 'primary_key' => 'creditbillnum', 'unique' => [], @@ -941,6 +943,7 @@ sub tables_hashref { #'paybatch', 'varchar', 'NULL', $char_d, '', '', #for auditing purposes. 'payunique', 'varchar', 'NULL', $char_d, '', '', #separate paybatch "unique" functions from current usage + 'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances 'status', 'varchar', '', $char_d, '', '', 'session_id', 'varchar', 'NULL', $char_d, '', '', #only need 32 'statustext', 'text', 'NULL', '', '', '', @@ -970,6 +973,7 @@ sub tables_hashref { 'paybatch', 'varchar', 'NULL', $char_d, '', '', #for auditing purposes. 'payunique', 'varchar', 'NULL', $char_d, '', '', #separate paybatch "unique" functions from current usage 'closed', 'char', 'NULL', 1, '', '', + 'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances ], 'primary_key' => 'paynum', #i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it# 'unique' => [ [ 'payunique' ] ], @@ -989,6 +993,7 @@ sub tables_hashref { 'paymask', 'varchar', 'NULL', $char_d, '', '', 'paybatch', 'varchar', 'NULL', $char_d, '', '', #for auditing purposes. 'closed', 'char', 'NULL', 1, '', '', + 'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances 'void_date', @date_type, '', '', 'reason', 'varchar', 'NULL', $char_d, '', '', 'otaker', 'varchar', '', 32, '', '', @@ -1005,6 +1010,7 @@ sub tables_hashref { 'paynum', 'int', '', '', '', '', 'amount', @money_type, '', '', '_date', @date_type, '', '', + 'pkgnum', 'int', 'NULL', '', '', '', #desired pkgnum for pkg-balances ], 'primary_key' => 'billpaynum', 'unique' => [], diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 4e28af39c..4acdd85a3 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -235,6 +235,25 @@ sub cust_bill_pkg { ); } +=item cust_bill_pkg_pkgnum PKGNUM + +Returns the line items (see L) for this invoice and +specified pkgnum. + +=cut + +sub cust_bill_pkg_pkgnum { + my( $self, $pkgnum ) = @_; + qsearch( + { 'table' => 'cust_bill_pkg', + 'hashref' => { 'invnum' => $self->invnum, + 'pkgnum' => $pkgnum, + }, + 'order_by' => 'ORDER BY billpkgnum', + } + ); +} + =item cust_pkg Returns the packages (see L) corresponding to the line items for @@ -432,6 +451,38 @@ sub cust_credited { ; } +=item cust_bill_pay_pkgnum + +Returns all payment applications (see L) for this invoice +with matching pkgnum. + +=cut + +sub cust_bill_pay_pkgnum { + my( $self, $pkgnum ) = @_; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum, + 'pkgnum' => $pkgnum, + } + ); +} + +=item cust_credited_pkgnum + +Returns all applied credits (see L) for this invoice +with matching pkgnum. + +=cut + +sub cust_credited_pkgnum { + my( $self, $pkgnum ) = @_; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum, + 'pkgnum' => $pkgnum, + } + ); +} + =item tax Returns the tax amount (see L) for this invoice. @@ -465,6 +516,21 @@ sub owed { $balance; } +sub owed_pkgnum { + my( $self, $pkgnum ) = @_; + + #my $balance = $self->charged; + my $balance = 0; + $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum); + + $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum); + $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum); + + $balance = sprintf( "%.2f", $balance); + $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp + $balance; +} + =item apply_payments_and_credits =cut @@ -488,6 +554,13 @@ sub apply_payments_and_credits { my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay; my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit; + if ( $conf->exists('pkg-balances') ) { + # limit @payments & @credits to those w/ a pkgnum grepped from $self + my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg; + @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments; + @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits; + } + while ( $self->owed > 0 and ( @payments || @credits ) ) { my $app = ''; @@ -525,28 +598,39 @@ sub apply_payments_and_credits { die "guru meditation #12 and 35"; } + my $unapp_amount; if ( $app eq 'pay' ) { my $payment = shift @payments; - - $app = new FS::cust_bill_pay { - 'paynum' => $payment->paynum, - 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ), - }; + $unapp_amount = $payment->unapplied; + $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum }; + $app->pkgnum( $payment->pkgnum ) + if $conf->exists('pkg-balances') && $payment->pkgnum; } elsif ( $app eq 'credit' ) { my $credit = shift @credits; - - $app = new FS::cust_credit_bill { - 'crednum' => $credit->crednum, - 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ), - }; + $unapp_amount = $credit->credited; + $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum }; + $app->pkgnum( $credit->pkgnum ) + if $conf->exists('pkg-balances') && $credit->pkgnum; } else { die "guru meditation #12 and 35"; } + my $owed; + if ( $conf->exists('pkg-balances') && $app->pkgnum ) { + warn "owed_pkgnum ". $app->pkgnum; + $owed = $self->owed_pkgnum($app->pkgnum); + } else { + $owed = $self->owed; + } + next unless $owed > 0; + + warn "min ( $unapp_amount, $owed )\n"; + $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) ); + $app->invnum( $self->invnum ); my $error = $app->insert; diff --git a/FS/FS/cust_bill_ApplicationCommon.pm b/FS/FS/cust_bill_ApplicationCommon.pm index af7e0879e..ec694ca58 100644 --- a/FS/FS/cust_bill_ApplicationCommon.pm +++ b/FS/FS/cust_bill_ApplicationCommon.pm @@ -115,6 +115,8 @@ sub apply_to_lineitems { my @apply = (); + my $conf = new FS::Conf; + local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; local $SIG{QUIT} = 'IGNORE'; @@ -127,6 +129,8 @@ sub apply_to_lineitems { my $dbh = dbh; my @open = $self->cust_bill->open_cust_bill_pkg; #FOR UPDATE...? + @open = grep { $_->pkgnum == $self->pkgnum } @open + if $conf->exists('pkg-balances') && $self->pkgnum; warn "$me ". scalar(@open). " open line items for invoice ". $self->cust_bill->invnum. ": ". join(', ', @open). "\n" if $DEBUG; diff --git a/FS/FS/cust_bill_pay.pm b/FS/FS/cust_bill_pay.pm index b7ba2b711..e1b02aecb 100644 --- a/FS/FS/cust_bill_pay.pm +++ b/FS/FS/cust_bill_pay.pm @@ -7,6 +7,7 @@ use FS::cust_main_Mixin; use FS::cust_bill_ApplicationCommon; use FS::cust_bill; use FS::cust_pay; +use FS::cust_pkg; @ISA = qw( FS::cust_main_Mixin FS::cust_bill_ApplicationCommon ); @@ -121,6 +122,7 @@ sub check { || $self->ut_foreign_key('invnum', 'cust_bill', 'invnum' ) || $self->ut_numbern('_date') || $self->ut_money('amount') + || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum') ; return $error if $error; diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 47a8119ee..6c3effa13 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -8,6 +8,7 @@ use FS::Misc qw(send_email); use FS::Record qw( qsearch qsearchs dbdef ); use FS::cust_main_Mixin; use FS::cust_main; +use FS::cust_pkg; use FS::cust_refund; use FS::cust_credit_bill; use FS::part_pkg; @@ -95,6 +96,10 @@ Text Books closed flag, empty or `Y' +=item pkgnum + +Desired pkgnum when using experimental package balances. + =back =head1 METHODS @@ -295,6 +300,7 @@ sub check { || $self->ut_foreign_key('reasonnum', 'reason', 'reasonnum') || $self->ut_textn('addlinfo') || $self->ut_enum('closed', [ '', 'Y' ]) + || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum') ; return $error if $error; diff --git a/FS/FS/cust_credit_bill.pm b/FS/FS/cust_credit_bill.pm index 375c885a0..900a5c0d5 100644 --- a/FS/FS/cust_credit_bill.pm +++ b/FS/FS/cust_credit_bill.pm @@ -8,6 +8,7 @@ use FS::cust_main_Mixin; use FS::cust_bill_ApplicationCommon; use FS::cust_bill; use FS::cust_credit; +use FS::cust_pkg; @ISA = qw( FS::cust_main_Mixin FS::cust_bill_ApplicationCommon ); @@ -122,6 +123,7 @@ sub check { || $self->ut_foreign_key('invnum', 'cust_bill', 'invnum' ) || $self->ut_numbern('_date') || $self->ut_money('amount') + || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum') ; return $error if $error; diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index f6ac186f9..9c131744d 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -6110,32 +6110,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'); } @@ -6183,33 +6203,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'); } @@ -6270,6 +6309,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. @@ -6306,6 +6380,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. @@ -6320,6 +6409,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 @@ -6372,6 +6477,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 @@ -7001,6 +7126,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. @@ -7013,6 +7154,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. diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm index 4ff291934..e1e6df2a2 100644 --- a/FS/FS/cust_pay.pm +++ b/FS/FS/cust_pay.pm @@ -17,6 +17,7 @@ use FS::cust_bill; use FS::cust_bill_pay; use FS::cust_pay_refund; use FS::cust_main; +use FS::cust_pkg; use FS::cust_pay_void; @ISA = qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record ); @@ -62,28 +63,54 @@ currently supported: =over 4 -=item paynum - primary key (assigned automatically for new payments) +=item paynum -=item custnum - customer (see L) +primary key (assigned automatically for new payments) -=item _date - specified as a UNIX timestamp; see L. Also see +=item custnum + +customer (see L) + +=item _date + +specified as a UNIX timestamp; see L. Also see L and L for conversion functions. -=item paid - Amount of this payment +=item paid + +Amount of this payment + +=item otaker + +order taker (assigned automatically, see L) + +=item payby + +Payment Type (See L for valid payby values) + +=item payinfo + +Payment Information (See L for data format) + +=item paymask + +Masked payinfo (See L for how this works) + +=item paybatch -=item otaker - order taker (assigned automatically, see L) +text field for tracking card processing or other batch grouping -=item payby - Payment Type (See L for valid payby values) +=item payunique -=item payinfo - Payment Information (See L for data format) +Optional unique identifer to prevent duplicate transactions. -=item paymask - Masked payinfo (See L for how this works) +=item closed -=item paybatch - text field for tracking card processing or other batch grouping +books closed flag, empty or `Y' -=item payunique - Optional unique identifer to prevent duplicate transactions. +=item pkgnum -=item closed - books closed flag, empty or `Y' +Desired pkgnum when using experimental package balances. =back @@ -417,6 +444,7 @@ sub check { || $self->ut_textn('paybatch') || $self->ut_textn('payunique') || $self->ut_enum('closed', [ '', 'Y' ]) + || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum') || $self->payinfo_check() ; return $error if $error; diff --git a/FS/FS/cust_pay_pending.pm b/FS/FS/cust_pay_pending.pm index fba19ea19..f48e1a8c1 100644 --- a/FS/FS/cust_pay_pending.pm +++ b/FS/FS/cust_pay_pending.pm @@ -6,6 +6,7 @@ use FS::Record qw( qsearch qsearchs dbh ); #dbh for _upgrade_data use FS::payinfo_transaction_Mixin; use FS::cust_main_Mixin; use FS::cust_main; +use FS::cust_pkg; use FS::cust_pay; @ISA = qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record ); @@ -77,6 +78,10 @@ Expiration date Unique identifer to prevent duplicate transactions. +=item pkgnum + +Desired pkgnum when using experimental package balances. + =item status Pending transaction status, one of the following: @@ -193,6 +198,7 @@ sub check { #|| $self->ut_money('cust_balance') || $self->ut_hexn('session_id') || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' ) + || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum') || $self->payinfo_check() #payby/payinfo/paymask/paydate ; return $error if $error; diff --git a/FS/FS/cust_pay_void.pm b/FS/FS/cust_pay_void.pm index de05f710b..86fbbe5ab 100644 --- a/FS/FS/cust_pay_void.pm +++ b/FS/FS/cust_pay_void.pm @@ -9,6 +9,7 @@ use FS::cust_pay; #use FS::cust_bill_pay; #use FS::cust_pay_refund; #use FS::cust_main; +use FS::cust_pkg; @ISA = qw( FS::Record FS::payinfo_Mixin ); @@ -40,24 +41,44 @@ are currently supported: =over 4 -=item paynum - primary key (assigned automatically for new payments) +=item paynum -=item custnum - customer (see L) +primary key (assigned automatically for new payments) -=item paid - Amount of this payment +=item custnum -=item _date - specified as a UNIX timestamp; see L. Also see +customer (see L) + +=item paid + +Amount of this payment + +=item _date + +specified as a UNIX timestamp; see L. Also see L and L for conversion functions. -=item payby - `CARD' (credit cards), `CHEK' (electronic check/ACH), +=item payby + +`CARD' (credit cards), `CHEK' (electronic check/ACH), `LECB' (phone bill billing), `BILL' (billing), `CASH' (cash), `WEST' (Western Union), `MCRD' (Manual credit card), or `COMP' (free) -=item payinfo - card number, check #, or comp issuer (4-8 lowercase alphanumerics; think username), respectively +=item payinfo + +card number, check #, or comp issuer (4-8 lowercase alphanumerics; think username), respectively + +=item paybatch + +text field for tracking card processing + +=item closed + +books closed flag, empty or `Y' -=item paybatch - text field for tracking card processing +=item pkgnum -=item closed - books closed flag, empty or `Y' +Desired pkgnum when using experimental package balances. =item void_date @@ -156,6 +177,7 @@ sub check { || $self->ut_number('_date') || $self->ut_textn('paybatch') || $self->ut_enum('closed', [ '', 'Y' ]) + || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum') || $self->ut_numbern('void_date') || $self->ut_textn('reason') ; diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 00a030117..4724514c0 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -1750,6 +1750,63 @@ sub statuscolor { $statuscolor{$self->status}; } +=item pkg_label + +Returns a label for this package. (Currently "pkgnum: pkg - comment" or +"pkg-comment" depending on user preference). + +=cut + +sub pkg_label { + my $self = shift; + my $label = $self->part_pkg->pkg_comment( 'nopkgpart' => 1 ); + $label = $self->pkgnum. ": $label" + if $FS::CurrentUser::CurrentUser->option('show_pkgnum'); + $label; +} + +=item pkg_label_long + +Returns a long label for this package, adding the primary service's label to +pkg_label. + +=cut + +sub pkg_label_long { + my $self = shift; + my $label = $self->pkg_label; + my $cust_svc = $self->primary_cust_svc; + $label .= ' ('. ($cust_svc->label)[1]. ')' if $cust_svc; + $label; +} + +=item primary_cust_svc + +Returns a primary service (as FS::cust_svc object) if one can be identified. + +=cut + +#for labeling purposes - might not 100% match up with part_pkg->svcpart's idea + +sub primary_cust_svc { + my $self = shift; + + my @cust_svc = $self->cust_svc; + + return '' unless @cust_svc; #no serivces - irrelevant then + + return $cust_svc[0] if scalar(@cust_svc) == 1; #always return a single service + + # primary service as specified in the package definition + # or exactly one service definition with quantity one + my $svcpart = $self->part_pkg->svcpart; + @cust_svc = grep { $_->svcpart == $svcpart } @cust_svc; + return $cust_svc[0] if scalar(@cust_svc) == 1; + + #couldn't identify one thing.. + return ''; +} + =item labels Returns a list of lists, calling the label method for all services diff --git a/httemplate/edit/cust_credit.cgi b/httemplate/edit/cust_credit.cgi index c9ca31ff3..c6b2bcbea 100755 --- a/httemplate/edit/cust_credit.cgi +++ b/httemplate/edit/cust_credit.cgi @@ -40,6 +40,16 @@ Credit +% if ( $conf->exists('pkg-balances') ) { + <% include('/elements/tr-select-cust_pkg-balances.html', + 'custnum' => $custnum, + 'cgi' => $cgi + ) + %> +% } else { + +% } +
@@ -65,4 +75,7 @@ my $_date = time; my $otaker = getotaker; my $p1 = popurl(1); +my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or die "unknown custnum $custnum\n"; + diff --git a/httemplate/edit/cust_pay.cgi b/httemplate/edit/cust_pay.cgi index 3c2877498..4dff06d88 100755 --- a/httemplate/edit/cust_pay.cgi +++ b/httemplate/edit/cust_pay.cgi @@ -72,6 +72,16 @@ Payment % } +% if ( $conf->exists('pkg-balances') ) { + <% include('/elements/tr-select-cust_pkg-balances.html', + 'custnum' => $custnum, + 'cgi' => $cgi + ) + %> +% } else { + +% } +
@@ -95,7 +105,7 @@ my $money_char = $conf->config('money_char') || '$'; die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Post payment'); -my($link, $linknum, $paid, $payby, $payinfo, $_date); +my($link, $linknum, $paid, $payby, $payinfo, $_date, $pkgnum); if ( $cgi->param('error') ) { $link = $cgi->param('link'); $linknum = $cgi->param('linknum'); @@ -131,7 +141,7 @@ if ( $link eq 'invnum' ) { my $cust_bill = qsearchs('cust_bill', { 'invnum' => $linknum } ) or die "unknown invnum $linknum"; $custnum = $cust_bill->custnum; -} elsif ( $link eq 'custnum' ) { +} elsif ( $link eq 'custnum' || $link eq 'popup' ) { $custnum = $linknum; } diff --git a/httemplate/edit/process/cust_pay.cgi b/httemplate/edit/process/cust_pay.cgi index 647f6fc6c..f8ac8b183 100755 --- a/httemplate/edit/process/cust_pay.cgi +++ b/httemplate/edit/process/cust_pay.cgi @@ -46,7 +46,9 @@ my $new = new FS::cust_pay ( { _date => $_date, map { $_, scalar($cgi->param($_)); - } qw(paid payby payinfo paybatch) + } qw( paid payby payinfo paybatch + pkgnum + ) #} fields('cust_pay') } ); diff --git a/httemplate/elements/select-cust_pkg-balances.html b/httemplate/elements/select-cust_pkg-balances.html new file mode 100644 index 000000000..d41bd033e --- /dev/null +++ b/httemplate/elements/select-cust_pkg-balances.html @@ -0,0 +1,30 @@ + +<%init> + +my %opt = @_; + +my @cust_pkg; +if ( $opt{'cust_pkg'} ) { + + @cust_pkg = @{ $opt{'cust_pkg'} }; + +} else { + + my $custnum = $opt{'custnum'}; + + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or die "unknown custnum $custnum\n"; + + @cust_pkg = + grep { ! $_->get('cancel') || $cust_main->balance_pkgnum($_->pkgnum) } + $cust_main->all_pkgs; + +} + + diff --git a/httemplate/elements/tr-select-cust_pkg-balances.html b/httemplate/elements/tr-select-cust_pkg-balances.html new file mode 100644 index 000000000..89dc5d415 --- /dev/null +++ b/httemplate/elements/tr-select-cust_pkg-balances.html @@ -0,0 +1,31 @@ +% if ( scalar(@cust_pkg) == 0 ) { + +% } elsif ( scalar(@cust_pkg) == 1 ) { + +% } else { + + For package + + <% include('select-cust_pkg-balances.html', + 'cust_pkg' => \@cust_pkg, + 'cgi' => $opt{'cgi'}, + ) + %> + + + +% } + +<%init> +my %opt = @_; + +my $custnum = $opt{'custnum'}; + +my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or die "unknown custnum $custnum\n"; + +my @cust_pkg = + grep { ! $_->get('cancel') || $cust_main->balance_pkgnum($_->pkgnum) } + $cust_main->all_pkgs; + + diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi index 450c74e61..2673e8239 100755 --- a/httemplate/view/cust_bill.cgi +++ b/httemplate/view/cust_bill.cgi @@ -6,6 +6,7 @@ % if ( $cust_bill->owed > 0 % && scalar( grep $payby{$_}, qw(BILL CASH WEST MCRD) ) % && $FS::CurrentUser::CurrentUser->access_right('Post payment') +% && ! $conf->exists('pkg-balances') % ) % { % my $s = 0; diff --git a/httemplate/view/cust_main/packages.html b/httemplate/view/cust_main/packages.html index 428794b7d..8fbefae50 100755 --- a/httemplate/view/cust_main/packages.html +++ b/httemplate/view/cust_main/packages.html @@ -138,6 +138,9 @@ my %conf_opt = ( #for status.html 'cust_pkg-show_autosuspend' => $conf->exists('cust_pkg-show_autosuspend'), + #for status.html pkg-balances + 'pkg-balances' => $conf->exists('pkg-balances'), + 'money_char' => ( $conf->config('money_char') || '$' ), #for location.html 'countrydefault' => $countrydefault, diff --git a/httemplate/view/cust_main/packages/status.html b/httemplate/view/cust_main/packages/status.html index 6daff5031..f3b2faaf4 100644 --- a/httemplate/view/cust_main/packages/status.html +++ b/httemplate/view/cust_main/packages/status.html @@ -8,15 +8,16 @@ <% pkg_status_row($cust_pkg, 'Cancelled', 'cancel', 'color'=>'FF0000', %opt ) %> - <% pkg_status_row_colspan( + <% pkg_status_row_colspan( $cust_pkg, ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '', 'align'=>'right', 'color'=>'ff0000', 'size'=>'-2', 'colspan'=>$colspan, + %opt ) %> % unless ( $cust_pkg->get('setup') ) { - <% pkg_status_row_colspan('Never billed', '', 'colspan'=>$colspan, ) %> + <% pkg_status_row_colspan( $cust_pkg, 'Never billed', '', 'colspan'=>$colspan, %opt, ) %> % } else { @@ -34,14 +35,15 @@ <% pkg_status_row( $cust_pkg, 'Suspended', 'susp', 'color'=>'FF9900', %opt ) %> - <% pkg_status_row_colspan( + <% pkg_status_row_colspan( $cust_pkg, ( $cpr ? $cpr->reasontext. ' by '. $cpr->otaker : '' ), '', 'align'=>'right', 'color'=>'FF9900', 'size'=>'-2', 'colspan'=>$colspan, + %opt, ) %> % unless ( $cust_pkg->get('setup') ) { - <% pkg_status_row_colspan('Never billed', '', 'colspan'=>$colspan ) %> + <% pkg_status_row_colspan( $cust_pkg, 'Never billed', '', 'colspan'=>$colspan, %opt ) %> % } else { <% pkg_status_row($cust_pkg, 'Setup', 'setup', %opt ) %> % } @@ -70,7 +72,7 @@ % % unless ( $part_pkg->freq ) { - <% pkg_status_row_colspan('Not yet billed (one-time charge)', '', 'colspan'=>$colspan, ) %> + <% pkg_status_row_colspan( $cust_pkg, 'Not yet billed (one-time charge)', '', 'colspan'=>$colspan, %opt ) %> <% pkg_status_row_if($cust_pkg, 'Start billing', 'start_date', %opt) %> @@ -86,7 +88,7 @@ % } else { - <% pkg_status_row_colspan("Not yet billed ($billed_or_prepaid ". myfreq($part_pkg). ')', '', 'colspan'=>$colspan ) %> + <% pkg_status_row_colspan($cust_pkg, "Not yet billed ($billed_or_prepaid ". myfreq($part_pkg). ')', '', 'colspan'=>$colspan, %opt ) %> <% pkg_status_row_if($cust_pkg, 'Start billing', 'start_date', %opt) %> @@ -96,7 +98,7 @@ % % unless ( $part_pkg->freq ) { - <% pkg_status_row_colspan('One-time charge', '', 'colspan'=>$colspan, ) %> + <% pkg_status_row_colspan($cust_pkg, 'One-time charge', '', 'colspan'=>$colspan, %opt ) %> <% pkg_status_row($cust_pkg, 'Billed', 'setup', %opt) %> @@ -104,18 +106,20 @@ % % if (scalar($cust_pkg->overlimit)) { - <% pkg_status_row_colspan( + <% pkg_status_row_colspan( $cust_pkg, 'Overlimit', $billed_or_prepaid. ' '. myfreq($part_pkg), 'color'=>'FFD000', 'colspan'=>$colspan, + %opt ) %> % } else { - <% pkg_status_row_colspan( + <% pkg_status_row_colspan( $cust_pkg, 'Active', $billed_or_prepaid. ' '. myfreq($part_pkg), 'color'=>'00CC00', 'colspan'=>$colspan, + %opt ) %> % } @@ -169,7 +173,6 @@ - <%init> my %opt = @_; @@ -218,8 +221,15 @@ sub pkg_status_row { $html .= qq() if length($color); $html .= qq($title ); $html .= qq() if length($color); + + if ( $opt{'pkg_balances'} && ! $cust_pkg->{_printed_balance}++ ) { #kludge + $html .= ' (Balance: '. $opt{'money_char'}. + $cust_pkg->cust_main->balance_pkgnum($cust_pkg->pkgnum). + ')'; + } + $html .= qq(); - $html .= pkg_datestr($cust_pkg, $field, %opt).''; + $html .= pkg_datestr($cust_pkg, $field, %opt). ''; $html; } @@ -253,7 +263,7 @@ sub pkg_status_row_changed { my $part_pkg = $old->part_pkg; my $label = 'Changed from '. $cust_pkg->change_pkgnum. ': '. $part_pkg->pkg_comment(nopartpkg => 1); - $html .= pkg_status_row_colspan( $label, '', + $html .= pkg_status_row_colspan( $cust_pkg, $label, '', 'size' => '-1', 'align' => 'right', 'colspan' => $opt{'colspan'}, @@ -264,7 +274,7 @@ sub pkg_status_row_changed { } sub pkg_status_row_colspan { - my($title, $addl, %opt) = @_; + my($cust_pkg, $title, $addl, %opt) = @_; my $colspan = $opt{'colspan'}; @@ -279,6 +289,13 @@ sub pkg_status_row_colspan { $html .= qq() if $color && !$size; $html .= qq() if length($color) || $size; $html .= ", $addl" if length($addl); + + if ( $opt{'pkg-balances'} && ! $cust_pkg->{_printed_balance}++ ) { #kludge + $html .= ' (Balance: '. $opt{'money_char'}. + $cust_pkg->cust_main->balance_pkgnum($cust_pkg->pkgnum). + ')'; + } + $html .= qq(); $html; diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html index 1711e1449..8adc95426 100644 --- a/httemplate/view/cust_main/payment_history.html +++ b/httemplate/view/cust_main/payment_history.html @@ -374,13 +374,16 @@ my %status = ( #get payment history my @history = (); -my %opt = +my %opt = ( ( map { $_ => scalar($conf->config($_)) } qw( card_refund-days ) ), ( map { $_ => $conf->exists($_) } - qw( deletepayments deleterefunds ) - ); + qw( deletepayments deleterefunds pkg-balances ) + ) +); + +warn Dumper(\%opt); #invoices foreach my $cust_bill ($cust_main->cust_bill) { @@ -405,7 +408,7 @@ foreach my $cust_pay ($cust_main->cust_pay) { foreach my $cust_pay_void ($cust_main->cust_pay_void) { push @history, { 'date' => $cust_pay_void->_date, - 'desc' => include('payment_history/voided_payment.html', $cust_pay_void), + 'desc' => include('payment_history/voided_payment.html', $cust_pay_void, %opt ), 'void_payment' => $cust_pay_void->paid, }; @@ -415,7 +418,7 @@ foreach my $cust_pay_void ($cust_main->cust_pay_void) { foreach my $cust_credit ($cust_main->cust_credit) { push @history, { 'date' => $cust_credit->_date, - 'desc' => include('payment_history/credit.html', $cust_credit), + 'desc' => include('payment_history/credit.html', $cust_credit, %opt ), 'credit' => $cust_credit->amount, }; diff --git a/httemplate/view/cust_main/payment_history/credit.html b/httemplate/view/cust_main/payment_history/credit.html index 2deb27564..058c6f536 100644 --- a/httemplate/view/cust_main/payment_history/credit.html +++ b/httemplate/view/cust_main/payment_history/credit.html @@ -9,7 +9,13 @@ my $curuser = $FS::CurrentUser::CurrentUser; my @cust_credit_bill = $cust_credit->cust_credit_bill; my @cust_credit_refund = $cust_credit->cust_credit_refund; -my( $pre, $post, $desc, $apply, $ext ) = ( '', '', '', '', '' ); +my $desc = ''; +if ( $opt{'pkg-balances'} && $cust_credit->pkgnum ) { + my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $cust_credit->pkgnum } ); + $desc .= ' for '. $cust_pkg->pkg_label_long; +} + +my( $pre, $post, $apply, $ext ) = ( '', '', '', '' ); if ( scalar(@cust_credit_bill) == 0 && scalar(@cust_credit_refund) == 0 ) { #completely unapplied @@ -45,15 +51,15 @@ if ( scalar(@cust_credit_bill) == 0 && scalar(@cust_credit_refund) == 0 && $cust_credit->credited == 0 ) { #applied to one invoice, the usual situation - $desc = ' '. $cust_credit_bill[0]->applied_to_invoice; + $desc .= ' '. $cust_credit_bill[0]->applied_to_invoice; } elsif ( scalar(@cust_credit_bill) == 0 && scalar(@cust_credit_refund) == 1 && $cust_credit->credited == 0 ) { #applied to one refund - $desc = ' refunded on '. time2str("%D", $cust_credit_refund[0]->_date); + $desc .= ' refunded on '. time2str("%D", $cust_credit_refund[0]->_date); } else { #complicated - $desc = '
'; + $desc .= '
'; foreach my $app ( sort { $a->_date <=> $b->_date } ( @cust_credit_bill, @cust_credit_refund ) ) { if ( $app->isa('FS::cust_credit_bill') ) { diff --git a/httemplate/view/cust_main/payment_history/payment.html b/httemplate/view/cust_main/payment_history/payment.html index 2e24b1785..a4a349bb8 100644 --- a/httemplate/view/cust_main/payment_history/payment.html +++ b/httemplate/view/cust_main/payment_history/payment.html @@ -32,7 +32,13 @@ $payby =~ s/^MCRD$/Manual credit card/; $payby =~ s/^BILL$//; my $info = $payby ? "($payby$payinfo)" : ''; -my( $pre, $post, $desc, $apply, $ext ) = ( '', '', '', '', '' ); +my $desc = ''; +if ( $opt{'pkg-balances'} && $cust_pay->pkgnum ) { + my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $cust_pay->pkgnum } ); + $desc .= ' for '. $cust_pkg->pkg_label_long; +} + +my( $pre, $post, $apply, $ext ) = ( '', '', '', '' ); if ( scalar(@cust_bill_pay) == 0 && scalar(@cust_pay_refund) == 0 ) { #completely unapplied @@ -68,15 +74,15 @@ if ( scalar(@cust_bill_pay) == 0 && scalar(@cust_pay_refund) == 0 && $cust_pay->unapplied == 0 ) { #applied to one invoice, the usual situation - $desc = ' '. $cust_bill_pay[0]->applied_to_invoice; + $desc .= ' '. $cust_bill_pay[0]->applied_to_invoice; } elsif ( scalar(@cust_bill_pay) == 0 && scalar(@cust_pay_refund) == 1 && $cust_pay->unapplied == 0 ) { #applied to one refund - $desc = ' refunded on '. time2str("%D", $cust_pay_refund[0]->_date); + $desc .= ' refunded on '. time2str("%D", $cust_pay_refund[0]->_date); } else { #complicated - $desc = '
'; + $desc .= '
'; foreach my $app ( sort { $a->_date <=> $b->_date } ( @cust_bill_pay, @cust_pay_refund ) ) { if ( $app->isa('FS::cust_bill_pay') ) { diff --git a/httemplate/view/cust_main/payment_history/voided_payment.html b/httemplate/view/cust_main/payment_history/voided_payment.html index 9cbc47b66..08469dbe9 100644 --- a/httemplate/view/cust_main/payment_history/voided_payment.html +++ b/httemplate/view/cust_main/payment_history/voided_payment.html @@ -18,6 +18,11 @@ $payby =~ s/^BILL$//; $payby =~ s/^(CARD|COMP)$/$1 /; my $info = $payby ? " ($payby$payinfo)" : ''; +if ( $opt{'pkg-balances'} && $cust_pay_void->pkgnum ) { + my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $cust_pay_void->pkgnum } ); + $info .= ' for '. $cust_pkg->pkg_label_long; +} + my $unvoid = ''; if ( $cust_pay_void->closed !~ /^Y/i && $curuser->access_right('Unvoid') diff --git a/httemplate/view/cust_pay.html b/httemplate/view/cust_pay.html index a5c99ac55..2f23d9e14 100644 --- a/httemplate/view/cust_pay.html +++ b/httemplate/view/cust_pay.html @@ -93,6 +93,15 @@ % } +% if ( $conf->exists('pkg-balances') && $cust_pay->pkgnum ) { +% my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $cust_pay->pkgnum } ); + + For package + <% $cust_pkg->pkg_label_long %> + + +% } + % if ( $link eq 'print' ) { -- 2.11.0