X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_credit.pm;h=6185fc4722fa754860086cc7d5800d4155689f9a;hp=9cc92d2e82dc6f6a1c0d7ee4a260b6b6b6542b58;hb=90393980e5f2859ee1e186fa461f48f5129e803e;hpb=08662d58e7b9a13cf841e9c89daa39b28655724e diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 9cc92d2e8..6185fc472 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -1,17 +1,29 @@ package FS::cust_credit; use strict; -use vars qw( @ISA $conf $unsuspendauto ); +use base qw( FS::otaker_Mixin FS::cust_main_Mixin FS::Record ); +use vars qw( $conf $unsuspendauto $me $DEBUG + $otaker_upgrade_kludge $ignore_empty_reasonnum + ); use Date::Format; use FS::UID qw( dbh getotaker ); use FS::Misc qw(send_email); -use FS::Record qw( qsearch qsearchs ); -use FS::cust_main_Mixin; +use FS::Record qw( qsearch qsearchs dbdef ); +use FS::CurrentUser; use FS::cust_main; +use FS::cust_pkg; use FS::cust_refund; use FS::cust_credit_bill; +use FS::part_pkg; +use FS::reason_type; +use FS::reason; +use FS::cust_event; -@ISA = qw( FS::cust_main_Mixin FS::Record ); +$me = '[ FS::cust_credit ]'; +$DEBUG = 0; + +$otaker_upgrade_kludge = 0; +$ignore_empty_reasonnum = 0; #ask FS::UID to run this stuff for us later $FS::UID::callback{'FS::cust_credit'} = sub { @@ -21,6 +33,11 @@ $FS::UID::callback{'FS::cust_credit'} = sub { }; +our %reasontype_map = ( 'referral_credit_type' => 'Referral Credit', + 'cancel_credit_type' => 'Cancellation Credit', + 'signup_credit_type' => 'Self-Service Credit', + ); + =head1 NAME FS::cust_credit - Object methods for cust_credit records @@ -48,20 +65,46 @@ FS::Record. The following fields are currently supported: =over 4 -=item crednum - primary key (assigned automatically for new credits) +=item crednum + +Primary key (assigned automatically for new credits) + +=item custnum -=item custnum - customer (see L) +Customer (see L) -=item amount - amount of the credit +=item amount -=item _date - specified as a UNIX timestamp; see L. Also see +Amount of the credit + +=item _date + +Specified as a UNIX timestamp; see L. Also see L and L for conversion functions. -=item otaker - order taker (assigned automatically, see L) +=item usernum + +Order taker (see L) + +=item reason + +Text ( deprecated ) + +=item reasonnum + +Reason (see L) + +=item addlinfo + +Text -=item reason - text +=item closed -=item closed - books closed flag, empty or `Y' +Books closed flag, empty or `Y' + +=item pkgnum + +Desired pkgnum when using experimental package balances. =back @@ -91,7 +134,7 @@ returns the error, otherwise returns false. =cut sub insert { - my $self = shift; + my ($self, %options) = @_; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -107,6 +150,20 @@ sub insert { my $cust_main = qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); my $old_balance = $cust_main->balance; + unless ($self->reasonnum) { + my $result = $self->reason( $self->getfield('reason'), + exists($options{ 'reason_type' }) + ? ('reason_type' => $options{ 'reason_type' }) + : (), + ); + unless($result) { + $dbh->rollback if $oldAutoCommit; + return "failed to set reason for $me"; #: ". $dbh->errstr; + } + } + + $self->setfield('reason', ''); + my $error = $self->SUPER::insert; if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -134,10 +191,13 @@ sub insert { =item delete -Currently unimplemented. +Unless the closed flag is set, deletes this credit and all associated +applications (see L). In most cases, you want to use +the void method instead to leave a record of the deleted credit. =cut +# very similar to FS::cust_pay::delete sub delete { my $self = shift; return "Can't delete closed credit" if $self->closed =~ /^Y/i; @@ -161,6 +221,14 @@ sub delete { } } + foreach my $cust_credit_refund ( $self->cust_credit_refund ) { + my $error = $cust_credit_refund->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + my $error = $self->SUPER::delete(@_); if ( $error ) { $dbh->rollback if $oldAutoCommit; @@ -169,10 +237,11 @@ sub delete { if ( $conf->config('deletecredits') ne '' ) { - my $cust_main = qsearchs('cust_main',{ 'custnum' => $self->custnum }); + my $cust_main = $self->cust_main; my $error = send_email( - 'from' => $conf->config('invoice_from'), #??? well as good as any + 'from' => $conf->config('invoice_from', $self->cust_main->agentnum), + #invoice_from??? well as good as any 'to' => $conf->config('deletecredits'), 'subject' => 'FREESIDE NOTIFICATION: Credit deleted', 'body' => [ @@ -201,15 +270,17 @@ sub delete { } -=item replace OLD_RECORD +=item replace [ OLD_RECORD ] + +You can, but probably shouldn't modify credits... -Credits may not be modified; there would then be no record the credit was ever -posted. +Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not +supplied, replaces this record. If there is an error, returns the error, +otherwise returns false. =cut sub replace { - #return "Can't modify credit!" my $self = shift; return "Can't modify closed credit" if $self->closed =~ /^Y/i; $self->SUPER::replace(@_); @@ -226,46 +297,39 @@ methods. sub check { my $self = shift; + $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum; + 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') ; return $error if $error; + my $method = $ignore_empty_reasonnum ? 'ut_foreign_keyn' : 'ut_foreign_key'; + $error = $self->$method('reasonnum', 'reason', 'reasonnum'); + return $error if $error; + return "amount must be > 0 " if $self->amount <= 0; + return "amount must be greater or equal to amount applied" + if $self->unapplied < 0 && ! $otaker_upgrade_kludge; + return "Unknown customer" unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); $self->_date(time) unless $self->_date; - $self->otaker(getotaker); - $self->SUPER::check; } -=item cust_refund - -Depreciated. See the cust_credit_refund method. - -#Returns all refunds (see L) for this credit. - -=cut - -sub cust_refund { - use Carp; - croak "FS::cust_credit->cust_pay depreciated; see ". - "FS::cust_credit->cust_credit_refund"; - #my $self = shift; - #sort { $a->_date <=> $b->_date } - # qsearch( 'cust_refund', { 'crednum' => $self->crednum } ) - #; -} - =item cust_credit_refund Returns all refund applications (see L) for this credit. @@ -274,6 +338,7 @@ Returns all refund applications (see L) for this credit. sub cust_credit_refund { my $self = shift; + map { $_ } #return $self->num_cust_credit_refund unless wantarray; sort { $a->_date <=> $b->_date } qsearch( 'cust_credit_refund', { 'crednum' => $self->crednum } ) ; @@ -288,20 +353,21 @@ credit. sub cust_credit_bill { my $self = shift; + map { $_ } #return $self->num_cust_credit_bill unless wantarray; sort { $a->_date <=> $b->_date } qsearch( 'cust_credit_bill', { 'crednum' => $self->crednum } ) ; } -=item credited +=item unapplied -Returns the amount of this credit that is still outstanding; which is +Returns the amount of this credit that is still unapplied/outstanding; amount minus all refund applications (see L) and applications to invoices (see L). =cut -sub credited { +sub unapplied { my $self = shift; my $amount = $self->amount; $amount -= $_->amount foreach ( $self->cust_credit_refund ); @@ -309,6 +375,18 @@ sub credited { sprintf( "%.2f", $amount ); } +=item credited + +Deprecated name for the unapplied method. + +=cut + +sub credited { + my $self = shift; + #carp "cust_credit->credited deprecated; use ->unapplied"; + $self->unapplied(@_); +} + =item cust_main Returns the customer (see L) for this credit. @@ -321,12 +399,234 @@ sub cust_main { } +=item reason + +Returns the text of the associated reason (see L) for this credit. + +=cut + +sub reason { + my ($self, $value, %options) = @_; + my $dbh = dbh; + my $reason; + my $typenum = $options{'reason_type'}; + + my $oldAutoCommit = $FS::UID::AutoCommit; # this should already be in + local $FS::UID::AutoCommit = 0; # a transaction if it matters + + if ( defined( $value ) ) { + my $hashref = { 'reason' => $value }; + $hashref->{'reason_type'} = $typenum if $typenum; + my $addl_from = "LEFT JOIN reason_type ON ( reason_type = typenum ) "; + my $extra_sql = " AND reason_type.class='R'"; + + $reason = qsearchs( { 'table' => 'reason', + 'hashref' => $hashref, + 'addl_from' => $addl_from, + 'extra_sql' => $extra_sql, + } ); + + if (!$reason && $typenum) { + $reason = new FS::reason( { 'reason_type' => $typenum, + 'reason' => $value, + 'disabled' => 'Y', + } ); + my $error = $reason->insert; + if ( $error ) { + warn "error inserting reason: $error\n"; + $reason = undef; + } + } + + $self->reasonnum($reason ? $reason->reasonnum : '') ; + warn "$me reason used in set mode with non-existant reason -- clearing" + unless $reason; + } + $reason = qsearchs( 'reason', { 'reasonnum' => $self->reasonnum } ); + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ( $reason ? $reason->reason : '' ). + ( $self->addlinfo ? ' '.$self->addlinfo : '' ); +} + +# _upgrade_data +# +# Used by FS::Upgrade to migrate to a new database. + +sub _upgrade_data { # class method + my ($class, %opts) = @_; + + warn "$me upgrading $class\n" if $DEBUG; + + if (defined dbdef->table($class->table)->column('reason')) { + + warn "$me Checking for unmigrated reasons\n" if $DEBUG; + + my @cust_credits = qsearch({ 'table' => $class->table, + 'hashref' => {}, + 'extra_sql' => 'WHERE reason IS NOT NULL', + }); + + if (scalar(grep { $_->getfield('reason') =~ /\S/ } @cust_credits)) { + warn "$me Found unmigrated reasons\n" if $DEBUG; + my $hashref = { 'class' => 'R', 'type' => 'Legacy' }; + my $reason_type = qsearchs( 'reason_type', $hashref ); + unless ($reason_type) { + $reason_type = new FS::reason_type( $hashref ); + my $error = $reason_type->insert(); + die "$class had error inserting FS::reason_type into database: $error\n" + if $error; + } + + $hashref = { 'reason_type' => $reason_type->typenum, + 'reason' => '(none)' + }; + my $noreason = qsearchs( 'reason', $hashref ); + unless ($noreason) { + $hashref->{'disabled'} = 'Y'; + $noreason = new FS::reason( $hashref ); + my $error = $noreason->insert(); + die "can't insert legacy reason '(none)' into database: $error\n" + if $error; + } + + foreach my $cust_credit ( @cust_credits ) { + my $reason = $cust_credit->getfield('reason'); + warn "Contemplating reason $reason\n" if $DEBUG > 1; + if ($reason =~ /\S/) { + $cust_credit->reason($reason, 'reason_type' => $reason_type->typenum) + or die "can't insert legacy reason $reason into database\n"; + }else{ + $cust_credit->reasonnum($noreason->reasonnum); + } + + $cust_credit->setfield('reason', ''); + my $error = $cust_credit->replace; + + warn "*** WARNING: error replacing reason in $class ". + $cust_credit->crednum. ": $error ***\n" + if $error; + } + } + + warn "$me Ensuring existance of auto reasons\n" if $DEBUG; + + foreach ( keys %reasontype_map ) { + unless ($conf->config($_)) { # hmmmm +# warn "$me Found $_ reason type lacking\n" if $DEBUG; +# my $hashref = { 'class' => 'R', 'type' => $reasontype_map{$_} }; + my $hashref = { 'class' => 'R', 'type' => 'Legacy' }; + my $reason_type = qsearchs( 'reason_type', $hashref ); + unless ($reason_type) { + $reason_type = new FS::reason_type( $hashref ); + my $error = $reason_type->insert(); + die "$class had error inserting FS::reason_type into database: $error\n" + if $error; + } + $conf->set($_, $reason_type->typenum); + } + } + + warn "$me Ensuring commission packages have a reason type\n" if $DEBUG; + + my $hashref = { 'class' => 'R', 'type' => 'Legacy' }; + my $reason_type = qsearchs( 'reason_type', $hashref ); + unless ($reason_type) { + $reason_type = new FS::reason_type( $hashref ); + my $error = $reason_type->insert(); + die "$class had error inserting FS::reason_type into database: $error\n" + if $error; + } + + my @plans = qw( flat_comission flat_comission_cust flat_comission_pkg ); + foreach my $plan ( @plans ) { + foreach my $pkg ( qsearch('part_pkg', { 'plan' => $plan } ) ) { + unless ($pkg->option('reason_type', 1) ) { + my $plandata = $pkg->plandata. + "reason_type=". $reason_type->typenum. "\n"; + $pkg->plandata($plandata); + my $error = + $pkg->replace( undef, + 'pkg_svc' => { map { $_->svcpart => $_->quantity } + $pkg->pkg_svc + }, + 'primary_svc' => $pkg->svcpart, + ); + die "failed setting reason_type option: $error" + if $error; + } + } + } + } + + local($otaker_upgrade_kludge) = 1; + local($ignore_empty_reasonnum) = 1; + $class->_upgrade_otaker(%opts); + +} + +=back + +=head1 CLASS METHODS + +=over 4 + +=item unapplied_sql + +Returns an SQL fragment to retreive the unapplied amount. + +=cut + +sub unapplied_sql { + my ($class, $start, $end) = @_; + + my $bill_start = $start ? "AND cust_credit_bill._date <= $start" : ''; + my $bill_end = $end ? "AND cust_credit_bill._date > $end" : ''; + my $refund_start = $start ? "AND cust_credit_refund._date <= $start" : ''; + my $refund_end = $end ? "AND cust_credit_refund._date > $end" : ''; + + "amount + - COALESCE( + ( SELECT SUM(amount) FROM cust_credit_refund + WHERE cust_credit.crednum = cust_credit_refund.crednum + $refund_start $refund_end ) + ,0 + ) + - COALESCE( + ( SELECT SUM(amount) FROM cust_credit_bill + WHERE cust_credit.crednum = cust_credit_bill.crednum + $bill_start $bill_end ) + ,0 + ) + "; + +} + +=item credited_sql + +Deprecated name for the unapplied_sql method. + +=cut + +sub credited_sql { + #my $class = shift; + + #carp "cust_credit->credited_sql deprecated; use ->unapplied_sql"; + + #$class->unapplied_sql(@_); + unapplied_sql(); +} + =back =head1 BUGS The delete method. The replace method. +B and B are now called B and +B. The old method names should start to give warnings. + =head1 SEE ALSO L, L, L,