From 6422e165313ee8d67790007581821217240734fb Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sat, 12 Oct 2013 18:44:52 -0700 Subject: [PATCH] allow changing package class of one-time charges post-billing, #25342 --- FS/FS/AccessRight.pm | 1 + FS/FS/Mason.pm | 1 + FS/FS/Schema.pm | 31 +++++ FS/FS/cust_credit.pm | 52 +++++++- FS/FS/cust_credit_void.pm | 134 +++++++++++++++++++++ FS/FS/cust_main.pm | 28 +++-- FS/FS/cust_pkg.pm | 122 +++++++++++++++++++ FS/MANIFEST | 2 + FS/t/cust_credit_void.t | 5 + httemplate/edit/process/quick-charge.cgi | 108 +++++++++++------ httemplate/edit/quick-charge.html | 114 +++++++++++++++--- httemplate/view/cust_main/packages/package.html | 28 ++++- httemplate/view/cust_main/payment_history.html | 14 +++ .../cust_main/payment_history/voided_credit.html | 25 ++++ 14 files changed, 598 insertions(+), 67 deletions(-) create mode 100644 FS/FS/cust_credit_void.pm create mode 100644 FS/t/cust_credit_void.t create mode 100644 httemplate/view/cust_main/payment_history/voided_credit.html diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 2783adac2..ca96eb52f 100644 --- a/FS/FS/AccessRight.pm +++ b/FS/FS/AccessRight.pm @@ -130,6 +130,7 @@ tie my %rights, 'Tie::IxHash', 'View customer packages', #NEW 'Order customer package', 'One-time charge', + 'Modify one-time charge', 'Change customer package', 'Detach customer package', 'Bulk change customer packages', diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 1215ca414..398d78561 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -356,6 +356,7 @@ if ( -e $addl_handler_use_file ) { use FS::invoice_mode; use FS::invoice_conf; use FS::cable_provider; + use FS::cust_credit_void; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index b6f3cf3ee..3029ab579 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1018,6 +1018,37 @@ sub tables_hashref { ], }, + 'cust_credit_void' => { + 'columns' => [ + 'crednum', 'serial', '', '', '', '', + 'custnum', 'int', '', '', '', '', + '_date', @date_type, '', '', + 'amount',@money_type, '', '', + 'currency', 'char', 'NULL', 3, '', '', + 'otaker', 'varchar', 'NULL', 32, '', '', + 'usernum', 'int', 'NULL', '', '', '', + 'reason', 'text', 'NULL', '', '', '', + 'reasonnum', 'int', 'NULL', '', '', '', + 'addlinfo', 'text', 'NULL', '', '', '', + 'closed', 'char', 'NULL', 1, '', '', + 'pkgnum', 'int', 'NULL', '', '','', + 'eventnum', 'int', 'NULL', '', '','', + 'commission_agentnum', 'int', 'NULL', '', '', '', + 'commission_salesnum', 'int', 'NULL', '', '', '', + 'commission_pkgnum', 'int', 'NULL', '', '', '', + #void fields + 'void_date', @date_type, '', '', + 'void_reason', 'varchar', 'NULL', $char_d, '', '', + 'void_usernum', 'int', 'NULL', '', '', '', + ], + 'primary_key' => 'crednum', + 'unique' => [], + 'index' => [ ['custnum'], ['_date'], ['usernum'], ['eventnum'], + [ 'commission_salesnum' ], + ], + }, + + 'cust_credit_bill' => { 'columns' => [ 'creditbillnum', 'serial', '', '', '', '', diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index bd92bdc75..96789343a 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -21,6 +21,7 @@ use FS::reason; use FS::cust_event; use FS::agent; use FS::sales; +use FS::cust_credit_void; $me = '[ FS::cust_credit ]'; $DEBUG = 0; @@ -203,6 +204,8 @@ the void method instead to leave a record of the deleted credit. # very similar to FS::cust_pay::delete sub delete { my $self = shift; + my %opt = @_; + return "Can't delete closed credit" if $self->closed =~ /^Y/i; local $SIG{HUP} = 'IGNORE'; @@ -238,7 +241,7 @@ sub delete { return $error; } - if ( $conf->config('deletecredits') ne '' ) { + if ( !$opt{void} and $conf->config('deletecredits') ne '' ) { my $cust_main = $self->cust_main; @@ -336,6 +339,53 @@ sub check { $self->SUPER::check; } +=item void [ REASON ] + +Voids this credit: deletes the credit and all associated applications and +adds a record of the voided credit to the cust_credit_void table. + +=cut + +# yes, false laziness with cust_pay and cust_bill +# but frankly I don't have time to fix it now + +sub void { + my $self = shift; + my $reason = shift; + + 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_credit_void = new FS::cust_credit_void ( { + map { $_ => $self->get($_) } $self->fields + } ); + $cust_credit_void->set('void_reason', $reason); + my $error = $cust_credit_void->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $error = $self->delete(void => 1); # suppress deletecredits warning + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; + +} + =item cust_credit_refund Returns all refund applications (see L) for this credit. diff --git a/FS/FS/cust_credit_void.pm b/FS/FS/cust_credit_void.pm new file mode 100644 index 000000000..ac47d954a --- /dev/null +++ b/FS/FS/cust_credit_void.pm @@ -0,0 +1,134 @@ +package FS::cust_credit_void; + +use strict; +use base qw( FS::otaker_Mixin FS::cust_main_Mixin FS::Record ); +use FS::Record qw(qsearch qsearchs dbh fields); +use FS::CurrentUser; +use FS::access_user; +use FS::cust_credit; + +=head1 NAME + +FS::cust_credit_void - Object methods for cust_credit_void objects + +=head1 SYNOPSIS + + use FS::cust_credit_void; + + $record = new FS::cust_credit_void \%hash; + $record = new FS::cust_credit_void { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_credit_void object represents a voided credit. All fields in +FS::cust_credit are present, as well as: + +=over 4 + +=item void_date - the date (unix timestamp) that the credit was voided + +=item void_reason - the reason (a freeform string) + +=item void_usernum - the user (L) who voided it + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new voided credit record. + +=cut + +sub table { 'cust_credit_void'; } + +=item insert + +Adds this voided credit to the database. + +=item check + +Checks all fields to make sure this is a valid voided credit. If there is an +error, returns the error, otherwise returns false. Called by the insert +method. + +=cut + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('crednum') + || $self->ut_number('custnum') + || $self->ut_numbern('_date') + || $self->ut_money('amount') + || $self->ut_alphan('otaker') + || $self->ut_textn('reason') + || $self->ut_textn('addlinfo') + || $self->ut_enum('closed', [ '', 'Y' ]) + || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum') + || $self->ut_foreign_keyn('eventnum', 'cust_event', 'eventnum') + || $self->ut_foreign_keyn('commission_agentnum', 'agent', 'agentnum') + || $self->ut_foreign_keyn('commission_salesnum', 'sales', 'salesnum') + || $self->ut_foreign_keyn('commission_pkgnum', 'cust_pkg', 'pkgnum') + || $self->ut_numbern('void_date') + || $self->ut_textn('void_reason') + || $self->ut_foreign_keyn('void_usernum', 'access_user', 'usernum') + ; + return $error if $error; + + $self->void_date(time) unless $self->void_date; + + $self->void_usernum($FS::CurrentUser::CurrentUser->usernum) + unless $self->void_usernum; + + $self->SUPER::check; +} + +=item cust_main + +Returns the parent customer object (see L). + +=cut + +sub cust_main { + my $self = shift; + qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); +} + +=item void_access_user + +Returns the voiding employee object (see L). + +=cut + +sub void_access_user { + my $self = shift; + qsearchs('access_user', { 'usernum' => $self->void_usernum } ); +} + +=back + +=head1 BUGS + +Doesn't yet support unvoid. + +=head1 SEE ALSO + +L, L, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index a9a4cb0ef..3e36c6049 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -1243,13 +1243,14 @@ sub merge { } tie my %financial_tables, 'Tie::IxHash', - 'cust_bill' => 'invoices', - 'cust_bill_void' => 'voided invoices', - 'cust_statement' => 'statements', - 'cust_credit' => 'credits', - 'cust_pay' => 'payments', - 'cust_pay_void' => 'voided payments', - 'cust_refund' => 'refunds', + 'cust_bill' => 'invoices', + 'cust_bill_void' => 'voided invoices', + 'cust_statement' => 'statements', + 'cust_credit' => 'credits', + 'cust_credit_void' => 'voided credits', + 'cust_pay' => 'payments', + 'cust_pay_void' => 'voided payments', + 'cust_refund' => 'refunds', ; foreach my $table ( keys %financial_tables ) { @@ -3732,6 +3733,19 @@ sub cust_credit_pkgnum { ); } +=item cust_credit_void + +Returns all voided credits (see L) for this customer. + +=cut + +sub cust_credit_void { + my $self = shift; + map { $_ } + sort { $a->_date <=> $b->_date } + qsearch( 'cust_credit_void', { 'custnum' => $self->custnum } ) +} + =item cust_pay Returns all the payments (see L) for this customer. diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 066b98755..be5ec6a95 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -35,6 +35,8 @@ use FS::cust_pkg_discount; use FS::discount; use FS::UI::Web; use FS::sales; +# for modify_charge +use FS::cust_credit; # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend, # setup } @@ -2256,8 +2258,128 @@ sub set_salesnum { $self = $self->replace_old; # just to make sure $self->salesnum(shift); $self->replace; + # XXX this should probably reassign any credit that's already been given } +=item modify_charge OPTIONS + +Change the properties of a one-time charge. Currently the only properties +that can be changed this way are those that have no impact on billing +calculations: +- pkg: the package description +- classnum: the package class +- additional: arrayref of additional invoice details to add to this package + +If you pass 'adjust_commission' => 1, and the classnum changes, and there are +commission credits linked to this charge, they will be recalculated. + +=cut + +sub modify_charge { + my $self = shift; + my %opt = @_; + my $part_pkg = $self->part_pkg; + my $pkgnum = $self->pkgnum; + + my $dbh = dbh; + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + + return "Can't use modify_charge except on one-time charges" + unless $part_pkg->freq eq '0'; + + if ( length($opt{'pkg'}) and $part_pkg->pkg ne $opt{'pkg'} ) { + $part_pkg->set('pkg', $opt{'pkg'}); + } + + my %pkg_opt = $part_pkg->options; + if ( ref($opt{'additional'}) ) { + delete $pkg_opt{$_} foreach grep /^additional/, keys %pkg_opt; + my $i; + for ( $i = 0; exists($opt{'additional'}->[$i]); $i++ ) { + $pkg_opt{ "additional_info$i" } = $opt{'additional'}->[$i]; + } + $pkg_opt{'additional_count'} = $i if $i > 0; + } + + my $old_classnum; + if ( exists($opt{'classnum'}) and $part_pkg->classnum ne $opt{'classnum'} ) { + # remember it + $old_classnum = $part_pkg->classnum; + $part_pkg->set('classnum', $opt{'classnum'}); + } + + my $error = $part_pkg->replace( options => \%pkg_opt ); + return $error if $error; + + if (defined $old_classnum) { + # fix invoice grouping records + my $old_catname = $old_classnum + ? FS::pkg_class->by_key($old_classnum)->categoryname + : ''; + my $new_catname = $opt{'classnum'} + ? $part_pkg->pkg_class->categoryname + : ''; + if ( $old_catname ne $new_catname ) { + foreach my $cust_bill_pkg ($self->cust_bill_pkg) { + # (there should only be one...) + my @display = qsearch( 'cust_bill_pkg_display', { + 'billpkgnum' => $cust_bill_pkg->billpkgnum, + 'section' => $old_catname, + }); + foreach (@display) { + $_->set('section', $new_catname); + $error = $_->replace; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + } # foreach $cust_bill_pkg + } + + if ( $opt{'adjust_commission'} ) { + # fix commission credits...tricky. + foreach my $cust_event ($self->cust_event) { + my $part_event = $cust_event->part_event; + foreach my $table (qw(sales agent)) { + my $class = + "FS::part_event::Action::Mixin::credit_${table}_pkg_class"; + my $credit = qsearchs('cust_credit', { + 'eventnum' => $cust_event->eventnum, + }); + if ( $part_event->isa($class) ) { + # Yes, this results in current commission rates being applied + # retroactively to a one-time charge. For accounting purposes + # there ought to be some kind of time limit on doing this. + my $amount = $part_event->_calc_credit($self); + if ( $credit and $credit->amount ne $amount ) { + # Void the old credit. + $error = $credit->void('Package class changed'); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "$error (adjusting commission credit)"; + } + } + # redo the event action to recreate the credit. + local $@ = ''; + eval { $part_event->do_action( $self, $cust_event ) }; + if ( $@ ) { + $dbh->rollback if $oldAutoCommit; + return $@; + } + } # if $part_event->isa($class) + } # foreach $table + } # foreach $cust_event + } # if $opt{'adjust_commission'} + } # if defined $old_classnum + + $dbh->commit if $oldAutoCommit; + ''; +} + + + use Storable 'thaw'; use MIME::Base64; use Data::Dumper; diff --git a/FS/MANIFEST b/FS/MANIFEST index 5dbe754c1..7a460dac3 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -726,3 +726,5 @@ FS/invoice_conf.pm t/invoice_conf.t FS/cable_provider.pm t/cable_provider.t +FS/cust_credit_void.pm +t/cust_credit_void.t diff --git a/FS/t/cust_credit_void.t b/FS/t/cust_credit_void.t new file mode 100644 index 000000000..6113ef5b8 --- /dev/null +++ b/FS/t/cust_credit_void.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_credit_void; +$loaded=1; +print "ok 1\n"; diff --git a/httemplate/edit/process/quick-charge.cgi b/httemplate/edit/process/quick-charge.cgi index 38f06e1e9..db41fb238 100644 --- a/httemplate/edit/process/quick-charge.cgi +++ b/httemplate/edit/process/quick-charge.cgi @@ -10,8 +10,9 @@ % } <%init> +my $curuser = $FS::CurrentUser::CurrentUser; die "access denied" - unless $FS::CurrentUser::CurrentUser->access_right('One-time charge'); + unless $curuser->access_right('One-time charge'); my $error = ''; my $conf = new FS::conf; @@ -27,49 +28,76 @@ $param->{"custnum"} =~ /^(\d+)$/ or $error .= "Illegal customer number " . $param->{"custnum"} . " "; my $custnum = $1; -$param->{"amount"} =~ /^\s*(\d*(?:\.?\d{1,2}))\s*$/ - or $error .= "Illegal amount " . $param->{"amount"} . " "; -my $amount = $1; +my $cust_main = FS::cust_main->by_key($custnum) + or die "custnum $custnum not found"; -my $quantity = 1; -if ( $cgi->param('quantity') =~ /^\s*(\d+)\s*$/ ) { - $quantity = $1; -} +exists($curuser->agentnums_href->{$cust_main->agentnum}) + or die "access denied"; -$param->{'tax_override'} =~ /^\s*([,\d]*)\s*$/ - or $error .= "Illegal tax override " . $param->{"tax_override"} . " "; -my $override = $1; +if ( $param->{'pkgnum'} =~ /^(\d+)$/ ) { + my $pkgnum = $1; + die "access denied" + unless $curuser->access_right('Modify one-time charge'); -if ( $param->{'taxclass'} eq '(select)' ) { - $error .= "Must select a tax class. " - unless ($conf->exists('enable_taxproducts') && - ( $override || $param->{taxproductnum} ) - ); - $cgi->param('taxclass', ''); -} + my $cust_pkg = FS::cust_pkg->by_key($1) + or die "pkgnum $pkgnum not found"; + + my $part_pkg = $cust_pkg->part_pkg; + die "pkgnum $pkgnum is not a one-time charge" unless $part_pkg->freq eq '0'; + + $error = $cust_pkg->modify_charge( + 'pkg' => scalar($cgi->param('pkg')), + 'classnum' => scalar($cgi->param('classnum')), + 'additional' => \@description, + 'adjust_commission' => ($cgi->param('adjust_commission') ? 1 : 0), + ); + +} else { + # the usual case: new one-time charge + $param->{"amount"} =~ /^\s*(\d*(?:\.?\d{1,2}))\s*$/ + or $error .= "Illegal amount " . $param->{"amount"} . " "; + my $amount = $1; + + my $quantity = 1; + if ( $cgi->param('quantity') =~ /^\s*(\d+)\s*$/ ) { + $quantity = $1; + } + + $param->{'tax_override'} =~ /^\s*([,\d]*)\s*$/ + or $error .= "Illegal tax override " . $param->{"tax_override"} . " "; + my $override = $1; + + if ( $param->{'taxclass'} eq '(select)' ) { + $error .= "Must select a tax class. " + unless ($conf->exists('enable_taxproducts') && + ( $override || $param->{taxproductnum} ) + ); + $cgi->param('taxclass', ''); + } + + unless ( $error ) { + my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) + or $error .= "Unknown customer number $custnum. "; -unless ( $error ) { - my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) - or $error .= "Unknown customer number $custnum. "; - - $error ||= $cust_main->charge( { - 'amount' => $amount, - 'quantity' => $quantity, - 'bill_now' => scalar($cgi->param('bill_now')), - 'invoice_terms' => scalar($cgi->param('invoice_terms')), - 'start_date' => ( scalar($cgi->param('start_date')) - ? parse_datetime($cgi->param('start_date')) - : '' - ), - 'no_auto' => scalar($cgi->param('no_auto')), - 'pkg' => scalar($cgi->param('pkg')), - 'setuptax' => scalar($cgi->param('setuptax')), - 'taxclass' => scalar($cgi->param('taxclass')), - 'taxproductnum' => scalar($cgi->param('taxproductnum')), - 'tax_override' => $override, - 'classnum' => scalar($cgi->param('classnum')), - 'additional' => \@description, - } ); + $error ||= $cust_main->charge( { + 'amount' => $amount, + 'quantity' => $quantity, + 'bill_now' => scalar($cgi->param('bill_now')), + 'invoice_terms' => scalar($cgi->param('invoice_terms')), + 'start_date' => ( scalar($cgi->param('start_date')) + ? parse_datetime($cgi->param('start_date')) + : '' + ), + 'no_auto' => scalar($cgi->param('no_auto')), + 'pkg' => scalar($cgi->param('pkg')), + 'setuptax' => scalar($cgi->param('setuptax')), + 'taxclass' => scalar($cgi->param('taxclass')), + 'taxproductnum' => scalar($cgi->param('taxproductnum')), + 'tax_override' => $override, + 'classnum' => scalar($cgi->param('classnum')), + 'additional' => \@description, + } ); + } } diff --git a/httemplate/edit/quick-charge.html b/httemplate/edit/quick-charge.html index 466091dfa..666ba82de 100644 --- a/httemplate/edit/quick-charge.html +++ b/httemplate/edit/quick-charge.html @@ -104,6 +104,49 @@ function bill_now_changed (what) { +% if ( $cust_pkg ) { + + +<& /elements/tr-fixed.html, + label => 'Amount', + field => 'amount', + value => $money_char . sprintf('%.2f',$part_pkg->option('setup_fee')), +&> + +% if ( $conf->exists('invoice-unitprice') ) { +<& /elements/tr-fixed.html, + label => 'Quantity', + field => 'quantity', + value => $cust_pkg->quantity +&> +% } + +<& /elements/tr-select-pkg_class.html, 'curr_value' => $classnum &> + +% # crudely estimate whether any agent commission credits might exist +% my @events = grep { $_->part_event->action =~ /credit/ } +% $cust_pkg->cust_event; +% if ( scalar @events ) { + + + +% } + +% #display the future or past charge date, but don't allow changes +% # XXX we probably _could_ let as-yet unbilled charges be rescheduled, but +% # there's no compelling need yet +% if ( $cust_pkg->setup or $cust_pkg->start_date ) { +% my $label = $cust_pkg->setup ? emt('Billed on') : emt('Will be billed'); +% my $field = $cust_pkg->setup ? 'setup' : 'start_date'; + <& /elements/tr-fixed-date.html, + label => $label, + value => $cust_pkg->get($field) + &> +% } # else we don't show anything here +% } else { # new one-time charge + -% if ( $conf->exists('invoice-unitprice') ) { +% if ( $conf->exists('invoice-unitprice') ) { -% } +% } -<& /elements/tr-select-pkg_class.html, 'curr_value' => $cgi->param('classnum') &> +<& /elements/tr-select-pkg_class.html, 'curr_value' => $classnum &> @@ -206,6 +249,8 @@ function bill_now_changed (what) { <& /elements/tr-select-taxoverride.html, 'onclick' => 'parent.taxoverridemagic(this);', 'curr_value' => $cgi->param('tax_override') &> +% } # if !$cust_pkg + % my $row = 0; -% if ( $cgi->param('error') || $cgi->param('magic') ) { -% my $param = $cgi->Vars; -% -% for ( $row = 0; exists($param->{"description$row"}); $row++ ) { - +% foreach (@description) { -% } +% $row++; % }
+<% emt('Adjust commission credits if necessary') %> +
<% mt('Amount') |h %> @@ -117,7 +160,7 @@ function bill_now_changed (what) {
<% mt('Quantity') |h %> @@ -128,9 +171,9 @@ function bill_now_changed (what) { onKeyPress = "return enable_quick_charge(event)">
<% mt('Invoice now') |h %>
<% mt('Description') |h %> @@ -226,11 +271,7 @@ function bill_now_changed (what) {
@@ -238,21 +279,25 @@ function bill_now_changed (what) { NAME = "description<% $row %>" SIZE = "60" MAXLENGTH = "65" - VALUE = "<% $param->{"description$row"} |h %>" + VALUE = "<% $_ |h %>" rownum = "<% $row %>" onKeyPress = "return enable_quick_charge(event)" onKeyUp = "return possiblyAddRow(event)" >

-param('error') ? '' :' DISABLED' %>> +% my $label = $cust_pkg +% ? emt('Modify one-time charge') +% : emt('Add one-time charge'); +param('error') || $cust_pkg) ? '' :' DISABLED' %>> @@ -329,9 +374,25 @@ my $conf = new FS::Conf; my $date_format = $conf->config('date_format') || '%m/%d/%Y'; my $money_char = $conf->config('money_char') || '$'; -$cgi->param('custnum') =~ /^(\d+)$/ or die 'illegal custnum'; -my $custnum = $1; -my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ); #XXX agent-virt +my ($cust_main, $cust_pkg); +if ( $cgi->param('change_pkgnum') ) { + # change an existing one-time charge + die "access denied" + unless $curuser->access_right('Modify one-time charge'); + + $cgi->param('change_pkgnum') =~ /^(\d+)$/ or die "illegal pkgnum"; + $cust_pkg = FS::cust_pkg->by_key($1) or die "pkgnum $1 not found"; + $cust_main = $cust_pkg->cust_main; +} else { + $cgi->param('custnum') =~ /^(\d+)$/ or die 'illegal custnum'; + $cust_main = FS::cust_main->by_key($1) or die "custnum $1 not found"; +} + +my $custnum = $cust_main->custnum; +# agent-virt +if (!exists($curuser->agentnums_href->{$cust_main->agentnum})) { + die "custnum $custnum not found"; +} my $format = "%m/%d/%Y %T %z (%Z)"; #false laziness w/REAL_cust_pkg.cgi? my $start_date = $cust_main->next_bill_date; @@ -360,4 +421,29 @@ if ( $cust_main->invoice_terms ) { ); } +my @description; +my %param = $cgi->Vars; +for (my $i = 0; exists($param{"description$i"}); $i++) { + push @description, $param{"description$i"}; +} + +my $classnum; +if ( $cgi->param('classnum') =~ /^(\d+)$/ ) { + $classnum = $1; +} + +my $part_pkg; + +if ( $cust_pkg ) { # set defaults + $part_pkg = $cust_pkg->part_pkg; + $pkg ||= $part_pkg->pkg; + $classnum ||= $part_pkg->classnum; + if (!@description) { + for (my $i = 0; $i < ($part_pkg->option('additional_count',1) || 0); $i++) + { + push @description, $part_pkg->option("additional_info$i",1); + } + } +} + diff --git a/httemplate/view/cust_main/packages/package.html b/httemplate/view/cust_main/packages/package.html index 1c8db15f4..e97c141d2 100644 --- a/httemplate/view/cust_main/packages/package.html +++ b/httemplate/view/cust_main/packages/package.html @@ -21,17 +21,22 @@ -% unless ( $cust_pkg->get('cancel') || $opt{no_links} ) { +% if ( $part_pkg->freq eq '0' and !$opt{no_links} ) { +% # One-time charge. Nothing you can do with this, unless: +% if ( $curuser->access_right('Modify one-time charge') ) { + ( <%onetime_change_link($cust_pkg)%> ) +
+% } +% +% } elsif ( !$cust_pkg->get('cancel') and !$opt{no_links} ) { % % if ( $change_from ) { % # This is the target package for a future change. % # Nothing you can do with it besides modify/cancel the % # future change, and that's on the current package. -% } elsif ( $supplemental or $part_pkg->freq eq '0' ) { +% } elsif ( $supplemental ) { % # Supplemental packages can't be changed independently. -% # One-time charges don't need to be changed. -% # For both of those, we only show "Add comments", -% # and "Add invoice details". +% # Show only "Add comments" and "Add invoice details". % } else { % # the usual case: links to change package definition, % # discount, and customization @@ -320,6 +325,19 @@ sub pkg_change_link { ); } +sub onetime_change_link { + my $cust_pkg = shift; + my $pkgnum = $cust_pkg->pkgnum; + include( '/elements/popup_link-cust_pkg.html', + 'action' => $p. "edit/quick-charge.html?change_pkgnum=$pkgnum", + 'label' => emt('Modify one-time charge'), + 'actionlabel' => emt('Modify'), + 'cust_pkg' => $cust_pkg, + 'width' => 690, + 'height' => 380, + ); +} + sub pkg_change_location_link { my $cust_pkg = shift; my $pkgpart = $cust_pkg->pkgpart; diff --git a/httemplate/view/cust_main/payment_history.html b/httemplate/view/cust_main/payment_history.html index c7bf3748c..73082ce96 100644 --- a/httemplate/view/cust_main/payment_history.html +++ b/httemplate/view/cust_main/payment_history.html @@ -270,6 +270,11 @@ % ? sprintf("- $money_char\%.2f", $item->{'credit'}) % : ''; % +% $credit ||= sprintf( "- $money_char\%.2f", +% $item->{'void_credit'} +% ) +% if exists($item->{'void_credit'}); +% % my $refund = exists($item->{'refund'}) % ? sprintf("$money_char\%.2f", $item->{'refund'}) % : ''; @@ -469,6 +474,15 @@ foreach my $cust_pay_void ($cust_main->cust_pay_void) { } +#voided credits +foreach my $cust_credit_void ($cust_main->cust_credit_void) { + push @history, { + 'date' => $cust_credit_void->_date, + 'desc' => include('payment_history/voided_credit.html', $cust_credit_void, %opt ), + 'void_credit' => $cust_credit_void->amount, + }; +} + #declined payments foreach my $cust_pay_pending ($cust_main->cust_pay_pending_attempt) { push @history, { diff --git a/httemplate/view/cust_main/payment_history/voided_credit.html b/httemplate/view/cust_main/payment_history/voided_credit.html new file mode 100644 index 000000000..0723a7282 --- /dev/null +++ b/httemplate/view/cust_main/payment_history/voided_credit.html @@ -0,0 +1,25 @@ +<% emt("Credit by [_1]", $cust_credit_void->otaker, $reason ) %>\ +<% $reason |h %> + +<% emt("voided [_1]", time2str($date_format, $cust_credit_void->void_date) )%> +% my $void_user = $cust_credit_void->void_access_user; +% if ($void_user) { +<% emt('by [_1]', $void_user->username) %> +% } +<% $void_reason |h %> + +<%init> + +my( $cust_credit_void, %opt ) = @_; + +my $date_format = $opt{'date_format'} || '%m/%d/%Y'; + +my $curuser = $FS::CurrentUser::CurrentUser; + +#my $unvoid = ''; # not yet available +my $reason = $cust_credit_void->reason; +$reason = " ($reason)" if $reason; + +my $void_reason = $cust_credit_void->void_reason; +$void_reason = " ($void_reason)" if $void_reason; + -- 2.11.0