diff options
author | Mark Wells <mark@freeside.biz> | 2013-10-12 18:44:52 -0700 |
---|---|---|
committer | Mark Wells <mark@freeside.biz> | 2013-10-12 18:44:52 -0700 |
commit | 6422e165313ee8d67790007581821217240734fb (patch) | |
tree | d2892b8c1aa62f50ee21f1ef1169bc842872cf52 /FS | |
parent | cd365522ec4e9f1b553bb1b5096c756c1fdb1d01 (diff) |
allow changing package class of one-time charges post-billing, #25342
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/AccessRight.pm | 1 | ||||
-rw-r--r-- | FS/FS/Mason.pm | 1 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 31 | ||||
-rw-r--r-- | FS/FS/cust_credit.pm | 52 | ||||
-rw-r--r-- | FS/FS/cust_credit_void.pm | 134 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 28 | ||||
-rw-r--r-- | FS/FS/cust_pkg.pm | 122 | ||||
-rw-r--r-- | FS/MANIFEST | 2 | ||||
-rw-r--r-- | FS/t/cust_credit_void.t | 5 |
9 files changed, 368 insertions, 8 deletions
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm index 2783ada..ca96eb5 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 1215ca4..398d785 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 b6f3cf3..3029ab5 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 bd92bdc..9678934 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<FS::cust_credit_refund>) for this credit. diff --git a/FS/FS/cust_credit_void.pm b/FS/FS/cust_credit_void.pm new file mode 100644 index 0000000..ac47d95 --- /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<FS::access_user>) 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<FS::cust_main>). + +=cut + +sub cust_main { + my $self = shift; + qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); +} + +=item void_access_user + +Returns the voiding employee object (see L<FS::access_user>). + +=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<FS::cust_credit>, L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index a9a4cb0..3e36c60 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<FS::cust_credit_void>) 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<FS::cust_pay>) for this customer. diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 066b987..be5ec6a 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 5dbe754..7a460da 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 0000000..6113ef5 --- /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"; |