use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
use FS::Misc qw( generate_email send_email generate_ps do_print );
use FS::Msgcat qw(gettext);
+use FS::CurrentUser;
use FS::payby;
use FS::cust_pkg;
use FS::cust_svc;
}
-=item delete NEW_CUSTNUM
+=item delete [ OPTION => VALUE ... ]
This deletes the customer. If there is an error, returns the error, otherwise
returns false.
customer's packages (see L</cancel>).
If the customer has any uncancelled packages, you need to pass a new (valid)
-customer number for those packages to be transferred to. Cancelled packages
-will be deleted. Did I mention that this is NOT what you want when a customer
-cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
+customer number for those packages to be transferred to, as the "new_customer"
+option. Cancelled packages will be deleted. Did I mention that this is NOT
+what you want when a customer cancels service and that you really should be
+looking at L<FS::cust_pkg/cancel>?
You can't delete a customer with invoices (see L<FS::cust_bill>),
-or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
-refunds (see L<FS::cust_refund>).
+statements (see L<FS::cust_statement>), credits (see L<FS::cust_credit>),
+payments (see L<FS::cust_pay>) or refunds (see L<FS::cust_refund>), unless you
+set the "delete_financials" option to a true value.
=cut
sub delete {
- my $self = shift;
+ my( $self, %opt ) = @_;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- if ( $self->cust_bill ) {
- $dbh->rollback if $oldAutoCommit;
- return "Can't delete a customer with invoices";
+ if ( qsearch('agent', { 'agent_custnum' => $self->custnum } ) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Can't delete a master agent customer";
}
- if ( $self->cust_credit ) {
- $dbh->rollback if $oldAutoCommit;
- return "Can't delete a customer with credits";
- }
- if ( $self->cust_pay ) {
- $dbh->rollback if $oldAutoCommit;
- return "Can't delete a customer with payments";
+
+ #use FS::access_user
+ if ( qsearch('access_user', { 'user_custnum' => $self->custnum } ) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Can't delete a master employee customer";
}
- if ( $self->cust_refund ) {
- $dbh->rollback if $oldAutoCommit;
- return "Can't delete a customer with refunds";
+
+ tie my %financial_tables, 'Tie::IxHash',
+ 'cust_bill' => 'invoices',
+ 'cust_statement' => 'statements',
+ 'cust_credit' => 'credits',
+ 'cust_pay' => 'payments',
+ 'cust_refund' => 'refunds',
+ ;
+
+ foreach my $table ( keys %financial_tables ) {
+
+ my @records = $self->$table();
+
+ if ( @records && ! $opt{'delete_financials'} ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Can't delete a customer with ". $financial_tables{$table};
+ }
+
+ foreach my $record ( @records ) {
+ my $error = $record->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error deleting ". $financial_tables{$table}. ": $error\n";
+ }
+ }
+
}
my @cust_pkg = $self->ncancelled_pkgs;
if ( @cust_pkg ) {
- my $new_custnum = shift;
+ my $new_custnum = $opt{'new_custnum'};
unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
$dbh->rollback if $oldAutoCommit;
return "Invalid new customer number: $new_custnum";
}
}
- foreach my $table (qw( cust_main_invoice cust_main_exemption cust_tag )) {
- foreach my $record ( qsearch( 'table', { 'custnum' => $self->custnum } ) ) {
+ #cust_tax_adjustment in financials?
+ #cust_pay_pending? ouch
+ #cust_recon?
+ foreach my $table (qw(
+ cust_main_invoice cust_main_exemption cust_tag cust_attachment contact
+ cust_location cust_main_note cust_tax_adjustment
+ cust_pay_void cust_pay_batch queue cust_tax_exempt
+ )) {
+ foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
my $error = $record->delete;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
}
}
+ my $sth = $dbh->prepare(
+ 'UPDATE cust_main SET referral_custnum = NULL WHERE referral_custnum = ?'
+ ) or do {
+ my $errstr = $dbh->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+ $sth->execute($self->custnum) or do {
+ my $errstr = $sth->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+
+ #tickets
+
+ my $ticket_dbh = '';
+ if ($conf->config('ticket_system') eq 'RT_Internal') {
+ $ticket_dbh = $dbh;
+ } elsif ($conf->config('ticket_system') eq 'RT_External') {
+ my ($datasrc, $user, $pass) = $conf->config('ticket_system-rt_external_datasrc');
+ $ticket_dbh = DBI->connect($datasrc, $user, $pass, { 'ChopBlanks' => 1 });
+ #or die "RT_External DBI->connect error: $DBI::errstr\n";
+ }
+
+ if ( $ticket_dbh ) {
+
+ my $ticket_sth = $ticket_dbh->prepare(
+ 'DELETE FROM Links WHERE Target = ?'
+ ) or do {
+ my $errstr = $ticket_dbh->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+ $ticket_sth->execute('freeside://freeside/cust_main/'.$self->custnum)
+ or do {
+ my $errstr = $ticket_sth->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+
+ #check and see if the customer is the only link on the ticket, and
+ #if so, set the ticket to deleted status in RT?
+ #maybe someday, for now this will at least fix tickets not displaying
+
+ }
+
+ #delete the customer record
+
my $error = $self->SUPER::delete;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
# bad idea to disable, causes billing to fail because of no tax rates later
-# unless ( $import ) {
+# except we don't fail any more
+ unless ( $import ) {
unless ( qsearch('cust_main_county', {
'country' => $self->country,
'state' => '',
'country' => $self->country,
} );
}
-# }
+ }
$error =
$self->ut_phonen('daytime', $self->country)
$self->$flag($1);
}
- $self->otaker(getotaker) unless $self->otaker;
+ $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
warn "$me check AFTER: \n". $self->_dump
if $DEBUG > 2;
my $real_pkgpart = $cust_pkg->pkgpart;
my %hash = $cust_pkg->hash;
- foreach my $part_pkg ( $cust_pkg->part_pkg->self_and_bill_linked ) {
+ # we could implement this bit as FS::part_pkg::has_hidden, but we already
+ # suffer from performance issues
+ $options{has_hidden} = 0;
+ my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked;
+ $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
+
+ foreach my $part_pkg ( @part_pkg ) {
$cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
foreach my $pass (@passes) { # keys %cust_bill_pkg ) {
- my @cust_bill_pkg = @{ $cust_bill_pkg{$pass} };
+ my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
next unless @cust_bill_pkg; #don't create an invoice w/o line items
} elsif ( $postal_pkg ) {
my $real_pkgpart = $postal_pkg->pkgpart;
- foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) {
+ # we could implement this bit as FS::part_pkg::has_hidden, but we already
+ # suffer from performance issues
+ $options{has_hidden} = 0;
+ my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
+ $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
+
+ foreach my $part_pkg ( @part_pkg ) {
my %postal_options = %options;
delete $postal_options{cancel};
my $error =
}
}
+ # it's silly to have a zero value postal_pkg, but....
+ @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
+
}
}
''; #no error
}
+#discard bundled packages of 0 value
+sub _omit_zero_value_bundles {
+
+ my @cust_bill_pkg = ();
+ my @cust_bill_pkg_bundle = ();
+ my $sum = 0;
+
+ foreach my $cust_bill_pkg ( @_ ) {
+ if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
+ push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
+ @cust_bill_pkg_bundle = ();
+ $sum = 0;
+ }
+ $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
+ push @cust_bill_pkg_bundle, $cust_bill_pkg;
+ }
+ push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
+
+ (@cust_bill_pkg);
+
+}
+
=item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME
This is a weird one. Perhaps it should not even be exposed.
my %param = ( 'precommit_hooks' => $precommit_hooks,
'increment_next_bill' => $increment_next_bill,
'discounts' => \@discounts,
+ 'real_pkgpart' => $real_pkgpart,
);
my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
# If $cust_pkg has been modified, update it (if we're a real pkgpart)
###
- if ( $lineitems ) {
+ if ( $lineitems || $options{has_hidden} ) {
if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
# hmm.. and if just the options are modified in some weird price plan?
return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
}
- if ( $setup != 0 || $recur != 0 ) {
+ if ( $setup != 0 ||
+ $recur != 0 ||
+ !$part_pkg->hidden && $options{has_hidden} ) #include some $0 lines
+ {
warn " charges (setup=$setup, recur=$recur); adding line items\n"
if $DEBUG > 1;
my @display = ();
my $separate = $conf->exists('separate_usage');
- my $usage_mandate = $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
- if ( $separate || $cust_bill_pkg->hidden || $usage_mandate ) {
+ my $temp_pkg = new FS::cust_pkg { pkgpart => $real_pkgpart };
+ my $usage_mandate = $temp_pkg->part_pkg->option('usage_mandate', 'Hush!');
+ my $section = $temp_pkg->part_pkg->categoryname;
+ if ( $separate || $section || $usage_mandate ) {
- my $temp_pkg = new FS::cust_pkg { pkgpart => $real_pkgpart };
- my %hash = $cust_bill_pkg->hidden # maybe for all bill linked?
- ? ( 'section' => $temp_pkg->part_pkg->categoryname )
- : ();
+ my %hash = ( 'section' => $section );
- my $section = $cust_pkg->part_pkg->option('usage_section', 'Hush!');
- my $summary = $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
+ $section = $temp_pkg->part_pkg->option('usage_section', 'Hush!');
+ my $summary = $temp_pkg->part_pkg->option('summarize_usage', 'Hush!');
if ( $separate ) {
push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
$hash{post_total} = 'Y';
}
- $hash{section} = $section if ($separate || $usage_mandate);
- push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
+ if ($separate || $usage_mandate) {
+ $hash{section} = $section if ($separate || $usage_mandate);
+ push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
+ }
}
$cust_bill_pkg->set('display', \@display);
#false laziness w/misc/process/payment.cgi - check both to make sure working
# correctly
- if ( defined $self->dbdef_table->column('paycvv')
- && length($self->paycvv)
+ if ( length($self->paycvv)
&& ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
) {
my $error = $self->remove_cvv;
my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
my $error = '';
if ( $msgnum ) {
+ # include the raw error message in the transaction state
+ $cust_pay_pending->setfield('error', $transaction->error_message);
my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
- $error = $msg_template->send( 'cust_main' => $self );
+ $error = $msg_template->send( 'cust_main' => $self,
+ 'object' => $cust_pay_pending );
}
else { #!$msgnum
my $self = shift;
my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
') FROM cust_main WHERE custnum='. $self->custnum;
- sprintf( "%.2f", $self->scalar_sql($sql) );
+ sprintf( '%.2f', $self->scalar_sql($sql) );
}
=item balance_pkgnum PKGNUM
);
}
+=item cust_pay_pending_attempt
+
+Returns all payment attempts / declined payments for this customer, as pending
+payments objects (see L<FS::cust_pay_pending>), with status "done" but without
+a corresponding payment (see L<FS::cust_pay>).
+
+=cut
+
+sub cust_pay_pending_attempt {
+ my $self = shift;
+ return $self->num_cust_pay_pending_attempt unless wantarray;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_pay_pending', {
+ 'custnum' => $self->custnum,
+ 'status' => 'done',
+ 'paynum' => '',
+ },
+ );
+}
+
=item num_cust_pay_pending
Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
sub num_cust_pay_pending {
my $self = shift;
- my $sql = " SELECT COUNT(*) FROM cust_pay_pending ".
- " WHERE custnum = ? AND status != 'done' ";
- my $sth = dbh->prepare($sql) or die dbh->errstr;
- $sth->execute($self->custnum) or die $sth->errstr;
- $sth->fetchrow_arrayref->[0];
+ $self->scalar_sql(
+ " SELECT COUNT(*) FROM cust_pay_pending ".
+ " WHERE custnum = ? AND status != 'done' ",
+ $self->custnum
+ );
+}
+
+=item num_cust_pay_pending_attempt
+
+Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
+customer, with status "done" but without a corresp. Also called automatically when the
+cust_pay_pending method is used in a scalar context.
+
+=cut
+
+sub num_cust_pay_pending_attempt {
+ my $self = shift;
+ $self->scalar_sql(
+ " SELECT COUNT(*) FROM cust_pay_pending ".
+ " WHERE custnum = ? AND status = 'done' AND paynum IS NULL",
+ $self->custnum
+ );
}
=item cust_refund
my $param = shift;
#warn join('-',keys %$param);
my $fh = $param->{filehandle};
- my @fields = @{$param->{fields}};
+ my $agentnum = $param->{agentnum};
+ my $format = $param->{format};
+
+ my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+ my @fields;
+ if ( $format eq 'simple' ) {
+ @fields = qw( custnum agent_custid amount pkg );
+ } else {
+ die "unknown format $format";
+ }
eval "use Text::CSV_XS;";
die $@ if $@;
$row{$field} = shift @columns;
}
- my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
+ if ( $row{custnum} && $row{agent_custid} ) {
+ dbh->rollback if $oldAutoCommit;
+ return "can't specify custnum with agent_custid $row{agent_custid}";
+ }
+
+ my %hash = ();
+ if ( $row{agent_custid} && $agentnum ) {
+ %hash = ( 'agent_custid' => $row{agent_custid},
+ 'agentnum' => $agentnum,
+ );
+ }
+
+ if ( $row{custnum} ) {
+ %hash = ( 'custnum' => $row{custnum} );
+ }
+
+ unless ( scalar(keys %hash) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't find customer without custnum or agent_custid and agentnum";
+ }
+
+ my $cust_main = qsearchs('cust_main', { %hash } );
unless ( $cust_main ) {
$dbh->rollback if $oldAutoCommit;
- return "unknown custnum $row{'custnum'}";
+ my $custnum = $row{custnum} || $row{agent_custid};
+ return "unknown custnum $custnum";
}
if ( $row{'amount'} > 0 ) {
=cut
+# a lot like cust_bill::print_latex
sub generate_letter {
my ($self, $template, %options) = @_;
$letter_data{returnaddress} = $retadd;
} elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
$letter_data{returnaddress} =
- join( '\\*'."\n", map s/( {2,})/'~' x length($1)/eg,
- $conf->config('company_address', $self->agentnum)
+ join( "\n", map { s/( {2,})/'~' x length($1)/eg;
+ s/$/\\\\\*/;
+ $_;
+ }
+ ( $conf->config('company_name', $self->agentnum),
+ $conf->config('company_address', $self->agentnum),
+ )
);
} else {
$letter_data{returnaddress} = '~';
$letter_data{company_name} = $conf->config('company_name', $self->agentnum);
my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
+
+ my $lh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
+ DIR => $dir,
+ SUFFIX => '.eps',
+ UNLINK => 0,
+ ) or die "can't open temp file: $!\n";
+ print $lh $conf->config_binary('logo.eps', $self->agentnum)
+ or die "can't write temp file: $!\n";
+ close $lh;
+ $letter_data{'logo_file'} = $lh->filename;
+
my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
DIR => $dir,
SUFFIX => '.tex',
$letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
close $fh;
$fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
- return $1;
+ return ($1, $letter_data{'logo_file'});
+
}
=item print_ps TEMPLATE
sub print_ps {
my $self = shift;
- my $file = $self->generate_letter(@_);
- FS::Misc::generate_ps($file);
+ my($file, $lfile) = $self->generate_letter(@_);
+ my $ps = FS::Misc::generate_ps($file);
+ unlink($file.'.tex');
+ unlink($lfile);
+
+ $ps;
}
=item print TEMPLATE