X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_credit.pm;h=674bc1047e1d2e51d711145853bcb2289c31419d;hb=7b125e587a4d1ee0aca692e23ea7897f671855ae;hp=278e6dedd5370f8907a84888b6d11fa3321217b2;hpb=d89908c40c18df348f580852f906853184fabd3c;p=freeside.git diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm index 278e6dedd..674bc1047 100644 --- a/FS/FS/cust_credit.pm +++ b/FS/FS/cust_credit.pm @@ -1,14 +1,23 @@ 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 ); +use Date::Format; use FS::UID qw( dbh getotaker ); -use FS::Record qw( qsearch qsearchs ); +use FS::Misc qw(send_email); +use FS::Record qw( qsearch qsearchs dbdef ); 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::Record ); +$me = '[ FS::cust_credit ]'; +$DEBUG = 0; #ask FS::UID to run this stuff for us later $FS::UID::callback{'FS::cust_credit'} = sub { @@ -18,6 +27,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 @@ -45,18 +59,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 + +Customer (see L) -=item custnum - customer (see L) +=item amount -=item amount - amount of the credit +Amount of the credit -=item _date - specified as a UNIX timestamp; see L. Also see +=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 closed + +Books closed flag, empty or `Y' + +=item pkgnum -=item reason - text +Desired pkgnum when using experimental package balances. =back @@ -71,6 +113,12 @@ Creates a new credit. To add the credit to the database, see L<"insert">. =cut sub table { 'cust_credit'; } +sub cust_linked { $_[0]->cust_main_custnum; } +sub cust_unlinked_msg { + my $self = shift; + "WARNING: can't find cust_main.custnum ". $self->custnum. + ' (cust_credit.crednum '. $self->crednum. ')'; +} =item insert @@ -80,7 +128,7 @@ returns the error, otherwise returns false. =cut sub insert { - my $self = shift; + my ($self, %options) = @_; local $SIG{HUP} = 'IGNORE'; local $SIG{INT} = 'IGNORE'; @@ -96,6 +144,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; @@ -115,29 +177,104 @@ sub insert { } #eslaf + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; } =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 { - return "Can't remove credit!" + my $self = shift; + return "Can't delete closed credit" if $self->closed =~ /^Y/i; + + 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; + + foreach my $cust_credit_bill ( $self->cust_credit_bill ) { + my $error = $cust_credit_bill->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + 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; + return $error; + } + + if ( $conf->config('deletecredits') ne '' ) { + + my $cust_main = $self->cust_main; + + my $error = send_email( + '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' => [ + "This is an automatic message from your Freeside installation\n", + "informing you that the following credit has been deleted:\n", + "\n", + 'crednum: '. $self->crednum. "\n", + 'custnum: '. $self->custnum. + " (". $cust_main->last. ", ". $cust_main->first. ")\n", + 'amount: $'. sprintf("%.2f", $self->amount). "\n", + 'date: '. time2str("%a %b %e %T %Y", $self->_date). "\n", + 'reason: '. $self->reason. "\n", + ], + ); + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "can't send credit deletion notification: $error"; + } + + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ''; + } =item replace OLD_RECORD -Credits may not be modified; there would then be no record the credit was ever -posted. +You can, but probably shouldn't modify credits... =cut sub replace { - return "Can't modify credit!" + #return "Can't modify credit!" + my $self = shift; + return "Can't modify closed credit" if $self->closed =~ /^Y/i; + $self->SUPER::replace(@_); } =item check @@ -151,43 +288,34 @@ methods. sub check { my $self = shift; + $self->otaker(getotaker) unless ($self->otaker); + my $error = $self->ut_numbern('crednum') || $self->ut_number('custnum') || $self->ut_numbern('_date') || $self->ut_money('amount') - || $self->ut_textn('reason'); + || $self->ut_alphan('otaker') + || $self->ut_textn('reason') + || $self->ut_foreign_key('reasonnum', 'reason', 'reasonnum') + || $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; - return "amount must be > 0 " if $self->amount == 0; + return "amount must be > 0 " if $self->amount <= 0; + + return "amount must be greater or equal to amount applied" + if $self->unapplied < 0; return "Unknown customer" unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); $self->_date(time) unless $self->_date; - $self->otaker(getotaker); - - ''; #no error -} - -=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 } ) - #; + $self->SUPER::check; } =item cust_credit_refund @@ -198,6 +326,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 } ) ; @@ -212,20 +341,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 ); @@ -233,15 +363,255 @@ 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. + +=cut + +sub cust_main { + my $self = shift; + qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); +} + + +=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; + } + } + } + } + + $class->_upgrade_otaker(%opts); + +} + =back -=head1 VERSION +=head1 CLASS METHODS + +=over 4 -$Id: cust_credit.pm,v 1.13 2002-01-22 15:57:33 ivan Exp $ +=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 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