From: Ivan Kohler Date: Sun, 9 Dec 2012 18:30:54 +0000 (-0800) Subject: create credits by selecting line items, RT#18676 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=93e3a52f23c3473207f29f36cda06adfe221353f create credits by selecting line items, RT#18676 --- diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index 826569b25..a83af1326 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -665,8 +665,9 @@ sub set_display { =item disintegrate -Returns a list of cust_bill_pkg objects each with no more than a single class -(including setup or recur) of charge. +Returns a hash: keys are "setup", "recur" or usage classnum, values are +FS::cust_bill_pkg objects, each with no more than a single class (setup or +recur) of charge. =cut @@ -843,6 +844,18 @@ sub _X_show_zero { $self->cust_pkg->_X_show_zero($what); } +=item credited [ BEFORE, AFTER, OPTIONS ] + +Returns the sum of credits applied to this item. Arguments are the same as +owed_sql/paid_sql/credited_sql. + +=cut + +sub credited { + my $self = shift; + $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum); +} + =back =head1 CLASS METHODS diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 6185fc472..f7f375874 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -172,7 +172,7 @@ sub insert { $dbh->commit or die $dbh->errstr if $oldAutoCommit; - #false laziness w/ cust_credit::insert + #false laziness w/ cust_pay::insert if ( $unsuspendauto && $old_balance && $cust_main->balance <= 0 ) { my @errors = $cust_main->unsuspend; #return @@ -618,6 +618,259 @@ sub credited_sql { unapplied_sql(); } +=item credit_lineitems + +Example: + + my $error = FS::cust_credit->credit_lineitems( + #the lineitems + 'billpkgnums' => \@billpkgnums, + + #the credit + 'newreasonnum' => scalar($cgi->param('newreasonnum')), + 'newreasonnum_type' => scalar($cgi->param('newreasonnumT')), + map { $_ => scalar($cgi->param($_)) } + fields('cust_credit') + ); + +=cut + +#maybe i should just be an insert with extra args instead of a class method +use FS::cust_bill_pkg; +sub credit_lineitems { + my( $class, %arg ) = @_; + + my $curuser = $FS::CurrentUser::CurrentUser; + + #some false laziness w/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html + + my $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $arg{custnum} }, + 'extra_sql' => ' AND '. $curuser->agentnums_sql, + }) or return 'unknown customer'; + + + 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; + + #my @cust_bill_pkg = qsearch({ + # 'select' => 'cust_bill_pkg.*', + # 'table' => 'cust_bill_pkg', + # 'addl_from' => ' LEFT JOIN cust_bill USING (invnum) '. + # ' LEFT JOIN cust_main USING (custnum) ', + # 'extra_sql' => ' WHERE custnum = $custnum AND billpkgnum IN ('. + # join( ',', @{$arg{billpkgnums}} ). ')', + # 'order_by' => 'ORDER BY invnum ASC, billpkgnum ASC', + #}); + + my $error = ''; + if ($arg{reasonnum} == -1) { + + $error = 'Enter a new reason (or select an existing one)' + unless $arg{newreasonnum} !~ /^\s*$/; + my $reason = new FS::reason { + 'reason' => $arg{newreasonnum}, + 'reason_type' => $arg{newreasonnum_type}, + }; + $error ||= $reason->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error inserting reason: $error"; + } + $arg{reasonnum} = $reason->reasonnum; + } + + my $cust_credit = new FS::cust_credit ( { + map { $_ => $arg{$_} } + #fields('cust_credit') + qw( custnum _date amount reason reasonnum addlinfo ), #pkgnum eventnum + } ); + $error = $cust_credit->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error inserting credit: $error"; + } + + #my $subtotal = 0; + my $taxlisthash = {}; + my %cust_credit_bill = (); + my %cust_bill_pkg = (); + my %cust_credit_bill_pkg = (); + foreach my $billpkgnum ( @{$arg{billpkgnums}} ) { + my $setuprecur = shift @{$arg{setuprecurs}}; + my $amount = shift @{$arg{amounts}}; + + my $cust_bill_pkg = qsearchs({ + 'table' => 'cust_bill_pkg', + 'hashref' => { 'billpkgnum' => $billpkgnum }, + 'addl_from' => 'LEFT JOIN cust_bill USING (invnum)', + 'extra_sql' => 'AND custnum = '. $cust_main->custnum, + }) or die "unknown billpkgnum $billpkgnum"; + + if ( $setuprecur eq 'setup' ) { + $cust_bill_pkg->setup($amount); + $cust_bill_pkg->recur(0); + $cust_bill_pkg->unitrecur(0); + $cust_bill_pkg->type(''); + } else { + $setuprecur = 'recur'; #in case its a usage classnum? + $cust_bill_pkg->recur($amount); + $cust_bill_pkg->setup(0); + $cust_bill_pkg->unitsetup(0); + } + + push @{$cust_bill_pkg{$cust_bill_pkg->invnum}}, $cust_bill_pkg; + + #unapply any payments applied to this line item (other credits too?) + foreach my $cust_bill_pay_pkg ( $cust_bill_pkg->cust_bill_pay_pkg($setuprecur) ) { + $error = $cust_bill_pay_pkg->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error unapplying payment: $error"; + } + } + + #$subtotal += $amount; + $cust_credit_bill{$cust_bill_pkg->invnum} += $amount; + push @{ $cust_credit_bill_pkg{$cust_bill_pkg->invnum} }, + new FS::cust_credit_bill_pkg { + 'billpkgnum' => $cust_bill_pkg->billpkgnum, + 'amount' => $amount, + 'setuprecur' => $setuprecur, + 'sdate' => $cust_bill_pkg->sdate, + 'edate' => $cust_bill_pkg->edate, + }; + + my $part_pkg = $cust_bill_pkg->part_pkg; + $cust_main->_handle_taxes( $part_pkg, + $taxlisthash, + $cust_bill_pkg, + $cust_bill_pkg->cust_pkg, + $cust_bill_pkg->cust_bill->_date, + $cust_bill_pkg->cust_pkg->pkgpart, + ); + } + + ### + # now loop through %cust_credit_bill and insert those + ### + + # (hack to prevent cust_credit_bill_pkg insertion) + local($FS::cust_bill_ApplicationCommon::skip_apply_to_lineitems_hack) = 1; + + foreach my $invnum ( sort { $a <=> $b } keys %cust_credit_bill ) { + + #taxes + + if ( @{ $cust_bill_pkg{$invnum} } ) { + + my $listref_or_error = + $cust_main->calculate_taxes( $cust_bill_pkg{$invnum}, $taxlisthash, $cust_bill_pkg{$invnum}->[0]->cust_bill->_date ); + + unless ( ref( $listref_or_error ) ) { + $dbh->rollback if $oldAutoCommit; + return "Error calculating taxes: $listref_or_error"; + } + + # so, loop through the taxlines, apply just that amount to the tax line + # item (save for later insert) & add to $ + + #my @taxlines = (); + #my $taxtotal = 0; + foreach my $taxline ( @$listref_or_error ) { + + #find equivalent tax line items on the existing invoice + # (XXX need a more specific/deterministic way to find these than itemdesc..) + my $tax_cust_bill_pkg = qsearchs('cust_bill_pkg', { + 'invnum' => $invnum, + 'pkgnum' => 0, #$taxline->invnum + 'itemdesc' => $taxline->desc, + }); + + my $amount = $taxline->setup; + my $desc = $taxline->desc; + + foreach my $location ( $tax_cust_bill_pkg->cust_bill_pkg_tax_Xlocation ) { + + $location->cust_bill_pkg_desc($taxline->desc); #ugh @ that kludge + + #$taxtotal += $location->amount; + $amount -= $location->amount; + + #push @taxlines, + # #[ $location->desc, $taxline->setup, $taxlocnum, $taxratelocnum ]; + # [ $location->desc, $location->amount, $taxlocnum, $taxratelocnum ]; + $cust_credit_bill{$invnum} += $location->amount; + push @{ $cust_credit_bill_pkg{$invnum} }, + new FS::cust_credit_bill_pkg { + 'billpkgnum' => $tax_cust_bill_pkg->billpkgnum, + 'amount' => $location->amount, + 'setuprecur' => 'setup', + 'billpkgtaxlocationnum' => $location->billpkgtaxlocationnum, + 'billpkgtaxratelocationnum' => $location->billpkgtaxratelocationnum, + }; + + } + if ($amount > 0) { + #$taxtotal += $amount; + #push @taxlines, + # [ $taxline->itemdesc. ' (default)', sprintf('%.2f', $amount), '', '' ]; + + $cust_credit_bill{$invnum} += $amount; + push @{ $cust_credit_bill_pkg{$invnum} }, + new FS::cust_credit_bill_pkg { + 'billpkgnum' => $tax_cust_bill_pkg->billpkgnum, + 'amount' => $amount, + 'setuprecur' => 'setup', + }; + + } + } + + } + + #insert cust_credit_bill + + my $cust_credit_bill = new FS::cust_credit_bill { + 'crednum' => $cust_credit->crednum, + 'invnum' => $invnum, + 'amount' => $cust_credit_bill{$invnum}, + }; + $error = $cust_credit_bill->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error applying credit of $cust_credit_bill{$invnum} ". + " to invoice $invnum: $error"; + } + + #and then insert cust_credit_bill_pkg for each cust_bill_pkg + foreach my $cust_credit_bill_pkg ( @{$cust_credit_bill_pkg{$invnum}} ) { + $cust_credit_bill_pkg->creditbillnum( $cust_credit_bill->creditbillnum ); + $error = $cust_credit_bill_pkg->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "Error applying credit to line item: $error"; + } + } + + } + + #$return->{taxlines} = \@taxlines; + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + =back =head1 BUGS diff --git a/httemplate/edit/credit-cust_bill_pkg.html b/httemplate/edit/credit-cust_bill_pkg.html new file mode 100644 index 000000000..e317936b3 --- /dev/null +++ b/httemplate/edit/credit-cust_bill_pkg.html @@ -0,0 +1,249 @@ +<& /elements/header-popup.html, 'Credit line items' &> + +
+ + + + + + +% my $old_invnum = 0; +%# foreach my $cust_bill_pkg ( @cust_bill_pkg ) { +% foreach my $item ( @items ) { +% my( $setuprecur, $cust_bill_pkg ) = @$item; + +% my $method = $setuprecur eq 'setup' ? 'setup' : 'recur'; +% my $amount = $cust_bill_pkg->$method(); +% my $credited = $cust_bill_pkg->credited('', '', 'setuprecur'=>$method); +% $amount -= $credited; +% $amount = sprintf('%.2f', $amount); +% next unless $amount > 0; + +% if ( $cust_bill_pkg->invnum ne $old_invnum ) { + + +% $old_invnum = $cust_bill_pkg->invnum; +% } + + + + +%# show one-time/setup vs recur vs usage? + + + +% } + + + + + + + + + + + + + + + + + +
 
Invoice #<% $cust_bill_pkg->invnum %> - <% time2str($date_format, $cust_bill_pkg->cust_bill->_date) %>
+ + <% $cust_bill_pkg->desc |h %><% $money_char. $amount %>
 
Subtotal: <% $money_char %><% sprintf('%.2f', 0) %>
Taxes: <% $money_char %><% sprintf('%.2f', 0) %>
Total credit amount: + <% $money_char %><% sprintf('%.2f', 0) %> +
+ + + +<& /elements/tr-select-reason.html, + 'field' => 'reasonnum', + 'reason_class' => 'R', + #XXX reconcile both this and show_taxes wanteding to enable this + 'control_button' => "document.getElementById('credit_button')", + 'cgi' => $cgi, +&> + + + + + + +
<% mt('Additional info') |h %> + +
+ +
+ + +
+ +<% include( '/elements/xmlhttp.html', + 'url' => $p.'misc/xmlhttp-cust_bill_pkg-calculate_taxes.html', + 'subs' => [ 'calculate_taxes' ], + ) +%> + + +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" unless $curuser->access_right('Post credit'); + +#a tiny bit of false laziness w/search/cust_bill_pkg.cgi, but we're pretty +# specialized and a piece of UI, not a report +#slightly more false laziness w/httemplate/edit/elements/ApplicationCommon.html +# show_taxes & calc_total here/do_calculate_tax there + +my $conf = new FS::Conf; +my $money_char = $conf->config('money_char') || '$'; +my $date_format = $conf->config('date_format') || '%m/%d/%Y'; + +$cgi->param('custnum') =~ /^(\d+)$/ or die 'illegal custnum'; +my $custnum = $1; + +my $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $custnum }, + 'extra_sql' => ' AND '. $curuser->agentnums_sql, +}) or die 'unknown customer'; + +my @cust_bill_pkg = qsearch({ + 'select' => 'cust_bill_pkg.*', + 'table' => 'cust_bill_pkg', + 'addl_from' => 'LEFT JOIN cust_bill USING (invnum)', + 'extra_sql' => "WHERE custnum = $custnum AND pkgnum != 0", + 'order_by' => 'ORDER BY invnum ASC, billpkgnum ASC', +}); + +my @items = map { my %hash = $_->disintegrate; + map [ $_, $hash{$_} ], + keys(%hash); + } + @cust_bill_pkg; + +#omit line items which have been previously credited? would be nice + + diff --git a/httemplate/edit/cust_credit.cgi b/httemplate/edit/cust_credit.cgi index 6e8a9c989..4dba1e769 100755 --- a/httemplate/edit/cust_credit.cgi +++ b/httemplate/edit/cust_credit.cgi @@ -34,6 +34,7 @@ + % if ( $conf->exists('credits-auto-apply-disable') ) { diff --git a/httemplate/edit/process/credit-cust_bill_pkg.html b/httemplate/edit/process/credit-cust_bill_pkg.html new file mode 100644 index 000000000..d3323e6ed --- /dev/null +++ b/httemplate/edit/process/credit-cust_bill_pkg.html @@ -0,0 +1,41 @@ +%if ($error) { +% errorpage_popup($error); #XXX redirect back for correction... +%} else { +<& /elements/header-popup.html, 'Credit successful' &> + + +% } +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('Post credit'); + +my @billpkgnum_setuprecurs = + map { $_ =~ /^billpkgnum(\d+\-\w*)$/ or die 'gm#23'; $1; } + grep { $_ =~ /^billpkgnum\d+\-\w*$/ && $cgi->param($_) } $cgi->param; + +my @billpkgnums = (); +my @setuprecurs = (); +my @amounts = (); +foreach my $billpkgnum_setuprecur (@billpkgnum_setuprecurs) { + my $amount = $cgi->param("billpkgnum$billpkgnum_setuprecur"); + my( $billpkgnum, $setuprecur ) = split('-', $billpkgnum_setuprecur); + push @billpkgnums, $billpkgnum; + push @setuprecurs, $setuprecur; + push @amounts, $amount; +} + +my $error = FS::cust_credit->credit_lineitems( + 'newreasonnum' => scalar($cgi->param('newreasonnum')), + 'newreasonnum_type' => scalar($cgi->param('newreasonnumT')), + 'billpkgnums' => \@billpkgnums, + 'setuprecurs' => \@setuprecurs, + 'amounts' => \@amounts, + map { $_ => scalar($cgi->param($_)) } + #fields('cust_credit') + qw( custnum _date amount reason reasonnum addlinfo ), #pkgnum eventnum +); + + diff --git a/httemplate/edit/process/cust_credit.cgi b/httemplate/edit/process/cust_credit.cgi index 776112ac0..245f31af7 100755 --- a/httemplate/edit/process/cust_credit.cgi +++ b/httemplate/edit/process/cust_credit.cgi @@ -15,7 +15,7 @@ % % $dbh->commit or die $dbh->errstr if $oldAutoCommit; % -<% header(emt('Credit sucessful')) %> +<% header(emt('Credit successful')) %> @@ -27,7 +27,7 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Post credit'); -$cgi->param('custnum') =~ /^(\d*)$/ or die "Illegal custnum!"; +$cgi->param('custnum') =~ /^(\d+)$/ or die "Illegal custnum!"; my $custnum = $1; $cgi->param('reasonnum') =~ /^(-?\d+)$/ or die "Illegal reasonnum"; diff --git a/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html b/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html new file mode 100644 index 000000000..993504619 --- /dev/null +++ b/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html @@ -0,0 +1,123 @@ +<% to_json($return) %> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" unless $curuser->access_right('Post credit'); + +my $DEBUG = 0; + +my $conf = new FS::Conf; + +my $sub = $cgi->param('sub'); + +my $return = {}; + +if ( $sub eq 'calculate_taxes' ) { + + { + + my %arg = $cgi->param('arg'); + $return = \%arg; + warn join('', map "$_: $arg{$_}\n", keys %arg ) + if $DEBUG; + + #some false laziness w/cust_credit::credit_lineitems + + my $cust_main = qsearchs({ + 'table' => 'cust_main', + 'hashref' => { 'custnum' => $arg{custnum} }, + 'extra_sql' => ' AND '. $curuser->agentnums_sql, + }) or die 'unknown customer'; + + my @billpkgnums = split(',', $arg{billpkgnums}); + my @setuprecurs = split(',', $arg{setuprecurs}); + my @amounts = split(',', $arg{amounts}); + + my @cust_bill_pkg = (); + my $taxlisthash = {}; + while ( @billpkgnums ) { + my $billpkgnum = shift @billpkgnums; + my $setuprecur = shift @setuprecurs; + my $amount = shift @amounts; + + my $cust_bill_pkg = qsearchs({ + 'table' => 'cust_bill_pkg', + 'hashref' => { 'billpkgnum' => $billpkgnum }, + 'addl_from' => 'LEFT JOIN cust_bill USING (invnum)', + 'extra_sql' => 'AND custnum = '. $cust_main->custnum, + }) or die "unknown billpkgnum $billpkgnum"; + + #shouldn't be passed# next if $cust_bill_pkg->pkgnum == 0; + + if ( $setuprecur eq 'setup' ) { + $cust_bill_pkg->setup($amount); + $cust_bill_pkg->recur(0); + $cust_bill_pkg->unitrecur(0); + $cust_bill_pkg->type(''); + } else { + $cust_bill_pkg->recur($amount); + $cust_bill_pkg->setup(0); + $cust_bill_pkg->unitsetup(0); + } + + push @cust_bill_pkg, $cust_bill_pkg; + + my $part_pkg = $cust_bill_pkg->part_pkg; + $cust_main->_handle_taxes( $part_pkg, + $taxlisthash, + $cust_bill_pkg, + $cust_bill_pkg->cust_pkg, + $cust_bill_pkg->cust_bill->_date, + $cust_bill_pkg->cust_pkg->pkgpart, + ); + + } + + if ( @cust_bill_pkg ) { + + my $listref_or_error = + $cust_main->calculate_taxes( \@cust_bill_pkg, $taxlisthash, $cust_bill_pkg[0]->cust_bill->_date ); + + unless ( ref( $listref_or_error ) ) { + $return->{error} = $listref_or_error; + last; + } + + my @taxlines = (); + my $taxtotal = 0; + $return->{taxlines} = \@taxlines; + foreach my $taxline ( @$listref_or_error ) { + my $amount = $taxline->setup; + my $desc = $taxline->desc; + foreach my $location ( @{$taxline->cust_bill_pkg_tax_location}, @{$taxline->cust_bill_pkg_tax_rate_location} ) { + my $taxlocnum = $location->locationnum || ''; + my $taxratelocnum = $location->taxratelocationnum || ''; + $location->cust_bill_pkg_desc($taxline->desc); #ugh @ that kludge + $taxtotal += $location->amount; + push @taxlines, + #[ $location->desc, $taxline->setup, $taxlocnum, $taxratelocnum ]; + [ $location->desc, $location->amount, $taxlocnum, $taxratelocnum ]; + $amount -= $location->amount; + } + if ($amount > 0) { + $taxtotal += $amount; + push @taxlines, + [ $taxline->itemdesc. ' (default)', sprintf('%.2f', $amount), '', '' ]; + } + } + + $return->{taxlines} = \@taxlines; + $return->{taxtotal} = sprintf('%.2f', $taxtotal); + + } else { + + $return->{taxlines} = []; + $return->{taxtotal} = '0.00'; + + } + + } + +} + + diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html index 166addbf4..6630d12a5 100644 --- a/httemplate/view/cust_main/payment_history.html +++ b/httemplate/view/cust_main/payment_history.html @@ -70,6 +70,16 @@ 'actionlabel' => emt('Enter credit'), 'width' => 616, #make room for reasons #540 default &> + | + <& /elements/popup_link-cust_main.html, + 'label' => emt('Credit line items'), + #'action' => "${p}search/cust_bill_pkg.cgi?nottax=1;type=select", + 'action' => "${p}edit/credit-cust_bill_pkg.html", + 'cust_main' => $cust_main, + 'actionlabel' => emt('Credit line items'), + 'width' => 884, #763, + 'height' => 575, + &>
% }