require 5.006;
use strict;
#FS::cust_main:_Marketgear when they're ready to move to 2.1
-use base qw( FS::cust_main::Packages
+use base qw( FS::cust_main::Packages FS::cust_main::Status
FS::cust_main::Billing FS::cust_main::Billing_Realtime
+ FS::cust_main::Billing_Discount
FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
FS::geocode_Mixin
FS::Record
use FS::Misc qw( generate_email send_email generate_ps do_print );
use FS::Msgcat qw(gettext);
use FS::CurrentUser;
+use FS::TicketSystem;
use FS::payby;
use FS::cust_pkg;
use FS::cust_svc;
use FS::payment_gateway;
use FS::agent_payment_gateway;
use FS::banned_pay;
-use FS::TicketSystem;
+use FS::cust_main_note;
+use FS::cust_attachment;
+use FS::contact;
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
Discourage individual CDR printing, empty or `Y'
+=item edit_subject
+
+Allow self-service editing of ticket subjects, empty or 'Y'
+
=back
=head1 METHODS
$cust_main->insert( {}, [ $email, 'POST' ] );
-Currently available options are: I<depend_jobnum>, I<noexport> and I<tax_exemption>.
+Currently available options are: I<depend_jobnum>, I<noexport>,
+I<tax_exemption> and I<prospectnum>.
If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
on the supplied jobnum (they will not run until the specific job completes).
The I<tax_exemption> option can be set to an arrayref of tax names.
FS::cust_main_exemption records will be created and inserted.
+If I<prospectnum> is set, moves contacts and locations from that prospect.
+
=cut
sub insert {
}
}
- if ( $invoicing_list ) {
- $error = $self->check_invoicing_list( $invoicing_list );
+ my $prospectnum = delete $options{'prospectnum'};
+ if ( $prospectnum ) {
+
+ warn " moving contacts and locations from prospect $prospectnum\n"
+ if $DEBUG > 1;
+
+ my $prospect_main =
+ qsearchs('prospect_main', { 'prospectnum' => $prospectnum } );
+ unless ( $prospect_main ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Unknown prospectnum $prospectnum";
+ }
+ $prospect_main->custnum($self->custnum);
+ $prospect_main->disabled('Y');
+ my $error = $prospect_main->replace;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- #return "checking invoicing_list (transaction rolled back): $error";
return $error;
}
- $self->invoicing_list( $invoicing_list );
- }
+ my @contact = $prospect_main->contact;
+ my @cust_location = $prospect_main->cust_location;
+ my @qual = $prospect_main->qual;
+
+ foreach my $r ( @contact, @cust_location, @qual ) {
+ $r->prospectnum('');
+ $r->custnum($self->custnum);
+ my $error = $r->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ }
warn " setting cust_main_exemption\n"
if $DEBUG > 1;
}
+=item merge NEW_CUSTNUM [ , OPTION => VALUE ... ]
+
+This merges this customer into the provided new custnum, and then deletes the
+customer. If there is an error, returns the error, otherwise returns false.
+
+The source customer's name, company name, phone numbers, agent,
+referring customer, customer class, advertising source, order taker, and
+billing information (except balance) are discarded.
+
+All packages are moved to the target customer. Packages with package locations
+are preserved. Packages without package locations are moved to a new package
+location with the source customer's service/shipping address.
+
+All invoices, statements, payments, credits and refunds are moved to the target
+customer. The source customer's balance is added to the target customer.
+
+All notes, attachments, tickets and customer tags are moved to the target
+customer.
+
+Change history is not currently moved.
+
+=cut
+
+sub merge {
+ my( $self, $new_custnum, %opt ) = @_;
+
+ return "Can't merge a customer into self" if $self->custnum == $new_custnum;
+
+ unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
+ return "Invalid new customer number: $new_custnum";
+ }
+
+ 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;
+
+ if ( qsearch('agent', { 'agent_custnum' => $self->custnum } ) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Can't merge a master agent customer";
+ }
+
+ #use FS::access_user
+ if ( qsearch('access_user', { 'user_custnum' => $self->custnum } ) ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Can't merge a master employee customer";
+ }
+
+ if ( qsearch('cust_pay_pending', { 'custnum' => $self->custnum,
+ 'status' => { op=>'!=', value=>'done' },
+ }
+ )
+ ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Can't merge a customer with pending payments";
+ }
+
+ tie my %financial_tables, 'Tie::IxHash',
+ 'cust_bill' => 'invoices',
+ 'cust_statement' => 'statements',
+ 'cust_credit' => 'credits',
+ 'cust_pay' => 'payments',
+ 'cust_pay_void' => 'voided payments',
+ 'cust_refund' => 'refunds',
+ ;
+
+ foreach my $table ( keys %financial_tables ) {
+
+ my @records = $self->$table();
+
+ foreach my $record ( @records ) {
+ $record->custnum($new_custnum);
+ my $error = $record->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error merging ". $financial_tables{$table}. ": $error\n";
+ }
+ }
+
+ }
+
+ my $name = $self->ship_name;
+
+ my $locationnum = '';
+ foreach my $cust_pkg ( $self->all_pkgs ) {
+ $cust_pkg->custnum($new_custnum);
+
+ unless ( $cust_pkg->locationnum ) {
+ unless ( $locationnum ) {
+ my $cust_location = new FS::cust_location {
+ $self->location_hash,
+ 'custnum' => $new_custnum,
+ };
+ my $error = $cust_location->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $locationnum = $cust_location->locationnum;
+ }
+ $cust_pkg->locationnum($locationnum);
+ }
+
+ my $error = $cust_pkg->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ # add customer (ship) name to svc_phone.phone_name if blank
+ my @cust_svc = $cust_pkg->cust_svc;
+ foreach my $cust_svc (@cust_svc) {
+ my($label, $value, $svcdb) = $cust_svc->label;
+ next unless $svcdb eq 'svc_phone';
+ my $svc_phone = $cust_svc->svc_x;
+ next if $svc_phone->phone_name;
+ $svc_phone->phone_name($name);
+ my $error = $svc_phone->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ }
+
+ #not considered:
+ # cust_tax_exempt (texas tax exemptions)
+ # cust_recon (some sort of not-well understood thing for OnPac)
+
+ #these are moved over
+ foreach my $table (qw(
+ cust_tag cust_location contact cust_attachment cust_main_note
+ cust_tax_adjustment cust_pay_batch queue
+ )) {
+ foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
+ $record->custnum($new_custnum);
+ my $error = $record->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ }
+
+ #these aren't preserved
+ foreach my $table (qw(
+ cust_main_exemption cust_main_invoice
+ )) {
+ foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
+ my $error = $record->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ }
+
+
+ my $sth = $dbh->prepare(
+ 'UPDATE cust_main SET referral_custnum = ? WHERE referral_custnum = ?'
+ ) or do {
+ my $errstr = $dbh->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+ $sth->execute($new_custnum, $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(
+ 'UPDATE Links SET Target = ? WHERE Target = ?'
+ ) or do {
+ my $errstr = $ticket_dbh->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+ $ticket_sth->execute('freeside://freeside/cust_main/'.$new_custnum,
+ 'freeside://freeside/cust_main/'.$self->custnum)
+ or do {
+ my $errstr = $ticket_sth->errstr;
+ $dbh->rollback if $oldAutoCommit;
+ return $errstr;
+ };
+
+ }
+
+ #delete the customer record
+
+ my $error = $self->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
=item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
return "You are not permitted to create complimentary accounts.";
}
+ if ( $old->get('geocode') && $old->get('geocode') eq $self->get('geocode')
+ && $conf->exists('enable_taxproducts')
+ )
+ {
+ my $pre = ($conf->exists('tax-ship_address') && $self->ship_zip)
+ ? 'ship_' : '';
+ $self->set('geocode', '')
+ if $old->get($pre.'zip') ne $self->get($pre.'zip')
+ && length($self->get($pre.'zip')) >= 10;
+ }
+
local($ignore_expired_card) = 1
if $old->payby =~ /^(CARD|DCRD)$/
&& $self->payby =~ /^(CARD|DCRD)$/
|| $self->ut_alphan('geocode')
|| $self->ut_floatn('cdr_termination_percentage')
|| $self->ut_floatn('credit_limit')
+ || $self->ut_numbern('billday')
+ || $self->ut_enum('edit_subject', [ '', 'Y' ] )
;
#barf. need message catalogs. i18n. etc.
} elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
my $payinfo = $self->payinfo;
- $payinfo =~ s/[^\d\@]//g;
- if ( $conf->exists('echeck-nonus') ) {
+ $payinfo =~ s/[^\d\@\.]//g;
+ if ( $conf->exists('cust_main-require-bank-branch') ) {
+ $payinfo =~ /^(\d+)\@(\d+)\.(\d+)$/ or return 'invalid echeck account@branch.bank';
+ $payinfo = "$1\@$2.$3";
+ }
+ elsif ( $conf->exists('echeck-nonus') ) {
$payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
$payinfo = "$1\@$2";
} else {
qsearch('cust_location', { 'custnum' => $self->custnum } );
}
+=item cust_contact
+
+Returns all contacts (see L<FS::contact>) for this customer.
+
+=cut
+
+#already used :/ sub contact {
+sub cust_contact {
+ my $self = shift;
+ qsearch('contact', { 'custnum' => $self->custnum } );
+}
+
=item unsuspend
Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
=cut
sub notes {
- my $self = shift;
- #order by?
+ my($self,$orderby_classnum) = (shift,shift);
+ my $orderby = "_DATE DESC";
+ $orderby = "CLASSNUM ASC, $orderby" if $orderby_classnum;
qsearch( 'cust_main_note',
{ 'custnum' => $self->custnum },
- '',
- 'ORDER BY _DATE DESC'
- );
+ '',
+ "ORDER BY $orderby",
+ );
}
=item agent
sub charge_postal_fee {
my $self = shift;
- my $pkgpart = $conf->config('postal_invoice-fee_pkgpart');
+ my $pkgpart = $conf->config('postal_invoice-fee_pkgpart', $self->agentnum);
return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
my $cust_pkg = new FS::cust_pkg ( {
=back
+Behavior of inactive vs. cancelled edge cases can be adjusted with the
+cust_main-status_module configuration option.
+
=cut
sub status { shift->cust_status(@_); }
=cut
-use vars qw(%statuscolor);
-tie %statuscolor, 'Tie::IxHash',
- 'prospect' => '7e0079', #'000000', #black? naw, purple
- 'active' => '00CC00', #green
- 'ordered' => '009999', #teal? cyan?
- 'suspended' => 'FF9900', #yellow
- 'cancelled' => 'FF0000', #red
- 'inactive' => '0000CC', #blue
-;
-
sub statuscolor { shift->cust_statuscolor(@_); }
sub cust_statuscolor {
my $self = shift;
- $statuscolor{$self->cust_status};
+ __PACKAGE__->statuscolors->{$self->cust_status};
}
=item tickets
=cut
sub statuses {
- #my $self = shift; #could be class...
- keys %statuscolor;
+ my $self = shift;
+ keys %{ $self->statuscolors };
}
=item cust_status_sql
=item ordered_sql
Returns an SQL expression identifying ordered cust_main records (customers with
-recurring packages not yet setup).
+no active packages, but recurring packages not yet setup or one time charges
+not yet billed).
=cut
sub ordered_sql {
FS::cust_main->none_active_sql.
- " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->ordered_sql. " ) ";
+ " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->not_yet_billed_sql. " ) ";
}
=item active_sql
=cut
-sub cancelled_sql { cancel_sql(@_); }
-sub cancel_sql {
-
- my $recurring_sql = FS::cust_pkg->recurring_sql;
- my $cancelled_sql = FS::cust_pkg->cancelled_sql;
-
- "
- 0 < ( $select_count_pkgs )
- AND 0 < ( $select_count_pkgs AND $recurring_sql AND $cancelled_sql )
- AND 0 = ( $select_count_pkgs AND $recurring_sql
- AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
- )
- ";
-# AND 0 = ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
-
-}
+sub cancel_sql { shift->cancelled_sql(@_); }
=item uncancel_sql
=item uncancelled_sql
=item unapplied_payments_date_sql START_TIME [ END_TIME ]
Returns an SQL fragment to retreive the total unapplied payments for this
-customer, only considering invoices with date earlier than START_TIME, and
+customer, only considering payments with date earlier than START_TIME, and
optionally not later than END_TIME.
Times are specified as SQL fragments or numeric
" ORDER BY
CASE WHEN part_event_condition_option.optionname IS NULL
THEN -1
- ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
+ ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
" END
, part_event.weight".
" LIMIT 1"