=over 4
-=item insert_payment
+=item insert_payment OPTION => VALUE, ...
-Adds a new payment to a customers account. Takes a hash reference as parameter with the following keys:
+Adds a new payment to a customers account. Takes a list of keys and values as
+paramters with the following keys:
=over 5
=item _date
-
Option date for payment
+=back
+
Example:
my $result = FS::API->insert_payment(
print "paynum ". $result->{'paynum'};
}
-=back
-
=cut
#enter cash payment
}
-=item insert_credit
+=item insert_credit OPTION => VALUE, ...
-Adds a a credit to a customers account. Takes a hash reference as parameter with the following keys
+Adds a a credit to a customers account. Takes a list of keys and values as
+parameters with the following keys
=over
The date the credit will be posted
+=back
+
Example:
my $result = FS::API->insert_credit(
print "crednum ". $result->{'crednum'};
}
-=back
-
=cut
#Enter credit
}
-=item insert_refund
+=item insert_refund OPTION => VALUE, ...
-Adds a a credit to a customers account. Takes a hash reference as parameter with the following keys: custnum,payby,refund
+Adds a a credit to a customers account. Takes a list of keys and values as
+parmeters with the following keys: custnum, payby, refund
Example:
# long-term: package changes?
-=item new_customer
+=item new_customer OPTION => VALUE, ...
-Creates a new customer. Takes a hash reference as parameter with the following keys:
+Creates a new customer. Takes a list of keys and values as parameters with the
+following keys:
=over 4
Referring customer number
+=back
=cut
$class->API_insert( %opt );
}
-=back
-
=item update_customer
-Updates an existing customer. Takes a hash reference as parameter with the foll$
+Updates an existing customer. Passing an empty value clears that field, while
+NOT passing that key/value at all leaves it alone. Takes a list of keys and
+values as parameters with the following keys:
=over 4
=item secret
-API Secret
+API Secret (required)
+
+=item custnum
+
+Customer number (required)
=item first
-first name (required)
+first name
=item last
-last name (required)
+last name
=item company
Company name
-=item address1 (required)
+=item address1
Address line one
-=item city (required)
+=item city
City
County
-=item state (required)
+=item state
State
-=item zip (required)
+=item zip
Zip or postal code
=item invoicing_list
-comma-separated list of email addresses for email invoices. The special value '$
+Comma-separated list of email addresses for email invoices. The special value
+'POST' is used to designate postal invoicing (it may be specified alone or in
+addition to email addresses),
postal_invoicing
Set to 1 to enable postal invoicing
=item payinfo
-Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid "pi$
+Card number for CARD/DCRD, account_number@aba_number for CHEK/DCHK, prepaid
+"pin" for PREPAY, purchase order number for BILL
=item paycvv
Referring customer number
=item salesnum
+
Sales person number
=item agentnum
Agent number
+=back
+
=cut
+
sub update_customer {
my( $class, %opt ) = @_;
FS::cust_main->API_update( %opt );
}
-=back
-
+=item customer_info OPTION => VALUE, ...
-=item customer_info
-
-Returns general customer information. Takes a hash reference as parameter with the following keys: custnum and API secret
+Returns general customer information. Takes a list of keys and values as
+parameters with the following keys: custnum, secret
=cut
=item location_info
-Returns location specific information for the customer. Takes a hash reference as parameter with the following keys: custnum,secret
-
-=back
+Returns location specific information for the customer. Takes a list of keys
+and values as paramters with the following keys: custnum, secret
=cut
return \%return;
}
+=item bill_now OPTION => VALUE, ...
+
+Bills a single customer now, in the same fashion as the "Bill now" link in the
+UI.
+
+Returns a hash reference with a single key, 'error'. If there is an error,
+the value contains the error, otherwise it is empty.
+
+=cut
+
+sub bill_now {
+ my( $class, %opt ) = @_;
+ my $conf = new FS::Conf;
+ return { 'error' => 'Incorrect shared secret' }
+ unless $opt{secret} eq $conf->config('api_shared_secret');
+
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
+ or return { 'error' => 'Unknown custnum' };
+
+ my $error = $cust_main->bill_and_collect( 'fatal' => 'return',
+ 'retry' => 1,
+ 'check_freq' =>'1d',
+ );
+
+ return { 'error' => $error,
+ };
+
+}
+
+
#Advertising sources?
use FS::acct_rt_transaction;
use FS::msg_template;
use FS::contact;
+use FS::cust_contact;
$DEBUG = 1;
$me = '[FS::ClientAPI::MyAccount]';
#return { 'error' => $session } if $context eq 'error';
my $agentnum = '';
- if ( $context eq 'customer' ) {
+ if ( $context eq 'customer' && $custnum ) {
my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?')
or die dbh->errstr;
return { error => 'Incorrect contact password.' }
unless $contact->authenticate_password($p->{'password'});
- $session->{'custnum'} = $contact->custnum;
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ if ( scalar(@cust_contact) == 1 ) {
+ $session->{'custnum'} = $cust_contact[0]->custnum;
+ } elsif ( scalar(@cust_contact) ) {
+ $session->{'customers'} = { map { $_->custnum => $_->cust_main->name }
+ @cust_contact
+ };
+ } else {
+ return { error => 'No customer self-service access for contact' }; #??
+ }
} else {
return { 'error' => '',
'session_id' => $session_id,
+ %$session,
};
}
}
+sub switch_cust {
+ my $p = shift;
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ $session->{'custnum'} = $p->{'custnum'}
+ if exists $session->{'customers'}{ $p->{'custnum'} };
+
+ my $conf = new FS::Conf;
+ my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
+ _cache->set( $p->{'session_id'}, $session, $timeout );
+
+ return { 'error' => '',
+ %{ customer_info( { session_id=>$p->{'session_id'} } ) },
+ };
+}
+
sub payment_gateway {
# internal use only
# takes a cust_main and a cust_payby entry, returns the payment_gateway
my($context, $session, $custnum) = _custoragent_session_custnum($p);
return { 'error' => $session } if $context eq 'error';
- my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
- or return { 'error' => "unknown custnum $custnum" };
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
$info->{'hide_payment_fields'} = [
map {
- my $pg = payment_gateway($cust_main, $_);
+ my $pg = $cust_main && payment_gateway($cust_main, $_);
$pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
} @{ $info->{cust_paybys} }
];
$info->{'self_suspend_reason'} =
- $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum);
+ $conf->config('selfservice-self_suspend_reason',
+ $cust_main ? $cust_main->agentnum : ''
+ );
$info->{'edit_ticket_subject'} =
$conf->exists('ticket_system-selfservice_edit_subject') &&
- $cust_main->edit_subject;
+ $cust_main && $cust_main->edit_subject;
$info->{'timeout'} = $conf->config('selfservice-timeout') || 3600;
my $search = { 'custnum' => $custnum };
$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
my $cust_main = qsearchs('cust_main', $search )
- or return { 'error' => "unknown custnum $custnum" };
+ or return { 'error' => "customer_info: unknown custnum $custnum" };
my $list_tickets = list_tickets($p);
$return{'tickets'} = $list_tickets->{'tickets'};
my $search = { 'custnum' => $custnum };
$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
my $cust_main = qsearchs('cust_main', $search )
- or return { 'error' => "unknown custnum $custnum" };
+ or return { 'error' => "customer_info_short: unknown custnum $custnum" };
$return{display_custnum} = $cust_main->display_custnum;
#need to support the "ISP provides email that's used as a contact email" case
#as well as we can.
my $contact = FS::contact->by_selfservice_email($svc_acct->email);
- if ( $contact && $contact->custnum == $custnum ) {
+ if ( $contact && qsearchs('cust_contact', { contactnum=> $contact->contactnum,
+ custnum => $custnum,
+ selfservice_access => 'Y',
+ }
+ )
+ ) {
#svc_acct was successful but this one returns an error? "shouldn't happen"
$error ||= $contact->change_password($p->{'new_password'});
}
$contact = FS::contact->by_selfservice_email($p->{'email'});
- $cust_main = $contact->cust_main if $contact;
+ if ( $contact ) {
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
+ }
#also look for an svc_acct, otherwise it would be super confusing
}
+ return { %$info, 'error' => 'Multi-customer contacts incompatible with customer-based verification' }
+ if ! $cust_main && $verification ne 'email';
+
my %verify = (
'email' => sub { 1; },
'paymask' => sub {
my @contact_email = $contact->contact_email;
return { 'error' => 'No contact email' } unless @contact_email;
- $p->{'agentnum'} = $contact->cust_main->agentnum;
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
+ if scalar(@cust_contact) == 1;
my $info = skin_info($p);
return { %$info,
$contact = qsearchs('contact', { 'contactnum' => $contactnum } )
or return { 'error' => "Contact not found" };
- $p->{'agentnum'} ||= $contact->cust_main->agentnum;
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
+ if scalar(@cust_contact) == 1;
$info ||= skin_info($p);
}
'login' => 'MyAccount/login',
'logout' => 'MyAccount/logout',
'switch_acct' => 'MyAccount/switch_acct',
+ 'switch_cust' => 'MyAccount/switch_cust',
'customer_info' => 'MyAccount/customer_info',
'customer_info_short' => 'MyAccount/customer_info_short',
'billing_history' => 'MyAccount/billing_history',
'type' => 'select',
'select_hash' => [ '' => 'Password reset disabled',
'email' => 'Click on a link in email',
- 'paymask,amount,zip' => 'Click on a link in email, and also verify with credit card (or bank account) last 4 digits, payment amount and zip code',
+ 'paymask,amount,zip' => 'Click on a link in email, and also verify with credit card (or bank account) last 4 digits, payment amount and zip code. Note: Do not use if you have multi-customer contacts, as they will be unable to reset their passwords.',
],
},
#parent doesn't need to hold a DB connection open
dbh->disconnect;
undef $FS::UID::dbh;
+ undef $RT::Handle;
server_spawn(MAX_PROCESSES);
POE::Kernel->run();
use FS::circuit_provider;
use FS::circuit_termination;
use FS::svc_circuit;
+ use FS::cust_credit_source_bill_pkg;
+ use FS::prospect_contact;
+ use FS::cust_contact;
# Sammath Naur
if ( $FS::Mason::addl_handler_use ) {
my $table = $_[0];
my(@result) = qsearch(@_);
cluck "warning: Multiple records in scalar search ($table)"
+ #.join(' / ', map "$_=>".$_[1]->{$_}, keys %{ $_[1] } )
if scalar(@result) > 1;
#should warn more vehemently if the search was on a primary key?
scalar(@result) ? ($result[0]) : ();
'index' => [ ['disabled'] ],
},
+ 'cust_contact' => {
+ 'columns' => [
+ 'custcontactnum', 'serial', '', '', '', '',
+ 'custnum', 'int', '', '', '', '',
+ 'contactnum', 'int', '', '', '', '',
+ 'classnum', 'int', 'NULL', '', '', '',
+ 'comment', 'varchar', 'NULL', 255, '', '',
+ 'selfservice_access', 'char', 'NULL', 1, '', '',
+ ],
+ 'primary_key' => 'custcontactnum',
+ 'unique' => [ [ 'custnum', 'contactnum' ], ],
+ 'index' => [ [ 'custnum' ], [ 'contactnum' ], ],
+ 'foreign_keys' => [
+ { columns => [ 'custnum' ],
+ table => 'cust_main',
+ },
+ { columns => [ 'contactnum' ],
+ table => 'contact',
+ },
+ { columns => [ 'classnum' ],
+ table => 'contact_class',
+ },
+ ],
+ },
+
+ 'prospect_contact' => {
+ 'columns' => [
+ 'prospectcontactnum', 'serial', '', '', '', '',
+ 'prospectnum', 'int', '', '', '', '',
+ 'contactnum', 'int', '', '', '', '',
+ 'classnum', 'int', 'NULL', '', '', '',
+ 'comment', 'varchar', 'NULL', 255, '', '',
+ ],
+ 'primary_key' => 'prospectcontactnum',
+ 'unique' => [ [ 'prospectnum', 'contactnum' ], ],
+ 'index' => [ [ 'prospectnum' ], [ 'contactnum' ], ],
+ 'foreign_keys' => [
+ { columns => [ 'prospectnum' ],
+ table => 'prospect_main',
+ },
+ { columns => [ 'contactnum' ],
+ table => 'contact',
+ },
+ { columns => [ 'classnum' ],
+ table => 'contact_class',
+ },
+ ],
+ },
+
'contact' => {
'columns' => [
'contactnum', 'serial', '', '', '', '',
- 'prospectnum', 'int', 'NULL', '', '', '',
- 'custnum', 'int', 'NULL', '', '', '',
+ 'prospectnum', 'int', 'NULL', '', '', '', #deprecated, now prospect_contact table
+ 'custnum', 'int', 'NULL', '', '', '', #deprecated, now cust_contact table
'locationnum', 'int', 'NULL', '', '', '', #not yet
- 'classnum', 'int', 'NULL', '', '', '',
+ 'classnum', 'int', 'NULL', '', '', '', #deprecated, now prospect_contact or cust_contact
# 'titlenum', 'int', 'NULL', '', '', '', #eg Mr. Mrs. Dr. Rev.
'last', 'varchar', '', $char_d, '', '',
# 'middle', 'varchar', 'NULL', $char_d, '', '',
'first', 'varchar', '', $char_d, '', '',
'title', 'varchar', 'NULL', $char_d, '', '', #eg Head Bottle Washer
- 'comment', 'varchar', 'NULL', 255, '', '',
- 'selfservice_access', 'char', 'NULL', 1, '', '',
+ 'comment', 'varchar', 'NULL', 255, '', '', #depredated, now prospect_contact or cust_contact
+ 'selfservice_access', 'char', 'NULL', 1, '', '', #deprecated, now cust_contact
'_password', 'varchar', 'NULL', $char_d, '', '',
'_password_encoding', 'varchar', 'NULL', $char_d, '', '',
'disabled', 'char', 'NULL', 1, '', '',
);
# but NOT $conf
use vars qw( $invoice_lines @buf ); #yuck
-use List::Util qw(sum);
+use List::Util qw(sum first);
use Date::Format;
use Date::Language;
use Text::Template 1.20;
warn "$me generating sections\n"
if $DEBUG > 1;
- my $taxtotal = 0;
- my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
- 'subtotal' => $taxtotal, # adjusted below
- 'tax_section' => 1,
- };
- my $tax_weight = _pkg_category($tax_section->{description})
- ? _pkg_category($tax_section->{description})->weight
- : 0;
- $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
- $tax_section->{'sort_weight'} = $tax_weight;
-
- my $adjusttotal = 0;
- my $adjust_section = {
- 'description' => $self->mt('Credits, Payments, and Adjustments'),
- 'adjust_section' => 1,
- 'subtotal' => 0, # adjusted below
- };
- my $adjust_weight = _pkg_category($adjust_section->{description})
- ? _pkg_category($adjust_section->{description})->weight
- : 0;
- $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
- $adjust_section->{'sort_weight'} = $adjust_weight;
-
my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
my $multisection = $conf->exists($tc.'sections', $cust_main->agentnum) ||
$conf->exists($tc.'sections_by_location', $cust_main->agentnum);
$previous_section = $default_section;
}
+ my $adjust_section = {
+ 'description' => $self->mt('Credits, Payments, and Adjustments'),
+ 'adjust_section' => 1,
+ 'subtotal' => 0, # adjusted below
+ };
+ my $adjust_weight = _pkg_category($adjust_section->{description})
+ ? _pkg_category($adjust_section->{description})->weight
+ : 0;
+ $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
+ # Note: 'sort_weight' here is actually a flag telling whether there is an
+ # explicit package category for the adjust section. If so, certain behavior
+ # happens.
+ $adjust_section->{'sort_weight'} = $adjust_weight;
+
+
if ( $multisection ) {
($extra_sections, $extra_lines) =
$self->_items_extra_usage_sections($escape_function_nonbsp, $format)
warn "$me adding taxes\n"
if $DEBUG > 1;
+ # create a tax section if we don't yet have one
+ my $tax_description = 'Taxes, Surcharges, and Fees';
+ my $tax_section = first { $_->{description} eq $tax_description } @sections;
+ if (!$tax_section) {
+ $tax_section = { 'description' => $tax_description };
+ push @sections, $tax_section if $multisection;
+ }
+ $tax_section->{tax_section} = 1; # mark this section as containing taxes
+ # if this is an existing tax section, we're merging the tax items into it.
+ # grab the taxtotal that's already there, strip the money symbol if any
+ my $taxtotal = $tax_section->{'subtotal'} || 0;
+ $taxtotal =~ s/^\Q$other_money_char\E//;
+
+ # this does nothing
+ #my $tax_weight = _pkg_category($tax_section->{description})
+ # ? _pkg_category($tax_section->{description})->weight
+ # : 0;
+ #$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
+ #$tax_section->{'sort_weight'} = $tax_weight;
+
my @items_tax = $self->_items_tax;
foreach my $tax ( @items_tax ) {
$other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
if ( $multisection ) {
- $tax_section->{'subtotal'} = $other_money_char.
- sprintf('%.2f', $taxtotal);
- $tax_section->{'pretotal'} = 'New charges sub-total '.
- $total->{'total_amount'};
- if ( $taxtotal ) {
- push @sections, $tax_section;
- push @summary_subtotals, $tax_section;
+ if ( $taxtotal > 0 ) {
+ $tax_section->{'subtotal'} = $other_money_char.
+ sprintf('%.2f', $taxtotal);
+ $tax_section->{'pretotal'} = 'New charges sub-total '.
+ $total->{'total_amount'};
+ $tax_section->{'description'} = $self->mt($tax_description);
+
+ # append it if it's not already there
+ if ( !grep $tax_section, @sections ) {
+ push @sections, $tax_section;
+ push @summary_subtotals, $tax_section;
+ }
}
+
} else {
unshift @total_items, $total;
}
$money_char. sprintf("%10.2f",$self->charged) ];
push @buf,['',''];
-
###
# Totals
###
$total->{'total_item'} = &$escape_function($credit->{'description'});
$credittotal += $credit->{'amount'};
$total->{'total_amount'} = $minus.$other_money_char.$credit->{'amount'};
- $adjusttotal += $credit->{'amount'};
if ( $multisection ) {
push @detail_items, {
ext_description => [],
$total->{'total_item'} = &$escape_function($payment->{'description'});
$paymenttotal += $payment->{'amount'};
$total->{'total_amount'} = $minus.$other_money_char.$payment->{'amount'};
- $adjusttotal += $payment->{'amount'};
if ( $multisection ) {
push @detail_items, {
ext_description => [],
if ( $multisection ) {
$adjust_section->{'subtotal'} = $other_money_char.
- sprintf('%.2f', $adjusttotal);
+ sprintf('%.2f', $credittotal + $paymenttotal);
+
+ #why this? because {sort_weight} forces the adjust_section to appear
+ #in @extra_sections instead of @sections. obviously.
push @sections, $adjust_section
unless $adjust_section->{sort_weight};
# do not summarize; adjustments there are shown according to
$self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
-The only OPTIONS accepted is 'section', which may point to a hashref
-with a key named 'condensed', which may have a true value. If it
-does, this method tries to merge identical items into items with
-'quantity' equal to the number of items (not the sum of their
-separate quantities, for some reason).
+OPTIONS are passed through to _items_cust_bill_pkg, and should include
+'format' and 'escape_function' at minimum.
+
+To produce items for a specific invoice section, OPTIONS should include
+'section', a hashref containing 'category' and/or 'locationnum' keys.
+
+'section' may also contain a key named 'condensed'. If this is present
+and has a true value, _items_pkg will try to merge identical items into items
+with 'quantity' equal to the number of items (not the sum of their separate
+quantities, for some reason).
=cut
my $self = shift;
my %options = @_;
my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
+ my $escape_function = $options{escape_function};
+
my @items;
foreach my $cust_bill_pkg (@cust_bill_pkg) {
# cache this, so we don't look it up again in every section
}
foreach (sort keys(%base_invnums)) {
next if $_ == $self->invnum;
+ # per convention, we must escape ext_description lines
push @ext_desc,
- $self->mt('from invoice \\#[_1] on [_2]', $_, $base_invnums{$_});
+ &{$escape_function}(
+ $self->mt('from invoice #[_1] on [_2]', $_, $base_invnums{$_})
+ );
}
+ my $desc = $part_fee->itemdesc_locale($self->cust_main->locale);
+ # but not escape the base description line
+
push @items,
{ feepart => $cust_bill_pkg->feepart,
amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
- description => $part_fee->itemdesc_locale($self->cust_main->locale),
+ description => $desc,
ext_description => \@ext_desc
# sdate/edate?
};
#cust_main (remove paycvv from history)
'cust_main' => [],
+ #contact -> cust_contact / prospect_contact
+ 'contact' => [],
+
#msgcat
'msgcat' => [],
use strict;
use vars qw( $skip_fuzzyfiles );
+use Carp;
use Scalar::Util qw( blessed );
use FS::Record qw( qsearch qsearchs dbh );
use FS::contact_phone;
use FS::contact_email;
use FS::queue;
use FS::phone_type; #for cgi_contact_fields
+use FS::cust_contact;
+use FS::prospect_contact;
$skip_fuzzyfiles = 0;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my $error = $self->SUPER::insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
+ #save off and blank values that move to cust_contact / prospect_contact now
+ my $prospectnum = $self->prospectnum;
+ $self->prospectnum('');
+ my $custnum = $self->custnum;
+ $self->custnum('');
+
+ my %link_hash = ();
+ for (qw( classnum comment selfservice_access )) {
+ $link_hash{$_} = $self->get($_);
+ $self->$_('');
+ }
+
+ #look for an existing contact with this email address
+ my $existing_contact = '';
+ if ( $self->get('emailaddress') =~ /\S/ ) {
+
+ my %existing_contact = ();
+
+ foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
+
+ my $contact_email = qsearchs('contact_email', { emailaddress=>$email } )
+ or next;
+
+ my $contact = $contact_email->contact;
+ $existing_contact{ $contact->contactnum } = $contact;
+
+ }
+
+ if ( scalar( keys %existing_contact ) > 1 ) {
+ $dbh->rollback if $oldAutoCommit;
+ return 'Multiple email addresses specified '.
+ ' that already belong to separate contacts';
+ } elsif ( scalar( keys %existing_contact ) ) {
+ ($existing_contact) = values %existing_contact;
+ }
+
+ }
+
+ if ( $existing_contact ) {
+
+ $self->$_($existing_contact->$_())
+ for qw( contactnum _password _password_encoding );
+ $self->SUPER::replace($existing_contact);
+
+ } else {
+
+ my $error = $self->SUPER::insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ }
+
+ my $cust_contact = '';
+ if ( $custnum ) {
+ my %hash = ( 'contactnum' => $self->contactnum,
+ 'custnum' => $custnum,
+ );
+ $cust_contact = qsearchs('cust_contact', \%hash )
+ || new FS::cust_contact { %hash, %link_hash };
+ my $error = $cust_contact->custcontactnum ? $cust_contact->replace
+ : $cust_contact->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ if ( $prospectnum ) {
+ my %hash = ( 'contactnum' => $self->contactnum,
+ 'prospectnum' => $prospectnum,
+ );
+ my $prospect_contact = qsearchs('prospect_contact', \%hash )
+ || new FS::prospect_contact { %hash, %link_hash };
+ my $error =
+ $prospect_contact->prospectcontactnum ? $prospect_contact->replace
+ : $prospect_contact->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
}
foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
$pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
my $phonetypenum = $1;
- my $contact_phone = new FS::contact_phone {
- 'contactnum' => $self->contactnum,
- 'phonetypenum' => $phonetypenum,
- _parse_phonestring( $self->get($pf) ),
- };
- $error = $contact_phone->insert;
+ my %hash = ( 'contactnum' => $self->contactnum,
+ 'phonetypenum' => $phonetypenum,
+ );
+ my $contact_phone =
+ qsearchs('contact_phone', \%hash)
+ || new FS::contact_phone { %hash, _parse_phonestring($self->get($pf)) };
+ my $error = $contact_phone->contactphonenum ? $contact_phone->replace
+ : $contact_phone->insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
if ( $self->get('emailaddress') =~ /\S/ ) {
foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
-
- my $contact_email = new FS::contact_email {
+ my %hash = (
'contactnum' => $self->contactnum,
'emailaddress' => $email,
- };
- $error = $contact_email->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
+ );
+ unless ( qsearchs('contact_email', \%hash) ) {
+ my $contact_email = new FS::contact_email \%hash;
+ my $error = $contact_email->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
}
-
}
}
unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
#warn " queueing fuzzyfiles update\n"
# if $DEBUG > 1;
- $error = $self->queue_fuzzyfiles_update;
+ my $error = $self->queue_fuzzyfiles_update;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "updating fuzzy search cache: $error";
}
}
- if ( $self->selfservice_access ) {
+ if ( $link_hash{'selfservice_access'} eq 'R'
+ or ( $link_hash{'selfservice_access'} && $cust_contact )
+ )
+ {
my $error = $self->send_reset_email( queue=>1 );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ #got a prospetnum or custnum? delete the prospect_contact or cust_contact link
+
+ if ( $self->prospectnum ) {
+ my $prospect_contact = qsearchs('prospect_contact', {
+ 'contactnum' => $self->contactnum,
+ 'prospectnum' => $self->prospectnum,
+ });
+ my $error = $prospect_contact->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ if ( $self->custnum ) {
+ my $cust_contact = qsearchs('cust_contact', {
+ 'contactnum' => $self->contactnum,
+ 'custnum' => $self->custnum,
+ });
+ my $error = $cust_contact->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ # then, proceed with deletion only if the contact isn't attached to any other
+ # prospects or customers
+
+ #inefficient, but how many prospects/customers can a single contact be
+ # attached too? (and is removing them from one a common operation?)
+ if ( $self->prospect_contact || $self->cust_contact ) {
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ return '';
+ }
+
+ #proceed with deletion
+
foreach my $cust_pkg ( $self->cust_pkg ) {
$cust_pkg->contactnum('');
my $error = $cust_pkg->replace;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ #save off and blank values that move to cust_contact / prospect_contact now
+ my $prospectnum = $self->prospectnum;
+ $self->prospectnum('');
+ my $custnum = $self->custnum;
+ $self->custnum('');
+
+ my %link_hash = ();
+ for (qw( classnum comment selfservice_access )) {
+ $link_hash{$_} = $self->get($_);
+ $self->$_('');
+ }
+
my $error = $self->SUPER::replace($old);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
}
- foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) }
+ my $cust_contact = '';
+ if ( $custnum ) {
+ my %hash = ( 'contactnum' => $self->contactnum,
+ 'custnum' => $custnum,
+ );
+ my $error;
+ if ( $cust_contact = qsearchs('cust_contact', \%hash ) ) {
+ $cust_contact->$_($link_hash{$_}) for keys %link_hash;
+ $error = $cust_contact->replace;
+ } else {
+ $cust_contact = new FS::cust_contact { %hash, %link_hash };
+ $error = $cust_contact->insert;
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ if ( $prospectnum ) {
+ my %hash = ( 'contactnum' => $self->contactnum,
+ 'prospectnum' => $prospectnum,
+ );
+ my $error;
+ if ( my $prospect_contact = qsearchs('prospect_contact', \%hash ) ) {
+ $prospect_contact->$_($link_hash{$_}) for keys %link_hash;
+ $error = $prospect_contact->replace;
+ } else {
+ my $prospect_contact = new FS::prospect_contact { %hash, %link_hash };
+ $error = $prospect_contact->insert;
+ }
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ foreach my $pf ( grep { /^phonetypenum(\d+)$/ }
keys %{ $self->hashref } ) {
$pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
my $phonetypenum = $1;
my %cp = ( 'contactnum' => $self->contactnum,
'phonetypenum' => $phonetypenum,
);
- my $contact_phone = qsearchs('contact_phone', \%cp)
- || new FS::contact_phone \%cp;
+ my $contact_phone = qsearchs('contact_phone', \%cp);
+
+ my $pv = $self->get($pf);
+ $pv =~ s/\s//g;
+
+ #if new value is empty, delete old entry
+ if (!$pv) {
+ if ($contact_phone) {
+ $error = $contact_phone->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ next;
+ }
- my %cpd = _parse_phonestring( $self->get($pf) );
+ $contact_phone ||= new FS::contact_phone \%cp;
+
+ my %cpd = _parse_phonestring( $pv );
$contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
}
}
- if ( ( $old->selfservice_access eq '' && $self->selfservice_access
- && ! $self->_password
- )
- || $self->_resend()
- )
+ if ( $cust_contact and (
+ ( $cust_contact->selfservice_access eq ''
+ && $link_hash{selfservice_access}
+ && ! length($self->_password)
+ )
+ || $cust_contact->_resend()
+ )
+ )
{
my $error = $self->send_reset_email( queue=>1 );
if ( $error ) {
;
return $error if $error;
- return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
return "Prospect and customer!" if $self->prospectnum && $self->custnum;
return "One of first name, last name, or title must have a value"
$self->first . ' ' . $self->last;
}
-=item contact_classname
-
-Returns the name of this contact's class (see L<FS::contact_class>).
-
-=cut
-
-sub contact_classname {
- my $self = shift;
- my $contact_class = $self->contact_class or return '';
- $contact_class->classname;
-}
+#=item contact_classname PROSPECT_OBJ | CUST_MAIN_OBJ
+#
+#Returns the name of this contact's class for the specified prospect or
+#customer (see L<FS::prospect_contact>, L<FS::cust_contact> and
+#L<FS::contact_class>).
+#
+#=cut
+#
+#sub contact_classname {
+# my( $self, $prospect_or_cust ) = @_;
+#
+# my $link = '';
+# if ( ref($prospect_or_cust) eq 'FS::prospect_main' ) {
+# $link = qsearchs('prospect_contact', {
+# 'contactnum' => $self->contactnum,
+# 'prospectnum' => $prospect_or_cust->prospectnum,
+# });
+# } elsif ( ref($prospect_or_cust) eq 'FS::cust_main' ) {
+# $link = qsearchs('cust_contact', {
+# 'contactnum' => $self->contactnum,
+# 'custnum' => $prospect_or_cust->custnum,
+# });
+# } else {
+# croak "$prospect_or_cust is not an FS::prospect_main or FS::cust_main object";
+# }
+#
+# my $contact_class = $link->contact_class or return '';
+# $contact_class->classname;
+#}
=item by_selfservice_email EMAILADDRESS
'table' => 'contact_email',
'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
'hashref' => { 'emailaddress' => $email, },
- 'extra_sql' => " AND selfservice_access = 'Y' ".
- " AND ( disabled IS NULL OR disabled = '' )",
+ 'extra_sql' => " AND ( disabled IS NULL OR disabled = '' )",
}) or return '';
$contact_email->contact;
my $conf = new FS::Conf;
- my $cust_main = $self->cust_main
- or die "no customer"; #reset a password for a prospect contact? someday
+ my $cust_main = '';
+ my @cust_contact = grep $_->selfservice_access, $self->cust_contact;
+ $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
- my $msgnum = $conf->config('selfservice-password_reset_msgnum', $cust_main->agentnum);
+ my $agentnum = $cust_main ? $cust_main->agentnum : '';
+ my $msgnum = $conf->config('selfservice-password_reset_msgnum', $agentnum);
#die "selfservice-password_reset_msgnum unset" unless $msgnum;
return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum;
my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
my $queue = new FS::queue {
'job' => 'FS::Misc::process_send_email',
- 'custnum' => $cust_main->custnum,
+ 'custnum' => $cust_main ? $cust_main->custnum : '',
};
$queue->insert( $msg_template->prepare( %msg_template ) );
}
-use FS::phone_type;
+use FS::upgrade_journal;
+sub _upgrade_data { #class method
+ my ($class, %opts) = @_;
+
+ unless ( FS::upgrade_journal->is_done('contact__DUPEMAIL') ) {
+
+ foreach my $contact (qsearch('contact', {})) {
+ my $error = $contact->replace;
+ die $error if $error;
+ }
+
+ FS::upgrade_journal->set_done('contact__DUPEMAIL');
+ }
+
+}
=back
if ( lc($opt{'format'}) eq 'billco' ) {
my $lineseq = 0;
- foreach my $item ( $self->_items_pkg ) {
+ my %items_opt = ( format => 'template',
+ escape_function => sub { shift } );
+ # I don't know what characters billco actually tolerates in spool entries.
+ # Text::CSV will take care of delimiters, though.
+
+ my @items = ( $self->_items_pkg(%items_opt),
+ $self->_items_fee(%items_opt) );
+ foreach my $item (@items) {
my $description = $item->{'description'};
if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
|| $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg', 'billpkgnum' )
|| $self->ut_number('taxnum') #cust_bill_pkg/tax_rate key, based on taxtype
|| $self->ut_enum('taxtype', [ qw( FS::cust_main_county FS::tax_rate ) ] )
- || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum' )
+ || $self->ut_number('pkgnum', 'cust_pkg', 'pkgnum' )
|| $self->ut_foreign_key('locationnum', 'cust_location', 'locationnum' )
|| $self->ut_money('amount')
|| $self->ut_foreign_key('taxable_billpkgnum', 'cust_bill_pkg', 'billpkgnum')
--- /dev/null
+package FS::cust_contact;
+use base qw( FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::cust_contact - Object methods for cust_contact records
+
+=head1 SYNOPSIS
+
+ use FS::cust_contact;
+
+ $record = new FS::cust_contact \%hash;
+ $record = new FS::cust_contact { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_contact object represents a contact's attachment to a specific
+customer. FS::cust_contact inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item custcontactnum
+
+primary key
+
+=item custnum
+
+custnum
+
+=item contactnum
+
+contactnum
+
+=item classnum
+
+classnum
+
+=item comment
+
+comment
+
+=item selfservice_access
+
+empty or Y
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'cust_contact'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ if ( $self->selfservice_access eq 'R' ) {
+ $self->selfservice_access('Y');
+ $self->_resend('Y');
+ }
+
+ my $error =
+ $self->ut_numbern('custcontactnum')
+ || $self->ut_number('custnum')
+ || $self->ut_number('contactnum')
+ || $self->ut_numbern('classnum')
+ || $self->ut_textn('comment')
+ || $self->ut_enum('selfservice_access', [ '', 'Y' ])
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item contact_classname
+
+Returns the name of this contact's class (see L<FS::contact_class>).
+
+=cut
+
+sub contact_classname {
+ my $self = shift;
+ my $contact_class = $self->contact_class or return '';
+ $contact_class->classname;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::contact>, L<FS::cust_main>, L<FS::Record>
+
+=cut
+
+1;
+
use FS::banned_pay;
use FS::cust_main_note;
use FS::cust_attachment;
-use FS::contact;
+use FS::cust_contact;
use FS::Locales;
use FS::upgrade_journal;
use FS::sales;
return $error;
}
- my @contact = $prospect_main->contact;
+ foreach my $prospect_contact ( $prospect_main->prospect_contact ) {
+ my $cust_contact = new FS::cust_contact {
+ 'custnum' => $self->custnum,
+ map { $_ => $prospect_contact->$_() } qw( contactnum classnum comment )
+ };
+ my $error = $cust_contact->insert
+ || $prospect_contact->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
my @cust_location = $prospect_main->cust_location;
my @qual = $prospect_main->qual;
- foreach my $r ( @contact, @cust_location, @qual ) {
+ foreach my $r ( @cust_location, @qual ) {
$r->prospectnum('');
$r->custnum($self->custnum);
my $error = $r->replace;
=item cust_contact
-Returns all contacts (see L<FS::contact>) for this customer.
+Returns all contact associations (see L<FS::cust_contact>) for this customer.
=cut
-#already used :/ sub contact {
sub cust_contact {
my $self = shift;
- qsearch('contact', { 'custnum' => $self->custnum } );
+ qsearch('cust_contact', { 'custnum' => $self->custnum } );
}
=item cust_payby
my $classnum = $self->scalar_sql(
'SELECT classnum FROM contact_class WHERE classname = \'Service\''
) || 0; #if it's zero, qsearchs will return nothing
- $self->{service_contact} = qsearchs('contact', {
- 'classnum' => $classnum, 'custnum' => $self->custnum
- }) || undef;
+ my $cust_contact = qsearchs('cust_contact', {
+ 'classnum' => $classnum,
+ 'custnum' => $self->custnum,
+ });
+ $self->{service_contact} = $cust_contact->contact if $cust_contact;
}
$self->{service_contact};
}
}
+sub process_o2m_qsearch {
+ my $self = shift;
+ my $table = shift;
+ return qsearch($table, @_) unless $table eq 'contact';
+
+ my $hashref = shift;
+ my %hash = %$hashref;
+ ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
+ or die 'guru meditation #4343';
+
+ qsearch({ 'table' => 'contact',
+ 'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
+ 'hashref' => \%hash,
+ 'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
+ " cust_contact.custnum = $custnum "
+ });
+}
+
+sub process_o2m_qsearchs {
+ my $self = shift;
+ my $table = shift;
+ return qsearchs($table, @_) unless $table eq 'contact';
+
+ my $hashref = shift;
+ my %hash = %$hashref;
+ ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
+ or die 'guru meditation #2121';
+
+ qsearchs({ 'table' => 'contact',
+ 'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
+ 'hashref' => \%hash,
+ 'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
+ " cust_contact.custnum = $custnum "
+ });
+}
+
=item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
Subroutine (not a method), designed to be called from the queue.
sub API_update {
- my( $class, %opt ) = @_;
+ my( $class, %opt ) = @_;
my $conf = new FS::Conf;
-
my $custnum = $opt{'custnum'}
or return { 'error' => "no customer record" };
payby payinfo paydate paycvv payname
),
-
- my @invoicing_list = $opt{'invoicing_list'}
- ? split( /\s*\,\s*/, $opt{'invoicing_list'} )
- : ();
- push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
-
- my ($bill_hash, $ship_hash);
- foreach my $f (FS::cust_main->location_fields) {
- # avoid having to change this in front-end code
- $bill_hash->{$f} = $opt{"bill_$f"} || $opt{$f};
- $ship_hash->{$f} = $opt{"ship_$f"};
+ my @invoicing_list;
+ if ( exists $opt{'invoicing_list'} || exists $opt{'postal_invoicing'} ) {
+ @invoicing_list = split( /\s*\,\s*/, $opt{'invoicing_list'} );
+ push @invoicing_list, 'POST' if $opt{'postal_invoicing'};
+ } else {
+ @invoicing_list = $cust_main->invoicing_list;
}
- my $bill_location = FS::cust_location->new($bill_hash);
- my $ship_location;
- # we don't have an equivalent of the "same" checkbox in selfservice^Wthis API
- # so is there a ship address, and if so, is it different from the billing
- # address?
- if ( length($ship_hash->{address1}) > 0 and
- grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
- ) {
+ if ( exists( $opt{'address1'} ) ) {
+ my $bill_location = FS::cust_location->new({
+ map { $_ => $opt{$_} } @location_editable_fields
+ });
+ $bill_location->set('custnum' => $custnum);
+ my $error = $bill_location->find_or_insert;
+ die $error if $error;
- $ship_location = FS::cust_location->new( $ship_hash );
-
- } else {
- $ship_location = $bill_location;
+ # if this is unchanged from before, cust_main::replace will ignore it
+ $new->set('bill_location' => $bill_location);
}
- $new->set('bill_location' => $bill_location);
- $new->set('ship_location' => $ship_location);
+ if ( exists($opt{'ship_address1'}) && length($opt{"ship_address1"}) > 0 ) {
+ my $ship_location = FS::cust_location->new({
+ map { $_ => $opt{"ship_$_"} } @location_editable_fields
+ });
+
+ $ship_location->set('custnum' => $custnum);
+ my $error = $ship_location->find_or_insert;
+ die $error if $error;
+
+ $new->set('ship_location' => $ship_location);
+
+ } elsif (exists($opt{'ship_address1'} ) && !grep { length($opt{"ship_$_"}) } @location_editable_fields ) {
+ my $ship_location = $new->bill_location;
+ $new->set('ship_location' => $ship_location);
+ }
my $error = $new->replace( $cust_main, \@invoicing_list );
return { 'error' => $error } if $error;
-
+
return { 'error' => '',
- };
-
+ };
}
1;
package FS::cust_pkg_discount;
-use base qw( FS::otaker_Mixin FS::cust_main_Mixin FS::Record );
+use base qw( FS::otaker_Mixin
+ FS::cust_main_Mixin
+ FS::pkg_discount_Mixin
+ FS::Record );
use strict;
use FS::Record qw( dbh ); # qsearch qsearchs dbh );
Adds this record to the database. If there is an error, returns the error,
otherwise returns false.
-=cut
-
-sub insert {
- #my( $self, %options ) = @_;
- my $self = 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;
-
- if ( $self->discountnum == -1 ) {
- my $discount = new FS::discount {
- '_type' => $self->_type,
- 'amount' => $self->amount,
- 'percent' => $self->percent,
- 'months' => $self->months,
- 'setup' => $self->setup,
- #'linked' => $self->linked,
- 'disabled' => 'Y',
- };
- my $error = $discount->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
- $self->discountnum($discount->discountnum);
- }
-
- my $error = $self->SUPER::insert; #(@_); #(%options);
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
- '';
-
-}
-
=item delete
Delete this record from the database.
sub prepare {
my( $self, %opt ) = @_;
- my $cust_main = $opt{'cust_main'} or die 'cust_main required';
+ my $cust_main = $opt{'cust_main'}; # or die 'cust_main required';
my $object = $opt{'object'} or die 'object required';
# localization
- my $locale = $cust_main->locale || '';
+ my $locale = $cust_main && $cust_main->locale || '';
warn "no locale for cust#".$cust_main->custnum."; using default content\n"
- if $DEBUG and !$locale;
- my $content = $self->content($cust_main->locale);
- warn "preparing template '".$self->msgname."' to cust#".$cust_main->custnum."\n"
- if($DEBUG);
+ if $DEBUG and $cust_main && !$locale;
+ my $content = $self->content($locale);
+
+ warn "preparing template '".$self->msgname."\n"
+ if $DEBUG;
my $subs = $self->substitutions;
# create substitution table
###
my %hash;
- my @objects = ($cust_main);
+ my @objects = ();
+ push @objects, $cust_main if $cust_main;
my @prefixes = ('');
my $svc;
if( ref $object ) {
my @to;
if ( exists($opt{'to'}) ) {
@to = split(/\s*,\s*/, $opt{'to'});
- }
- else {
+ } elsif ( $cust_main ) {
@to = $cust_main->invoicing_list_emailonly;
+ } else {
+ die 'no To: address or cust_main object specified';
}
- # no warning when preparing with no destination
my $from_addr = $self->from_addr;
if ( !$from_addr ) {
+
+ my $agentnum = $cust_main ? $cust_main->agentnum : '';
+
if ( $opt{'from_config'} ) {
- $from_addr = scalar( $conf->config($opt{'from_config'},
- $cust_main->agentnum) );
+ $from_addr = $conf->config($opt{'from_config'}, $agentnum);
}
- $from_addr ||= $conf->invoice_from_full($cust_main->agentnum);
+ $from_addr ||= $conf->invoice_from_full($agentnum);
}
# my @cust_msg = ();
# if ( $conf->exists('log_sent_mail') and !$opt{'preview'} ) {
->format( HTML::TreeBuilder->new_from_content($body) )
);
(
- 'custnum' => $cust_main->custnum,
- 'msgnum' => $self->msgnum,
- 'from' => $from_addr,
- 'to' => \@to,
- 'bcc' => $self->bcc_addr || undef,
+ 'custnum' => ( $cust_main ? $cust_main->custnum : ''),
+ 'msgnum' => $self->msgnum,
+ 'from' => $from_addr,
+ 'to' => \@to,
+ 'bcc' => $self->bcc_addr || undef,
'subject' => $subject,
'html_body' => $body,
'text_body' => $text_body
foreach my $del_obj (
grep { ! $edits{$_->$table_pkey()} }
- qsearch( $table, $hashref )
+ $self->process_o2m_qsearch( $table, $hashref )
) {
my $error = $del_obj->delete;
if ( $error ) {
}
foreach my $pkey_value ( keys %edits ) {
- my $old_obj = qsearchs( $table, { %$hashref, $table_pkey => $pkey_value } ),
+ my $old_obj = $self->process_o2m_qsearchs( $table, { %$hashref, $table_pkey => $pkey_value } );
my $add_param = $edits{$pkey_value};
my %hash = ( $table_pkey => $pkey_value,
map { $_ => $opt{'params'}->{$add_param."_$_"} }
'';
}
+sub process_o2m_qsearch { my $self = shift; qsearch( @_ ); }
+sub process_o2m_qsearchs { my $self = shift; qsearchs( @_ ); }
+
sub _load_table {
my( $self, $table ) = @_;
eval "use FS::$table";
--- /dev/null
+package FS::part_event::Action::pkg_discount;
+
+use strict;
+use base qw( FS::part_event::Action );
+
+sub description { "Discount active customer packages"; }
+
+sub eventtable_hashref {
+ { 'cust_main' => 1 };
+}
+
+sub event_stage { 'pre-bill'; }
+
+sub option_fields {
+ (
+ 'if_pkgpart' => { 'label' => 'Only packages',
+ 'type' => 'select-table',
+ 'table' => 'part_pkg',
+ 'name_col' => 'pkg',
+ #can tweak after fixing discount bug with non-monthly recurring pkgs
+ 'extra_sql' => q(AND freq NOT LIKE '0%' AND freq NOT LIKE '%d' AND freq NOT LIKE '%h' AND freq NOT LIKE '%w'),
+ 'multiple' => 1,
+ },
+ 'discountnum' => { 'label' => 'Discount',
+ 'type' => 'select-table', #we don't handle the select-discount create a discount case
+ 'table' => 'discount',
+ 'name_col' => 'description', #well, method
+ 'order_by' => 'ORDER BY discountnum', #requied because name_col is a method
+ 'hashref' => { 'disabled' => '',
+ 'months' => { op=>'!=', value=>'0' },
+ },
+ 'disable_empty' => 1,
+ },
+ );
+}
+
+#lots of false laziness with referral_pkg_discount
+#but also lots of doing it differently...and better???
+sub do_action {
+ my( $self, $object, $cust_event ) = @_;
+
+ my $cust_main = $self->cust_main($object);
+ my %if_pkgpart = map { $_=>1 } split(/\s*,\s*/, $self->option('if_pkgpart') );
+ my @cust_pkg = grep { $if_pkgpart{ $_->pkgpart } && $_->part_pkg->freq
+ #can remove after fixing discount bug with non-monthly pkgs
+ && ( $_->part_pkg->freq =~ /^\d+$/) }
+ $cust_main->active_pkgs;
+ return 'No qualifying packages' unless @cust_pkg;
+
+ my $gotit = 0;
+ foreach my $cust_pkg (@cust_pkg) {
+
+ my @cust_pkg_discount = $cust_pkg->cust_pkg_discount_active;
+
+ #our logic here only makes sense insomuch as you can't have multiple discounts
+ die "Unexpected multiple discounts, contact developers"
+ if scalar(@cust_pkg_discount) > 1;
+
+ my @my_cust_pkg_discount =
+ grep { $_->discountnum == $self->option('discountnum') } @cust_pkg_discount;
+
+ if ( @my_cust_pkg_discount ) { #reset the existing one instead
+
+ $gotit = 1;
+
+ #it's already got this discount and discount never expires--great, move on
+ next unless $cust_pkg_discount[0]->discount->months;
+
+ #reset the discount
+ my $error = $cust_pkg_discount[0]->decrement_months_used( $cust_pkg_discount[0]->months_used );
+ die "Error extending discount: $error\n" if $error;
+
+ } elsif ( @cust_pkg_discount ) {
+
+ #can't currently discount an already discounted package,
+ #but maybe we can discount a different package
+ next;
+
+ } else { #normal case, create a new one
+
+ $gotit = 1;
+ my $cust_pkg_discount = new FS::cust_pkg_discount {
+ 'pkgnum' => $cust_pkg->pkgnum,
+ 'discountnum' => $self->option('discountnum'),
+ 'months_used' => 0
+ };
+ my $error = $cust_pkg_discount->insert;
+ die "Error discounting package: $error\n" if $error;
+
+ }
+ }
+
+ return $gotit ? '' : 'Discount not applied due to existing discounts';
+
+}
+
+1;
my $servicenums =
$class->condition_sql_option_option_integer('has_service');
- my $sql = qq| 0 < ( SELECT COUNT(cs.svcpart)
+ my $sql = " 0 < ( SELECT COUNT(cs.svcpart)
FROM cust_bill_pkg cbp, cust_svc cs
WHERE cbp.invnum = cust_bill.invnum
AND cs.pkgnum = cbp.pkgnum
AND cs.svcpart IN $servicenums
)
- |;
+ ";
return $sql;
}
};
}
-#something like this
sub option_fields {
(
'tagnum' => { 'label' => 'Customer tag',
my($self, $object, %opt) = @_;
my $cust_main = $self->cust_main($object);
+ return 0 unless $cust_main; #sanity check
+ return 0 unless $cust_main->referral_custnum;
+
+ my $referring_cust_main = $cust_main->referral_custnum_cust_main;
+ return 0 unless $referring_cust_main; #sanity check;
+
+ #referring customer must sign up before referred customer
+ return 0 unless $cust_main->signupdate > $referring_cust_main->signupdate;
if ( $self->option('active') ) {
- return 0 unless $cust_main->referral_custnum;
#check for no cust_main for referral_custnum? (deleted?)
- return 0 unless $cust_main->referral_custnum_cust_main->status eq 'active';
- } else {
- return 0 unless $cust_main->referral_custnum; # ? 1 : 0;
+ return 0 unless $referring_cust_main->status eq 'active';
}
return 1 unless $self->option('check_bal');
- my $referring_cust_main = $cust_main->referral_custnum_cust_main;
-
#false laziness w/ balance_age_under
my $under = $self->option('balance');
$under = 0 unless length($under);
package FS::part_event::Condition::has_referral_pkgpart;
use base qw( FS::part_event::Condition );
+use FS::part_event::Condition::has_referral_custnum;
#maybe i should be incorporated in has_referral_custnum
use strict;
sub condition {
my($self, $object, %opt) = @_;
+ return 0 unless FS::part_event::Condition::has_referral_custnum::condition($self, $object, %opt);
+
my $cust_main = $self->cust_main($object);
- return 0 unless $cust_main->referral_custnum;
-
my $if_pkgpart = $self->option('if_pkgpart') || {};
grep $if_pkgpart->{ $_->pkgpart },
$cust_main->referral_custnum_cust_main->ncancelled_pkgs;
--- /dev/null
+package FS::part_event::Condition::nopostal;
+use base qw( FS::part_event::Condition );
+use strict;
+
+sub description {
+ 'Customer does not receive a postal mail invoice';
+}
+
+sub condition {
+ my( $self, $object ) = @_;
+ my $cust_main = $self->cust_main($object);
+
+ scalar( grep { $_ eq 'POST' } $cust_main->invoicing_list ) ? 0 : 1;
+}
+
+sub condition_sql {
+ my( $self, $table ) = @_;
+
+ " NOT EXISTS( SELECT 1 FROM cust_main_invoice
+ WHERE cust_main_invoice.custnum = cust_main.custnum
+ AND cust_main_invoice.dest = 'POST'
+ )
+ ";
+}
+
+1;
use FS::svc_external;
tie my %options, 'Tie::IxHash',
- 'access_key' => { label => 'AWS access key', },
- 'secret_key' => { label => 'AWS secret key', },
- 'ami' => { label => 'AMI', 'default' => 'ami-ff46a796', },
- 'keyname' => { label => 'Keypair name', },
+ 'access_key' => { label => 'AWS access key', },
+ 'secret_key' => { label => 'AWS secret key', },
+ 'ami' => { label => 'AMI', 'default' => 'ami-ff46a796', },
+ 'keyname' => { label => 'Keypair name', },
+ 'region' => { label => 'Region', },
+ 'InstanceType' => { label => 'Instance Type', },
#option to turn off (or on) ip address allocation
;
$svc_external->svcnum,
$self->option('ami'),
$self->option('keyname'),
+ $self->option('InstanceType'),
);
ref($err_or_queue) ? '' : $err_or_queue;
}
};
$queue->insert( $self->option('access_key'),
$self->option('secret_key'),
+ $self->option('region'),
@_
)
or $queue;
}
sub amazon_ec2_new {
- my( $access_key, $secret_key, @rest ) = @_;
+ my( $access_key, $secret_key, $region, @rest ) = @_;
eval 'use Net::Amazon::EC2;';
die $@ if $@;
my $ec2 = new Net::Amazon::EC2 'AWSAccessKeyId' => $access_key,
- 'SecretAccessKey' => $secret_key;
-
+ 'SecretAccessKey' => $secret_key,
+ 'region' => $region || 'us-east-1',
+ ;
( $ec2, @rest );
}
sub amazon_ec2_insert { #subroutine, not method
- my( $ec2, $svcnum, $ami, $keyname ) = amazon_ec2_new(@_);
-
- my $reservation_info = $ec2->run_instances( 'ImageId' => $ami,
- 'KeyName' => $keyname,
- 'MinCount' => 1,
- 'MaxCount' => 1,
- );
+ my( $ec2, $svcnum, $ami, $keyname, $InstanceType ) = amazon_ec2_new(@_);
+
+ my $reservation_info = $ec2->run_instances(
+ 'ImageId' => $ami,
+ 'KeyName' => $keyname,
+ 'InstanceType' => $InstanceType || 'm1.small',
+ 'MinCount' => 1,
+ 'MaxCount' => 1,
+ );
my $instance_id = $reservation_info->instances_set->[0]->instance_id;
my $ssh = Net::OpenSSH->new( $self->machine,
default_stdin_fh => $def_in );
+ #capture2 and return STDERR, its probably useful if there's a problem
my $private_key = $ssh->capture(
{ 'stdin_data' => $svc_acct->_password. "\n" },
'/usr/local/bin/merchant_create', map $svc_acct->$_, qw( username finger )
my $ssh = Net::OpenSSH->new( $self->machine,
default_stdin_fh => $def_in );
+ #capture2 and return STDERR, its probably useful if there's a problem
my $unused_output = $ssh->capture(
'/usr/local/bin/merchant_disable', map $svc_acct->$_, qw( username )
);
use strict;
use base qw( FS::o2m_Common FS::Record );
-use vars qw( $DEBUG );
use FS::Record qw( qsearch qsearchs );
use FS::cust_bill_pkg_display;
-$DEBUG = 0;
+our $DEBUG = 0;
+our $default_class;
=head1 NAME
=item disabled - 'Y' if the fee is disabled
=item classnum - the L<FS::pkg_class> that the fee belongs to, for reporting
+and placement on multisection invoices. Unlike packages, fees I<must> be
+assigned to a class; they will default to class named "Fees", which belongs
+to the same invoice section that normally contains taxes.
=item taxable - 'Y' if this fee should be considered a taxable sale.
Currently, taxable fees will be treated like they exist at the customer's
$self->set('amount', 0) unless $self->amount;
$self->set('percent', 0) unless $self->percent;
+ $default_class ||= qsearchs('pkg_class', { classname => 'Fees' })
+ or die "default package fee class not found; run freeside-upgrade to continue.\n";
+
+ if (!$self->get('classnum')) {
+ $self->set('classnum', $default_class->classnum);
+ }
+
my $error =
$self->ut_numbern('feepart')
|| $self->ut_textn('comment')
=item select_label - Used with select_table, this is the field name of labels
+=item select_allow_empty - Used with select_table, adds an empty option
+
=back
=cut
my $sth = dbh->prepare(
'UPDATE phone_avail SET svcnum = NULL
WHERE svcnum IS NOT NULL
- AND 0 = ( SELECT COUNT(*) FROM svc_phone
- WHERE phone_avail.svcnum = svc_phone.svcnum )'
+ AND NOT EXISTS ( SELECT 1 FROM svc_phone
+ WHERE phone_avail.svcnum = svc_phone.svcnum )'
) or die dbh->errstr;
$sth->execute or die $sth->errstr;
use strict;
use vars qw( @ISA $me $DEBUG );
-use FS::Record qw( qsearch );
+use FS::Record qw( qsearch qsearchs );
use FS::pkg_class;
use FS::part_pkg;
$weight += 10;
}
}
+
+ # create default category for package fees
+ my $tax_category_name = 'Taxes, Surcharges, and Fees';
+ my $tax_category = qsearchs('pkg_category',
+ { categoryname => $tax_category_name }
+ );
+ if (!$tax_category) {
+ $tax_category = FS::pkg_category->new({
+ categoryname => $tax_category_name,
+ weight => 1000, # doesn't really matter
+ });
+ my $error = $tax_category->insert;
+ die "error creating tax category: $error\n" if $error;
+ }
+
+ my $fee_class_name = 'Fees'; # does not appear on invoice
+ my $fee_class = qsearchs('pkg_class', { classname => $fee_class_name });
+ if (!$fee_class) {
+ $fee_class = FS::pkg_class->new({
+ classname => $fee_class_name,
+ categorynum => $tax_category->categorynum,
+ });
+ my $error = $fee_class->insert;
+ die "error creating fee class: $error\n" if $error;
+ }
+
+ # assign it to all fee defs that don't otherwise have a class
+ foreach my $part_fee (qsearch('part_fee', { classnum => '' })) {
+ $part_fee->set('classnum', $fee_class->classnum);
+ my $error = $part_fee->replace;
+ die "error assigning default class to fee def#".$part_fee->feepart .
+ ":$error\n" if $error;
+ }
+
'';
}
--- /dev/null
+package FS::pkg_discount_Mixin;
+
+use strict;
+use NEXT;
+use FS::Record qw(dbh);
+
+=head1 NAME
+
+FS::pkg_discount_Mixin - mixin class for package-discount link objects.
+
+=head1 DESCRIPTION
+
+Implements some behavior that's common to cust_pkg_discount and
+quotation_pkg_discount objects. The only required field is "discountnum",
+a foreign key to L<FS::discount>.
+
+=head1 METHODS
+
+=over 4
+
+=item insert
+
+Inserts the record. If the 'discountnum' field is -1, this will first create
+a discount using the contents of the '_type', 'amount', 'percent', 'months',
+and 'setup' field. The new discount will be disabled, since it's a one-off
+discount.
+
+=cut
+
+sub insert {
+ my $self = shift;
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ if ( $self->discountnum == -1 ) {
+ my $discount = new FS::discount {
+ '_type' => $self->_type,
+ 'amount' => $self->amount,
+ 'percent' => $self->percent,
+ 'months' => $self->months,
+ 'setup' => $self->setup,
+ #'linked' => $self->linked,
+ 'disabled' => 'Y',
+ };
+ my $error = $discount->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $self->set('discountnum', $discount->discountnum);
+ }
+
+ my $error = $self->NEXT::insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=back
+
+=cut
+
+1;
--- /dev/null
+package FS::prospect_contact;
+use base qw( FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::prospect_contact - Object methods for prospect_contact records
+
+=head1 SYNOPSIS
+
+ use FS::prospect_contact;
+
+ $record = new FS::prospect_contact \%hash;
+ $record = new FS::prospect_contact { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::prospect_contact object represents a contact's attachment to a specific
+prospect. FS::prospect_contact inherits from FS::Record. The following fields
+are currently supported:
+
+=over 4
+
+=item prospectcontactnum
+
+primary key
+
+=item prospectnum
+
+prospectnum
+
+=item contactnum
+
+contactnum
+
+=item classnum
+
+classnum
+
+=item comment
+
+comment
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'prospect_contact'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('prospectcontactnum')
+ || $self->ut_number('prospectnum')
+ || $self->ut_number('contactnum')
+ || $self->ut_numbern('classnum')
+ || $self->ut_textn('comment')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::contact>, L<FS::prospect_main>, L<FS::Record>
+
+=cut
+
+1;
+
my $self = shift;
return $self->company if $self->company;
- my $contact = ($self->contact)[0]; #first contact? good enough for now
+ my $contact = ($self->prospect_contact)[0]->contact; #first contact? good enough for now
return $contact->line if $contact;
'Prospect #'. $self->prospectnum;
my @cust_location = $self->cust_location;
#the interface only allows one, so we're just gonna go with that for now
- my @contact = $self->contact;
+ my @contact = map $_->contact, $self->prospect_contact;
#XXX define one contact type as "billing", then we could pick just that one
my @invoicing_list = map $_->emailaddress, map $_->contact_email, @contact;
sub order {
my $self = shift;
- tie my %cust_pkg, 'Tie::RefHash',
- map { FS::cust_pkg->new({ pkgpart => $_->pkgpart,
- quantity => $_->quantity,
- })
- => [] #services
- }
- $self->quotation_pkg ;
-
- $self->cust_main->order_pkgs( \%cust_pkg );
+ tie my %all_cust_pkg, 'Tie::RefHash';
+ foreach my $quotation_pkg ($self->quotation_pkg) {
+ my $cust_pkg = FS::cust_pkg->new;
+ foreach (qw(pkgpart locationnum start_date contract_end quantity waive_setup)) {
+ $cust_pkg->set( $_, $quotation_pkg->get($_) );
+ }
+
+ # currently only one discount each
+ my ($pkg_discount) = $quotation_pkg->quotation_pkg_discount;
+ if ( $pkg_discount ) {
+ $cust_pkg->set('discountnum', $pkg_discount->discountnum);
+ }
+
+ $all_cust_pkg{$cust_pkg} = []; # no services
+ }
+
+ $self->cust_main->order_pkgs( \%all_cust_pkg );
}
=cut
+use Data::Dumper; #XXX DEBUG
sub insert {
my ($self, %options) = @_;
+ warn Dumper($self);
+ warn Dumper(\%options);
my $dbh = dbh;
my $oldAutoCommit = $FS::UID::AutoCommit;
# XXX the order of applying discounts is ill-defined, which matters
# if there are percentage and amount discounts on the same package.
+ #
+ # but right now there can only be one discount on any package, so
+ # it doesn't matter
foreach my $pkg_discount ($self->quotation_pkg_discount) {
my $discount = $pkg_discount->discount;
package FS::quotation_pkg_discount;
-use base qw( FS::Record );
+
+use base qw( FS::pkg_discount_Mixin FS::Record );
use FS::Maketext 'mt'; # XXX not really correct
use strict;
Adds this record to the database. If there is an error, returns the error,
otherwise returns false.
-=cut
-
-# the insert method can be inherited from FS::Record
-
=item delete
Delete this record from the database.
-=cut
-
-# the delete method can be inherited from FS::Record
-
=item replace OLD_RECORD
Replaces the OLD_RECORD with this one in the database. If there is an error,
returns the error, otherwise returns false.
-=cut
-
-# the replace method can be inherited from FS::Record
-
=item check
Checks all fields to make sure this is a valid quotation package discount.
select_table => 'svc_domain',
select_key => 'svcnum',
select_label => 'domain',
+ select_allow_empty => 1,
disable_inventory => 1,
},
'circuit_svcnum' => { label => 'Circuit',
t/svc_circuit.t
FS/cust_credit_source_bill_pkg.pm
t/cust_credit_source_bill_pkg.t
+FS/prospect_contact.pm
+t/prospect_contact.t
+FS/cust_contact.pm
+t/cust_contact.t
+FS/pkg_discount_Mixin.pm
+t/pkg_discount_Mixin.t
sub _shouldrun {
my $extra_sql =
- ' AND 0 < ( SELECT COUNT(*) FROM cust_pkg
- WHERE cust_pkg.pkgpart = part_pkg.pkgpart
- AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
- )
+ ' AND EXISTS ( SELECT 1 FROM cust_pkg
+ WHERE cust_pkg.pkgpart = part_pkg.pkgpart
+ AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
+ )
';
my @part_pkg =
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_contact;
+$loaded=1;
+print "ok 1\n";
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::pkg_discount_Mixin;
+$loaded=1;
+print "ok 1\n";
--- /dev/null
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::prospect_contact;
+$loaded=1;
+print "ok 1\n";
my $cur_cr = 0;
$cur_cr += $_->amount foreach $cust_bill->cust_credited;
$cur_cr = '' if $cur_cr == 0;
+
+ next if $cur_cr > 0 && $opt_k;
+
if ( $opt_p ) {
#print $cust_bill->invnum. ','. $cust_bill->custnum. ",$tax,$credit,$cr_percent%\n";
+# print $cust_bill->invnum. ','. $cust_bill->custnum. ',"'.
+# $cust_bill->cust_main->name. '",'. "$tax,$credit,$cur_cr\n";
print $cust_bill->invnum. ','. $cust_bill->custnum. ',"'.
- $cust_bill->cust_main->name. '",'. "$tax,$credit,$cur_cr\n";
+ $cust_bill->cust_main->name. '",'. "$tax,$credit\n";
}
- next if $cur_cr > 0 && $opt_k;
-
-#COMMENTING OUT ALL DANGEROUS STUFF
-#
# if ( $opt_m && ! $opt_r ) {
#
# my $msg_template = qsearchs('msg_template', { 'msgnum' => $opt_m } )
# " custnum ". $cust_bill->custnum. ": $error\n";
# }
# }
-#
-# if ( $opt_c ) {
-# my $cust_credit = new FS::cust_credit {
-# 'custnum' => $cust_main->custnum,
-# 'amount' => $credit,
-# 'reasonnum' => $opt_c,
-# };
-# my $error = $cust_credit->insert;
-# if ( $error ) {
-# warn "error inserting credit: $error\n";
-# }
-# my $cust_credit_bill = new FS::cust_credit_bill {
-# 'crednum' => $cust_credit->crednum,
-# 'invnum' => $cust_bill->invnum,
-# 'amount' => $credit,
-# };
-# my $aerror = $cust_credit_bill->insert;
-# if ( $aerror ) {
-# warn "error applying credit to invnum ". $cust_bill->invnum. ": $aerror\n";
-# }
-# }
-#
+
+ if ( $opt_c ) {
+ my $cust_credit = new FS::cust_credit {
+ 'custnum' => $cust_main->custnum,
+ 'amount' => $credit,
+ 'reasonnum' => $opt_c,
+ };
+ my $error = $cust_credit->insert;
+ if ( $error ) {
+ warn "error inserting credit: $error\n";
+ }
+ my $cust_credit_bill = new FS::cust_credit_bill {
+ 'crednum' => $cust_credit->crednum,
+ 'invnum' => $cust_bill->invnum,
+ 'amount' => $credit,
+ };
+ my $aerror = $cust_credit_bill->insert;
+ if ( $aerror ) {
+ warn "error applying credit to invnum ". $cust_bill->invnum. ": $aerror\n";
+ }
+ }
+
# if ( $opt_e && ! $opt_r ) {
# eval { $cust_bill->email };
# if ( $@ ) {
&{$section->{description_generator}}($line);
} else {
my $class = 'invoice_desc_more';
- if ( $line->{'ref'} and $line->{'ref'} ne $lastref ) {
+ if ( ($line->{'ref'} || 0) ne $lastref ) {
# then it's a new package (not a continuation)
$class = 'invoice_desc';
}
$OUT .= '<tr class="'.$class.'">
<td align="center">';
- #if ( $line->{'ref'} ne $lastref ) {
- # $OUT .= $line->{'ref'};
- #}
$OUT .= '</td>
<td align="left">'. $line->{'description'}. '</td>';
if ( $unitprices ) {
$OUT .= '<td align="right">'. $line->{'amount'}. '</td>';
}
$OUT .= '</tr>';
- $lastref = $line->{'ref'};
+ $lastref = $line->{'ref'} || 0;
if ( @{$line->{'ext_description'} } ) {
unless ( $section->{description_generator} ) {
$OUT .= '<tr class="invoice_extdesc"><td></td><td';
# Don't break-up small packages.\r
my $rowbreak = @$ext_description < 5 ? '*' : '';\r
\r
- $OUT .= "\\hline\n" if ($line->{'ref'} && $line->{'ref'} ne $lastref);\r
+ $OUT .= "\\hline\n" if (($line->{'ref'} || 0) ne $lastref);\r
if ($section->{description_generator}) {\r
$OUT .= &{$section->{description_generator}}($line);\r
} else {\r
$OUT .= '\FSdesc'.\r
- '{}'. #'{' . ( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ) . '}'.\r
+ '{}'.\r
'{' . $line->{'description'} . '}' ;\r
if ( $unitprices and length($line->{'unit_amount'}) ) {\r
# then show the unit amount and quantity\r
}\r
$OUT .= '{\\dollar' . $line->{'amount'} . "}${rowbreak}\n";\r
}\r
- $lastref = $line->{'ref'};\r
+ $lastref = $line->{'ref'} || 0;\r
\r
foreach my $ext_desc (@$ext_description) {\r
if ($section->{extended_description_generator}) {\r
package FS::table_name;
+use base qw( FS::Record );
use strict;
-use base qw( FS::Record );
use FS::Record qw( qsearch qsearchs );
=head1 NAME
cd ..
#( cd ..; make deploy; cd fs_selfservice )
-( cd ..; make clean; make configure-rt; make install-perl-modules; /etc/init.d/freeside restart; cd fs_selfservice )
+#( cd ..; make clean; make configure-rt; make install-perl-modules; /etc/init.d/freeside restart; cd fs_selfservice )
+( cd ..; make clean; make configure-rt; make install-perl-modules; make deploy; cd fs_selfservice )
#cp /home/ivan/freeside/fs_selfservice/FS-SelfService/cgi/* /var/www/MyAccount
#chown freeside /var/www/MyAccount/*.cgi
'login' => 'MyAccount/login',
'logout' => 'MyAccount/logout',
'switch_acct' => 'MyAccount/switch_acct',
+ 'switch_cust' => 'MyAccount/switch_cust',
'customer_info' => 'MyAccount/customer_info',
'customer_info_short' => 'MyAccount/customer_info_short',
'billing_history' => 'MyAccount/billing_history',
--- /dev/null
+<HTML>
+ <HEAD>
+ <TITLE>Select customer</TITLE>
+ <%= $head %>
+ </HEAD>
+ <BODY BGCOLOR="<%= $body_bgcolor || '#eeeeee' %>">
+ <%= $body_header %>
+
+<FONT SIZE=5>Select customer</FONT><BR><BR>
+<FONT SIZE="+1" COLOR="#ff0000"><%= $error %></FONT>
+
+<%= $selfurl =~ s/\?.*//; ''; %>
+<FORM NAME="SelectCustomerForm" ACTION="<%= $selfurl %>" METHOD=POST>
+<INPUT TYPE="hidden" NAME="action" VALUE="switch_cust">
+<INPUT TYPE="hidden" NAME="agentnum" VALUE="<%= $agentnum %>">
+
+<TABLE BGCOLOR="<%= $box_bgcolor || '#c0c0c0' %>" BORDER=0 CELLSPACING=2 CELLPADDING=0>
+
+ <TR>
+ <TH ALIGN="right">Customer </TH>
+ <TD>
+ <SELECT NAME="custnum" ID="custnum" onChange="custnum_changed()">
+ <OPTION VALUE="">Select a customer
+<%= $OUT .= qq(<OPTION VALUE="$_">). encode_entities( $customers{$_} )
+ foreach keys %customers;
+%>
+ </SELECT>
+ </TD>
+ </TR>
+
+ <TR>
+ <TD COLSPAN=2 ALIGN="center"><INPUT TYPE="submit" ID="submit" VALUE="Select customer" DISABLED></TD>
+ </TR>
+
+</TABLE>
+</FORM>
+
+<SCRIPT TYPE="text/javascript">
+
+function custnum_changed () {
+ var form = document.SelectCustomerForm;
+ if ( form.custnum.selectedIndex > 0 ) {
+ form.submit.disabled = false;
+ } else {
+ form.submit.disabled = true;
+ }
+}
+
+</SCRIPT>
+
+<%= $body_footer %>
process_change_password
customer_suspend_pkg
process_suspend_pkg
+ switch_cust
));
my @nologin_actions = (qw(
# at this point $session_id is a real session
+ if ( ! $login_rv->{'custnum'} && ! $login_rv->{'svcnum'} && $login_rv->{'customers'} ) {
+ #select a customer if we're a multi-contact customer
+ do_template('select_cust', { %$login_rv } );
+ exit;
+ }
+
}
warn "calling $action sub\n"
my $result = eval "&$action();";
die $@ if $@;
+use Data::Dumper;
warn Dumper($result) if $DEBUG;
if ( $result->{error} && ( $result->{error} eq "Can't resume session"
#--
-use Data::Dumper;
+sub switch_cust {
+ $action = 'myaccount';
+ FS::SelfService::switch_cust( 'session_id' => $session_id,
+ 'custnum' => scalar($cgi->param('custnum')),
+ );
+}
+
sub myaccount {
customer_info( 'session_id' => $session_id );
}
'count_query' => 'SELECT COUNT(*) FROM discount',
'disableable' => 1,
'disabled_statuspos' => 1,
- 'header' => [ 'Name', 'Class', 'Discount', ],
+ 'header' => [ 'Name', 'Comment', 'Class', 'Discount', ],
'fields' => [ 'name',
+ 'comment',
'classname',
'description',
],
% unless ( $agentnum ) {
<CENTER>
- <FONT SIZE="-3">"" - R. Hunter</FONT>
+ <FONT SIZE="-3">"Half the world's a desert / Cannibals eat human brains for dessert" - D. Zero</FONT>
</CENTER>
% }
Stephen Bechard<BR>
Eric Bosrup<BR>
Dickie Bradford<BR>
+Alex Brelsfoard<BR>
Dave Burgess<BR>
Joe Camadine<BR>
Chris Cappuccio<BR>
David Peters<BR>
Matt Peterson<BR>
Luke Pfeifer<BR>
+Jonathan Prykop<BR>
Ricardo Signes<BR>
Steve Simitzis<BR>
Stanislav Sinyagin<BR>
{ 'field' => 'contactnum',
'type' => 'contact',
'colspan' => 6,
+ 'custnum' => $custnum,
'm2m_method' => 'cust_contact',
'm2m_dstcol' => 'contactnum',
'm2_label' => ' ', #'Contact',
$cust_main->company( $prospect_main->company );
#first contact? -> name
- my @contacts = $prospect_main->contact;
- my $contact = $contacts[0];
+ my @prospect_contacts = $prospect_main->prospect_contact;
+ my $contact = $prospect_contacts[0]->contact;
$cust_main->first( $contact->first );
$cust_main->set( 'last', $contact->get('last') );
#contact phone numbers?
{ 'field' => 'contactnum',
'type' => 'contact',
'colspan' => 6,
+ 'custnum' => $opt{cust_main}->custnum,
'm2m_method' => 'cust_contact',
'm2m_dstcol' => 'contactnum',
'm2_label' => 'Contact',
my($cgi, $object) = @_;
#process_o2m fields in process/cust_main-contacts.html
- my @fields = qw( first last title comment );
- my @gfields = ( '', map "_$_", @fields );
+ my $fields = FS::contact->cgi_contact_fields;
+ my @gfields = ( '', map "_$_", @$fields );
map {
if ( /^contactnum(\d+)$/ ) {
if ( grep $cgi->param("contactnum$num$_"), @gfields ) {
my $x = new FS::contact {
'contactnum' => scalar($cgi->param("contactnum$num")),
- map { $_ => scalar($cgi->param("contactnum${num}_$_")) } @fields,
+ map { $_ => scalar($cgi->param("contactnum${num}_$_")) } @$fields,
};
$x;
} else {
% #any?
% 'colspan' => $f->{'colspan'},
% 'required' => $f->{'required'},
+%
+% #contact
+% 'custnum' => $f->{'custnum'},
+% 'prospectnum' => $f->{'prospectnum'},
% );
%
% $include_common{$_} = $f->{$_} foreach grep exists($f->{$_}),
'value_col' => $def->{'select_key'},
'order_by' => dbdef->table($def->{'select_table'})->primary_key,
'multiple' => $def->{'multiple'},
- 'disable_empty' => 1,
+ 'disable_empty' => $def->{'select_allow_empty'} ? undef : 1,
+ 'empty_label' => $def->{'select_allow_empty'} ? ' ' : undef,
'curr_value' => $value,
# these can be switched between multiple and singular,
# so put the complete curr_value in an attribute
<TABLE ID="QuickChargeTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0 STYLE="background-color: #cccccc">
-% if ( $cust_pkg ) {
+% if ( $cust_pkg ) { #modify one-time charge
<INPUT TYPE="hidden" NAME="pkgnum" VALUE="<% $cust_pkg->pkgnum %>">
% my $field = '/elements/tr-input-text.html';
&>
% }
+% unless ($billed) {
<TR>
<TD ALIGN="right"><% mt('Tax exempt') |h %> </TD>
<TD><INPUT TYPE="checkbox" NAME="setuptax" VALUE="Y" <% $cgi->param('setuptax') ? 'CHECKED' : '' %>></TD>
<& /elements/tr-select-taxclass.html, 'curr_value' => $part_pkg->get('taxclass') &>
<& /elements/tr-select-taxproduct.html, 'label' => emt('Tax product'), 'onclick' => 'parent.taxproductmagic(this);', 'curr_value' => $part_pkg->get('taxproductnum') &>
+% }
% } else { # new one-time charge
$label. ': <b>'. encode_entities($item->label($item->history_date)). '</b>';
};
+my $discounts = {};
+my $discount_labelsub = sub {
+ my($item, $label) = @_;
+ my $dnum = $item->discountnum;
+ $discounts->{$dnum} ||= qsearchs({
+ 'table'=>'discount',
+ 'hashref'=>{'discountnum'=>$dnum}
+ });
+ my $d = $discounts->{$dnum};
+ $label . ': <b>' . encode_entities($d->description_short) . '<b>';
+};
+
my %h_table_labelsub = (
'h_cust_pkg' => $pkg_labelsub,
'h_svc_acct' => $svc_labelsub,
'h_svc_external' => $svc_labelsub,
'h_svc_phone' => $svc_labelsub,
#'h_phone_device'
+ 'h_cust_pkg_discount' => $discount_labelsub,
);
my $cust_pkg_date_format = '%b %o, %Y';
<SELECT NAME="<%$name%>_classnum" <% $onchange %>>
<OPTION VALUE="">
% my $classnum = scalar($cgi->param($name.'_classnum'))
-% || $contact->classnum;
+% || $X_contact->classnum;
% foreach my $contact_class (@contact_class) {
<OPTION VALUE="<% $contact_class->classnum %>"
<% ($contact_class->classnum == $classnum) ? 'SELECTED' : '' %>
% }
% } elsif ( $field eq 'emailaddress' ) {
% $value = join(', ', map $_->emailaddress, $contact->contact_email);
+% } elsif ( $field eq 'selfservice_access' || $field eq 'comment' ) {
+% $value = $X_contact->get($field);
% } else {
% $value = $contact->get($field);
% }
my @contact_class = qsearch('contact_class', { 'disabled' => '' });
my $contact;
+my $X_contact;
if ( $curr_value ) {
$contact = qsearchs('contact', { 'contactnum' => $curr_value } );
+ if ( $opt{'custnum'} ) {
+ $X_contact = qsearchs('cust_contact', {
+ 'contactnum' => $curr_value,
+ 'custnum' => $opt{'custnum'},
+ });
+ } elsif ( $opt{'prospectnum'} ) {
+ $X_contact = qsearchs('prospect_contact', {
+ 'contactnum' => $curr_value,
+ 'prospectnum' => $opt{'prospectnum'},
+ });
+ } else {
+ die 'neither custnum nor prospectnum specified';
+ }
} else {
$contact = new FS::contact {};
+ $X_contact = new FS::cust_contact; #arbitrary, it could be prospect_contact
}
my %size = ( 'title' => 12 );
+++ /dev/null
-/***********************************************************************
- Masked Input version 1.1
-************************************************************************
-Author: Kendall Conrad
-Home page: http://www.angelwatt.com/coding/masked_input.php
-Created: 2008-12-16
-Modified: 2010-04-14
-Description:
-License: This work is licensed under a Creative Commons Attribution-Share Alike
- 3.0 United States License http://creativecommons.org/licenses/by-sa/3.0/us/
-
-Argument pieces:
-- elm: [req] text input node to apply the mask on
-- format: [req] string format for the mask
-- allowed: [opt, '0123456789'] string with chars allowed to be typed
-- sep: [opt, '\/:-'] string of char(s) used as separators in mask
-- typeon: [opt, '_YMDhms'] string of chars in mask that can be typed on
-- onbadkey: [opt, null] function to run when user types a unallowed key
-- badkeywait: [opt, 0] used with onbadkey. Indicates how long (in ms) to lock
- text input for onbadkey function to run
-***********************************************************************/
-function MaskedInput(args)
-{
- if (args['elm'] === null || args['format'] === null) { return false; }
- var el = args['elm'],
- format = args['format'],
- allowed = args['allowed'] || '0123456789',
- sep = args['separator'] || '\/:-',
- open = args['typeon'] || '_YMDhms',
- onbadkey = args['onbadkey'] || function(){},
- badwait = args['badkeywait'] || 0;
-
- var locked = false, hold = 0;
- el.value = format;
- // Assign events
- el.onkeydown = KeyHandlerDown; //
- el.onkeypress = KeyHandlerPress; // add event handlers to element
- el.onkeyup = KeyHandlerUp; //
-
- function GetKey(code)
- {
- code = code || window.event, ch = '';
- var keyCode = code.which, evt = code.type;
- if (keyCode == null) { keyCode = code.keyCode; }
- if (keyCode === null) { return ''; } // no key, no play
- // deal with special keys
- switch (keyCode) {
- case 8: ch = 'bksp'; break;
- case 46: // handle del and . both being 46
- ch = (evt == 'keydown') ? 'del' : '.'; break;
- case 16: ch = 'shift'; break;//shift
- case 0:/*CRAP*/ case 9:/*TAB*/ case 13:/*ENTER*/
- ch = 'etc'; break;
- case 37: case 38: case 39: case 40: // arrow keys
- ch = (!code.shiftKey &&
- (code.charCode != 39 && code.charCode !== undefined)) ?
- 'etc' : String.fromCharCode(keyCode);
- break;
- // default to thinking it's a character or digit
- default: ch = String.fromCharCode(keyCode);
- }
- return ch;
- }
- function KeyHandlerDown(e)
- {
- e = e || event;
- if (locked) { return false; }
- var key = GetKey(e);
- if (el.value == '') { el.value = format; SetTextCursor(el,0); }
- // Only do update for bksp del
- if (key == 'bksp' || key == 'del') { Update(key); return false; }
- else if (key == 'etc' || key == 'shift') { return true; }
- else { return true; }
- }
- function KeyHandlerPress(e)
- {
- e = e || event;
- if (locked) { return false; }
- var key = GetKey(e);
- // Check if modifier key is being pressed; command
- if (key=='etc' || e.metaKey || e.ctrlKey || e.altKey) { return true; }
- if (key != 'bksp' && key != 'del' && key != 'etc' && key != 'shift') {
- if (!GoodOnes(key)) { return false; }
- return Update(key);
- }
- else { return false; }
- }
- function KeyHandlerUp(e) { hold = 0; }
- function Update(key)
- {
- var p = GetTextCursor(el), c = el.value, val = '';
- // Handle keys now
- switch (true) {
- case (allowed.indexOf(key) != -1):
- if (++p > format.length) { return false; } // if text csor at end
- // Handle cases where user places csor before separator
- while (sep.indexOf(c.charAt(p-1)) != -1 && p <= format.length) { p++; }
- val = c.substr(0, p-1) + key + c.substr(p);
- // Move csor up a spot if next char is a separator char
- if (allowed.indexOf(c.charAt(p)) == -1
- && open.indexOf(c.charAt(p)) == -1) { p++; }
- break;
- case (key=='bksp'): // backspace
- if (--p < 0) return false; // at start of field
- // If previous char is a separator, move a little more
- while (allowed.indexOf(c.charAt(p)) == -1
- && open.indexOf(c.charAt(p)) == -1
- && p > 1) { p--; }
- val = c.substr(0, p) + format.substr(p,1) + c.substr(p+1);
- break;
- case (key=='del'): // forward delete
- if (p >= c.length) { return false; } // at end of field
- // If next char is a separator and not the end of the text field
- while (sep.indexOf(c.charAt(p)) != -1
- && c.charAt(p) != '') { p++; }
- val = c.substr(0, p) + format.substr(p,1) + c.substr(p+1);
- p++; // Move position forward
- break;
- case (key=='etc'): return true; // Catch other allowed chars
- default: return false; // Ignore the rest
- }
- el.value = ''; // blank it first (Firefox issue)
- el.value = val; // put updated value back in
- SetTextCursor(el, p); // Set the text cursor
- return false;
- }
- function GetTextCursor(node)
- {
- try {
- if (node.selectionStart >= 0) { return node.selectionStart; }
- else if (document.selection) {// IE
- var ntxt = node.value; // getting starting text
- var rng = document.selection.createRange();
- rng.text = '|%|';
- var start = node.value.indexOf('|%|');
- rng.moveStart('character', -3);
- rng.text = '';
- // put starting text back in,
- // fixes issue if all text was highlighted
- node.value = ntxt;
- return start;
- } return -1;
- } catch(e) { return false; }
- }
- function SetTextCursor(node, pos)
- {
- try {
- if (node.selectionStart) {
- node.focus();
- node.setSelectionRange(pos,pos);
- }
- else if (node.createTextRange) { // IE
- var rng = node.createTextRange();
- rng.move('character', pos);
- rng.select();
- }
- } catch(e) { return false; }
- }
- function GoodOnes(k)
- {
- if (allowed.indexOf(k) == -1 && k!='bksp' && k!='del' && k!='etc') {
- var p = GetTextCursor(el); // Need to ensure cursor position not lost
- locked = true; onbadkey();
- // Hold lock long enough for onbadkey function to run
- setTimeout(function(){locked=false; SetTextCursor(el,p);}, badwait);
- return false;
- } return true;
- }
- function resetField() {
- el.value = format;
- }
- function setAllowed(a) {
- allowed = a;
- resetField();
- }
- function setFormat(f) {
- format = f;
- resetField();
- }
- function setSeparator(s) {
- sep = s;
- resetField();
- }
- function setTypeon(t) {
- open = t;
- resetField();
- }
- return {
- resetField:resetField,
- setAllowed:setAllowed,
- setFormat:setFormat,
- setSeparator:setSeparator,
- setTypeon:setTypeon
- }
-}
--- /dev/null
+/**
+ * AW Masked Input
+ * @version 1.3
+ * @author Kendall Conrad
+ * @url http://www.angelwatt.com/coding/masked_input.php
+ * @created 2008-12-16
+ * @modified 2013-08-19
+ * @license This work is licensed under a Creative Commons
+ * Attribution-Share Alike 3.0 United States License
+ * http://creativecommons.org/licenses/by-sa/3.0/us/
+ *
+ * @param scope The object to attach MaskedInput to.
+ */
+(function(scope) {
+ 'use strict';
+
+ /**
+ * MaskedInput takes many possible arguments described below.
+ * Note: req = required, opt = optional
+ * @param {object} args {
+ * -elm [req] text input node to apply the mask on
+ * -format [req] string format for the mask
+ * -allowed [opt, '0123456789'] string with chars allowed to be typed
+ * -sep [opt, '\/:-'] string of char(s) used as separators in mask
+ * -typeon [opt, '_YMDhms'] string of chars in mask that can be typed on
+ * -onfilled [opt, null] function to run when the format is filled in
+ * -onbadkey [opt, null] function to run when user types a unallowed key
+ * -badkeywait [opt, 0] used with onbadkey. Indicates how long (in ms)
+ * to lock text input for onbadkey function to run
+ * -preserve [opt, true] whether to preserve existing text in
+ * field during init.
+ * }
+ * @returns MaskedInput
+ */
+ scope.MaskedInput = function(args) {
+ // Ensure passing in valid argument
+ if (!args || !args.elm || !args.format) {
+ return null;
+ }
+ // Ensure use of 'new'
+ if (!(this instanceof scope.MaskedInput)) {
+ return new scope.MaskedInput(args);
+ }
+ // Initialize variables
+ var self = this,
+ el = args.elm,
+ format = args.format,
+ allowed = args.allowed || '0123456789',
+ sep = args.separator || '\/:-',
+ open = args.typeon || '_YMDhms',
+ onbadkey = args.onbadkey || function() {},
+ onfilled = args.onfilled || function() {},
+ badwait = args.badkeywait || 0,
+ preserve = args.hasOwnProperty('preserve') ? !!args.preserve : true,
+ // ----
+ enabled = true,
+ locked = false,
+ startText = format,
+ /**
+ * Add events to objects.
+ */
+ evtAdd = (function() {
+ if (window.addEventListener) {
+ return function(obj, type, fx, capture) {
+ obj.addEventListener(type, fx,
+ (capture === undefined) ? false : capture);
+ };
+ }
+ if (window.attachEvent) {
+ return function(obj, type, fx) {
+ obj.attachEvent('on' + type, fx);
+ };
+ }
+ return function(obj, type, fx) {
+ obj['on' + type] = fx;
+ };
+ }()),
+ /**
+ * Checks whether the format has been completely filled out.
+ * @return boolean if all typeon chars have been filled.
+ */
+ isFilled = function() {
+ // Check if any typeon characters are left
+ // Work from end of string as it's usually last filled
+ for (var a = el.value.length - 1; a >= 0; a--) {
+ // Check against each typeon character
+ for (var c = 0, d = open.length; c < d; c++) {
+ // If one matches we don't need to check anymore
+ if (el.value[a] === open[c]) {
+ return false;
+ }
+ }
+ }
+ return true;
+ },
+ /**
+ * Gets the current position of the text cursor in a text field.
+ * @param node a input or textarea HTML node.
+ * @return int text cursor position index, or -1 if there was a problem.
+ */
+ getTextCursor = function(node) {
+ try {
+ node.focus();
+ if (node.selectionStart >= 0) {
+ return node.selectionStart;
+ }
+ if (document.selection) {// IE
+ var rng = document.selection.createRange();
+ return -rng.moveStart('character', -node.value.length);
+ }
+ return -1;
+ }
+ catch (e) {
+ return -1;
+ }
+ },
+ /**
+ * Sets the text cursor in a text field to a specific position.
+ * @param node a input or textarea HTML node.
+ * @param pos int of the position to be placed.
+ * @return boolean true is successful, false otherwise.
+ */
+ setTextCursor = function(node, pos) {
+ try {
+ if (node.selectionStart) {
+ node.focus();
+ node.setSelectionRange(pos, pos);
+ }
+ else if (node.createTextRange) { // IE
+ var rng = node.createTextRange();
+ rng.move('character', pos);
+ rng.select();
+ }
+ }
+ catch (e) {
+ return false;
+ }
+ return true;
+ },
+ /**
+ * Gets the keyboard input in usable way.
+ * @param code integer character code
+ * @return string representing character code
+ */
+ getKey = function(code) {
+ code = code || window.event;
+ var ch = '',
+ keyCode = code.which,
+ evt = code.type;
+ if (keyCode === undefined || keyCode === null) {
+ keyCode = code.keyCode;
+ }
+ // no key, no play
+ if (keyCode === undefined || keyCode === null) {
+ return '';
+ }
+ // deal with special keys
+ switch (keyCode) {
+ case 8:
+ ch = 'bksp';
+ break;
+ case 46: // handle del and . both being 46
+ ch = (evt === 'keydown') ? 'del' : '.';
+ break;
+ case 16:
+ ch = 'shift';
+ break;
+ case 0: /*CRAP*/
+ case 9: /*TAB*/
+ case 13:/*ENTER*/
+ ch = 'etc';
+ break;
+ case 37:
+ case 38:
+ case 39:
+ case 40: // arrow keys
+ ch = (!code.shiftKey &&
+ (code.charCode !== 39 && code.charCode !== undefined)) ?
+ 'etc' : String.fromCharCode(keyCode);
+ break;
+ // default to thinking it's a character or digit
+ default:
+ ch = String.fromCharCode(keyCode);
+ break;
+ }
+ return ch;
+ },
+ /**
+ * Stop the event propogation chain.
+ * @param evt Event to stop
+ * @param ret boolean, used for IE to prevent default event
+ */
+ stopEvent = function(evt, ret) {
+ // Stop default behavior the standard way
+ if (evt.preventDefault) {
+ evt.preventDefault();
+ }
+ // Then there's IE
+ evt.returnValue = ret || false;
+ },
+ /**
+ * Updates the text field with the given key.
+ * @param key string keyboard input.
+ */
+ update = function(key) {
+ var p = getTextCursor(el),
+ c = el.value,
+ val = '',
+ cond = true;
+ // Handle keys now
+ switch (cond) {
+ // Allowed characters
+ case (allowed.indexOf(key) !== -1):
+ p = p + 1;
+ // if text cursor at end
+ if (p > format.length) {
+ return false;
+ }
+ // Handle cases where user places cursor before separator
+ while (sep.indexOf(c.charAt(p - 1)) !== -1 && p <= format.length) {
+ p = p + 1;
+ }
+ val = c.substr(0, p - 1) + key + c.substr(p);
+ // Move csor up a spot if next char is a separator char
+ if (allowed.indexOf(c.charAt(p)) === -1
+ && open.indexOf(c.charAt(p)) === -1) {
+ p = p + 1;
+ }
+ break;
+ case (key === 'bksp'): // backspace
+ p = p - 1;
+ // at start of field
+ if (p < 0) {
+ return false;
+ }
+ // If previous char is a separator, move a little more
+ while (allowed.indexOf(c.charAt(p)) === -1
+ && open.indexOf(c.charAt(p)) === -1
+ && p > 1) {
+ p = p - 1;
+ }
+ val = c.substr(0, p) + format.substr(p, 1) + c.substr(p + 1);
+ break;
+ case (key === 'del'): // forward delete
+ // at end of field
+ if (p >= c.length) {
+ return false;
+ }
+ // If next char is a separator and not the end of the text field
+ while (sep.indexOf(c.charAt(p)) !== -1
+ && c.charAt(p) !== '') {
+ p = p + 1;
+ }
+ val = c.substr(0, p) + format.substr(p, 1) + c.substr(p + 1);
+ p = p + 1; // Move position forward
+ break;
+ case (key === 'etc'):
+ // Catch other allowed chars
+ return true;
+ default:
+ return false; // Ignore the rest
+ }
+ el.value = ''; // blank it first (Firefox issue)
+ el.value = val; // put updated value back in
+ setTextCursor(el, p); // Set the text cursor
+ return false;
+ },
+ /**
+ * Returns whether or not a given input is valid for the mask.
+ * @param k string of character to check.
+ * @return bool true if it's a valid character.
+ */
+ goodOnes = function(k) {
+ // if not in allowed list, or invisible key action
+ if (allowed.indexOf(k) === -1 && k !== 'bksp' && k !== 'del' && k !== 'etc') {
+ // Need to ensure cursor position not lost
+ var p = getTextCursor(el);
+ locked = true;
+ onbadkey(k);
+ // Hold lock long enough for onbadkey function to run
+ setTimeout(function() {
+ locked = false;
+ setTextCursor(el, p);
+ }, badwait);
+ return false;
+ }
+ return true;
+ },
+ /**
+ * Handles the key down events.
+ * @param e Event
+ */
+ keyHandlerDown = function(e) {
+ if (!enabled) {
+ return true;
+ }
+ if (locked) {
+ stopEvent(e);
+ return false;
+ }
+ e = e || event;
+ var key = getKey(e);
+ // Stop copy and paste
+ if ((e.metaKey || e.ctrlKey) && (key === 'X' || key === 'V')) {
+ stopEvent(e);
+ return false;
+ }
+ // Allow for OS commands
+ if (e.metaKey || e.ctrlKey) {
+ return true;
+ }
+ if (el.value === '') {
+ el.value = format;
+ setTextCursor(el, 0);
+ }
+ // Only do update for bksp del
+ if (key === 'bksp' || key === 'del') {
+ update(key);
+ stopEvent(e);
+ return false;
+ }
+ return true;
+ },
+ /**
+ * Handles the key press events.
+ * @param e Event
+ */
+ keyHandlerPress = function(e) {
+ if (!enabled) {
+ return true;
+ }
+ if (locked) {
+ stopEvent(e);
+ return false;
+ }
+ e = e || event;
+ var key = getKey(e);
+ // Check if modifier key is being pressed; command
+ if (key === 'etc' || e.metaKey || e.ctrlKey || e.altKey) {
+ return true;
+ }
+ if (key !== 'bksp' && key !== 'del' && key !== 'shift') {
+ if (!goodOnes(key)) {
+ stopEvent(e);
+ return false;
+ }
+ if (update(key)) {
+ if (isFilled()) {
+ onfilled();
+ }
+ stopEvent(e, true);
+ return true;
+ }
+ if (isFilled()) {
+ onfilled();
+ }
+ stopEvent(e);
+ return false;
+ }
+ return false;
+ },
+ /**
+ * Initialize the object.
+ */
+ init = function() {
+ // Check if an input or textarea tag was passed in
+ if (!el.tagName || (el.tagName.toUpperCase() !== 'INPUT'
+ && el.tagName.toUpperCase() !== 'TEXTAREA')) {
+ return null;
+ }
+ // Only place formatted text in field when not preserving
+ // text or it's empty.
+ if (!preserve || el.value === '') {
+ el.value = format;
+ }
+ // Assign events
+ evtAdd(el, 'keydown', function(e) {
+ keyHandlerDown(e);
+ });
+ evtAdd(el, 'keypress', function(e) {
+ keyHandlerPress(e);
+ });
+ // Let us set the initial text state when focused
+ evtAdd(el, 'focus', function() {
+ startText = el.value;
+ });
+ // Handle onChange event manually
+ evtAdd(el, 'blur', function() {
+ if (el.value !== startText && el.onchange) {
+ el.onchange();
+ }
+ });
+ return self;
+ };
+
+ /**
+ * Resets the text field so just the format is present.
+ */
+ self.resetField = function() {
+ el.value = format;
+ };
+
+ /**
+ * Set the allowed characters that can be used in the mask.
+ * @param a string of characters that can be used.
+ */
+ self.setAllowed = function(a) {
+ allowed = a;
+ self.resetField();
+ };
+
+ /**
+ * The format to be used in the mask.
+ * @param f string of the format.
+ */
+ self.setFormat = function(f) {
+ format = f;
+ self.resetField();
+ };
+
+ /**
+ * Set the characters to be used as separators.
+ * @param s string representing the separator characters.
+ */
+ self.setSeparator = function(s) {
+ sep = s;
+ self.resetField();
+ };
+
+ /**
+ * Set the characters that the user will be typing over.
+ * @param t string representing the characters that will be typed over.
+ */
+ self.setTypeon = function(t) {
+ open = t;
+ self.resetField();
+ };
+
+ /**
+ * Sets whether the mask is active.
+ */
+ self.setEnabled = function(enable) {
+ enabled = enable;
+ };
+
+ /**
+ * Local change for Freeside: sets the content of the field,
+ * respecting formatting rules
+ */
+ self.setValue = function(value) {
+ self.resetField();
+ setTextCursor(el, 0);
+ var i = 0; // index in value
+ while (i < value.length && !isFilled()) {
+ update(value[i]);
+ i++;
+ }
+ }
+
+ return init();
+ };
+}(window));
Example:
- include('/elements/init_overlib.html')
+ <& /elements/init_overlib.html &>
- include( '/elements/popup_link.html', { #hashref or a list, either way is fine
+ <& /elements/popup_link.html', { #hashref or a list, either way is fine
#required
'action' => 'content.html', # uri for content of popup
'aname' => "target", # link NAME= value, useful for #targets
'target' => '_parent',
'style' => 'css-attribute:value',
- } )
+ }
+ &>
</%doc>
% if ($params->{'action'} && $label) {
<OPTION VALUE="<% $option %>"
<% $option eq $selected ? ' SELECTED' : '' %>
- ><% $options->{$option} %></OPTION>
+ ><% $options->{$option} |h %></OPTION>
% }
<% include('tr-td-label.html', @_ ) %>
- <TD BGCOLOR="#dddddd" <% $style %>><% $value %></TD>
+ <TD BGCOLOR="#dddddd" <% $style %> <% $colspan %>><% $value %></TD>
</TR>
my %opt = @_;
-my $style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+my $style = $opt{'cell_style'} ? ' STYLE="'. $opt{'cell_style'}. '" ' : '';
+
+my $colspan = $opt{'colspan'} ? ' COLSPAN="'. $opt{'colspan'}. '" ' : '';
my $value = $opt{'formatted_value'} || $opt{'curr_value'} || $opt{'value'};
$value = $opt{'prefix'} . $value if defined($opt{'prefix'});
% if ( !$init ) {
-<script type="text/javascript" src="<%$p%>elements/masked_input_1.1.js">
+<script type="text/javascript" src="<%$p%>elements/masked_input_1.3.js">
</script>
% $init++;
% }
<& /elements/tr-input-text.html, id => $id, @_ &>
<script type="text/javascript">
<&| /elements/onload.js &>
-MaskedInput({
- elm: document.getElementById('<%$id%>'),
+var el = document.getElementById('<%$id%>');
+el.MaskedInput = window.MaskedInput({
+ elm: el,
format: '<% $opt{format} %>',
<% $opt{allowed} ? "allowed: '$opt{allowed}'," : '' %>
<% $opt{typeon} ? "typeon: '$opt{typeon}'," : '' %>
});
-document.getElementById('<%$id%>').value = <% $value |js_string %>;
+el.value = <% $value |js_string %>;
% if ( $clipboard_hack ) {
-var t = document.getElementById('<% $id %>');
var container = document.getElementById('<%$id%>_clipboard');
-var KeyHandlerDown = t.onkeydown
-t.onkeydown = function(e) {
- if (typeof(e) == 'undefined') {
- // ie8 hack
- e = event;
- }
+var KeyDownHandler = function(e) {
+ e = e || event; // IE8
// intercept ctrl-c and ctrl-x
// and cmd-c and cmd-x on mac
- // when text is selected
if ( ( e.ctrlKey || e.metaKey ) ) {
- // do the dance
- var separators = /[\\/:-]/g;
- var s = t.value.substr(t.selectionStart, t.selectionEnd);
- if ( s ) {
- container.value = s.replace(separators, '');
- container.previous = t;
- container.focus();
- container.select();
- return true;
+ // grab contents of the field, strip out delimiters and copy to container,
+ // and select its contents so that the next "ctrl-c" copies it
+
+ el.select(); // just a visual hint to the user
+ var reject = /[^A-Za-z0-9]/g;
+ container.value = el.value.replace(reject, '');
+ container.focus();
+ container.select();
+ // don't confuse the maskedinput key handlers by letting them see this
+ if (e.stopImmediatePropagation) {
+ e.stopImmediatePropagation();
+ } else {
+ // IE8
+ e.returnValue = false;
+ e.cancelBubble = true;
}
}
- return KeyHandlerDown.call(t, e);
};
-container.onkeyup = function(e) {
- if ( container.previous ) {
- setTimeout(function() {
- //container.previous.value = container.value;
- container.previous.focus();
- }, 10);
- }
+var KeyUpHandler = function(e) {
+ e = e || event;
+ setTimeout( function() { el.focus() } , 10);
return true;
+};
+var PasteHandler = function(e) {
+ setTimeout( function() {
+ el.MaskedInput.setValue(container.value);
+ }, 10);
+};
+if ( el.addEventListener ) {
+ el.addEventListener('keydown', KeyDownHandler);
+ container.addEventListener('keyup', KeyUpHandler);
+ container.addEventListener('paste', PasteHandler);
+} else if ( el.attachEvent ) {
+ el.attachEvent('onkeydown', KeyDownHandler);
+ container.attachEvent('onkeyup', KeyUpHandler);
+ container.attachEvent('onpaste', PasteHandler);
}
% } # clipboard hack
</&>
if ( length($opt{'curr_value'}) ) {
$contactnum = $opt{'curr_value'};
} elsif ($prospect_main) {
- my @cust_contact = $prospect_main->contact;
- $contactnum = $cust_contact[0]->contactnum if scalar(@cust_contact)==1;
+ my @prospect_contact = $prospect_main->prospect_contact;
+ $contactnum = $prospect_contact[0]->contactnum if scalar(@prospect_contact)==1;
} else { #$cust_main
$cgi->param('contactnum') =~ /^(\-?\d*)$/ or die "illegal contactnum";
$contactnum = $1;
};
my @contact;
-push @contact, $cust_main->cust_contact if $cust_main;
-push @contact, $prospect_main->contact if $prospect_main;
+push @contact, map $_->contact, $cust_main->cust_contact
+ if $cust_main;
+push @contact, map $_->contact, $prospect_main->prospect_contact
+ if $prospect_main;
push @contact, $contact
if !$cust_main && $contact && $contact->contactnum > 0
&& ! grep { $_->contactnum == $contact->contactnum } @contact;
$cust_location->coord_auto('Y');
my $location_sort = sub {
+ #enabled w/label_prefix _location # $a->locationname cmp $b->locationname
+ # or
$a->country cmp $b->country
or lc($a->city) cmp lc($b->city)
or lc($a->address1) cmp lc($b->address1)
% }
% }
-% my @contact = $quotation->custnum ? $quotation->cust_main->cust_contact
-% : $quotation->prospect_main->contact;
-% foreach my $contact ( @contact ) {
+% my @X_contact = $quotation->custnum
+% ? $quotation->cust_main->cust_contact
+% : $quotation->prospect_main->prospect_contact;
+% foreach my $X_contact ( @X_contact ) {
+% my $contact = $X_contact->contact;
% foreach my $contact_email ( $contact->contact_email ) {
% $emails++;
<& .emailrow, $contact_email->emailaddress, $contact->firstlast &>
<& elements/search.html,
title => 'Contacts',
name_singular => 'contact',
- query => { select => $select,
+ query => { select => join(', ', @select),
table => 'contact',
addl_from => $addl_from,
hashref => \%hash,
extra_sql => $extra_sql,
},
- count_query => "SELECT COUNT(*) FROM contact $extra_sql", #XXX
+ count_query => "SELECT COUNT(*) FROM contact $addl_from $extra_sql", #XXX
header => \@header,
fields => \@fields,
links => \@links,
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('List contacts');
-my $select = 'contact.*';
+my @select = 'contact.contactnum AS contact_contactnum'; #if we select it as bare contactnum, the multi-customer listings go away
+push @select, map "contact.$_", qw( first last title );
my %hash = ();
my $addl_from = '';
-my @header = ( 'First', 'Last', 'Title', );
-my @fields = ( 'first', 'last', 'title', );
-my @links = ( '', '', '' );
+my $link; #for closure in this sub, we'll define it later
+my $contact_classname_sub = sub {
+ my $contact = shift;
+ my %hash = ( 'contactnum' => $contact->contact_contactnum );
+ my $X_contact;
+ if ( $link eq 'cust_main' ) {
+ $X_contact = qsearchs('cust_contact', { %hash, 'custnum' => $contact->custnum } );
+ } elsif ( $link eq 'prospect_main' ) {
+ $X_contact = qsearchs('prospect_contact', { %hash, 'prospectnum' => $contact->prospectnum } );
+ } else {
+ die 'guru meditation #5555';
+ }
+ $X_contact->contact_classname;
+};
+
+my @header = ( 'First', 'Last', 'Title', 'Type' );
+my @fields = ( 'first', 'last', 'title', $contact_classname_sub );
+my @links = ( '', '', '', '', );
my $company_link = '';
}
my $extra_sql = '';
-if ( $cgi->param('link') ) {
+$link = $cgi->param('link');
+if ( $link ) {
- my $coalesce = ', COALESCE( cust_main.company,';
my $as = ') AS prospect_or_customer';
- if ( $cgi->param('link') eq 'cust_main' ) {
+ if ( $link eq 'cust_main' ) {
push @header, 'Customer';
- $select .= "$coalesce cust_main.first||' '||cust_main.last $as";
- $addl_from = ' LEFT JOIN cust_main USING ( custnum )';
- $extra_sql = ' custnum IS NOT NULL ';
+ push @select,
+ "COALESCE( cust_main.company, cust_main.first||' '||cust_main.last $as",
+ map "cust_contact.$_", qw( custnum classnum comment selfservice_access );
+ $addl_from =
+ ' LEFT JOIN cust_contact USING ( contactnum ) '.
+ ' LEFT JOIN cust_main ON ( cust_contact.custnum = cust_main.custnum )';
+ $extra_sql = ' cust_contact.custnum IS NOT NULL ';
$company_link = [ $p.'view/cust_main.cgi?', 'custnum' ];
- } elsif ( $cgi->param('link') eq 'prospect_main' ) {
+ } elsif ( $link eq 'prospect_main' ) {
push @header, 'Prospect';
- $select .= "$coalesce contact.first||' '||contact.last $as";
- $addl_from = ' LEFT JOIN prospect_main USING ( prospectnum )';
- $extra_sql = ' prospectnum IS NOT NULL ';
+ push @select,
+ "COALESCE( prospect_main.company, contact.first||' '||contact.last $as",
+ map "prospect_contact.$_", qw( prospectnum classnum comment );
+ $addl_from =
+ ' LEFT JOIN prospect_contact USING ( contactnum ) '.
+ ' LEFT JOIN prospect_main ON ( prospect_contact.prospectnum = prospect_main.prospectnum )';
+ $extra_sql = ' prospect_contact.prospectnum IS NOT NULL ';
$company_link = [ $p.'view/prospect_main.html?', 'prospectnum' ];
} else {
die "don't know how to report on contacts linked to specified table";
push @header, 'Self-service';
push @fields, 'selfservice_access';
+push @header, 'Comment';
+push @fields, 'comment';
+
$extra_sql = (keys(%hash) ? ' AND ' : ' WHERE '). $extra_sql
if $extra_sql;
],
'html_init' => $html_init,
'really_disable_download' => 1,
- @_
+ @_ #why?
&>
<%init>
#hmm...
push @where, "msgtype = '$1'";
}
if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
- push @where, "custnum = $1";
+ push @where, "cust_msg.custnum = $1";
}
my ($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, '');
push @where, "(_date >= $beginning AND _date <= $ending)";
sub {
my $pm = shift;
[ map {
- [ { 'data' => $_->line, }, ];
+ [ { 'data'=>$_->contact->line, }, ];
}
- $pm->contact
+ $pm->prospect_contact
];
},
],
% my $bgcolor1 = '#eeeeee';
% my $bgcolor2 = '#ffffff';
% my $bgcolor = $bgcolor2;
+% my $th = '<TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">';
<TR>
- <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Type</TH>
- <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Contact</TH>
- <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Email</TH>
- <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc">Self-service</TH>
+ <%$th%>Type</TH>
+ <%$th%>Contact</TH>
+ <%$th%>Email</TH>
+ <%$th%>Self-service</TH>
% foreach my $phone_type (@phone_type) {
- <TH CLASS="grid" ALIGN="left" BGCOLOR="#cccccc"><% $phone_type->typename |h %> phone</TD>
+ <%$th%><% $phone_type->typename |h %></TH>
% }
+ <%$th%>Comment</TH>
</TR>
-% foreach my $contact ( @contacts ) {
+% foreach my $cust_contact ( @cust_contacts ) {
+% my $contact = $cust_contact->contact;
+% my $td = qq(<TD CLASS="grid" BGCOLOR="$bgcolor">);
+
<TR>
- <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $contact->contact_classname |h %></TD>
- <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $contact->line |h %></TD>
+ <%$td%><% $cust_contact->contact_classname |h %></TD>
+ <%$td%><% $contact->line |h %></TD>
% my @contact_email = $contact->contact_email;
- <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% join(', ', map $_->emailaddress, @contact_email) %></TD>
+ <%$td%><% join(', ', map $_->emailaddress, @contact_email) %></TD>
- <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-% if ( $contact->selfservice_access ) {
+ <%$td%>
+% if ( $cust_contact->selfservice_access ) {
Enabled
%# <FONT SIZE="-1"><A HREF="XXX">disable</A>
%# <A HREF="XXX">re-email</A></FONT>
% 'contactnum' => $contact->contactnum,
% 'phonetypenum' => $phone_type->phonetypenum,
% });
- <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $contact_phone ? $contact_phone->phonenum_pretty : '' |h %></TD>
+ <%$td%><% $contact_phone ? $contact_phone->phonenum_pretty : '' |h %></TD>
% }
+ <%$td%><% $cust_contact->comment |h %></TD>
+
</TR>
% if ( $bgcolor eq $bgcolor1 ) {
my( $cust_main ) = @_;
#my $conf = new FS::Conf;
-my @contacts = $cust_main->cust_contact;
+my @cust_contacts = $cust_main->cust_contact;
</%init>
</TR>
% }
-% foreach my $contact ( $prospect_main->contact ) {
+% foreach my $prospect_contact ( $prospect_main->prospect_contact ) {
+% my $contact = $prospect_contact->contact;
<TR>
- <TD ALIGN="right"><% $contact->contact_classname %> Contact</TD>
+ <TD ALIGN="right"><% $prospect_contact->contact_classname %> Contact</TD>
<TD BGCOLOR="#FFFFFF"><% $contact->line %></TD>
</TR>
%}
then
echo -n "Stopping (old) freeside-selfservice-server: "
kill `cat /var/run/freeside-selfservice-server.$SELFSERVICE_USER.pid`
+ sleep
rm /var/run/freeside-selfservice-server.$SELFSERVICE_USER.pid
echo "done."
fi
for MACHINE in $SELFSERVICE_MACHINES; do
if [ -e /var/run/freeside-selfservice-server.$SELFSERVICE_USER.$MACHINE.pid ]
then
- echo -n "Stopping freeside-selfservice-server to $MACHINE: "
- kill `cat /var/run/freeside-selfservice-server.$SELFSERVICE_USER.$MACHINE.pid`
+ echo -n "Stopping freeside-selfservice-server to $MACHINE"
+ howlong=10
+ while [ $howlong -gt 0 ] && kill -0 `cat /var/run/freeside-selfservice-server.$SELFSERVICE_USER.$MACHINE.pid` 2>/dev/null; do
+ echo -n '.'
+ kill `cat /var/run/freeside-selfservice-server.$SELFSERVICE_USER.$MACHINE.pid`
+ sleep 1
+ howlong=$(( $howlong - 1 ))
+ if [ $howlong -eq 0 ]; then
+ echo -n 'forcefully.'
+ kill -9 `cat /var/run/freeside-selfservice-server.$SELFSERVICE_USER.$MACHINE.pid`
+ fi
+ done
echo "done."
+ rm /var/run/freeside-selfservice-server.$SELFSERVICE_USER.$MACHINE.pid
fi
done
$error = $_GET['error'];
if ( $error ) {
$username = $_GET['username'];
+ $email = $_GET['email'];
$domain = $_GET['domain'];
}
<TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=2 CELLPADDING=0>
-<TR>
- <TH ALIGN="right">Username </TH>
- <TD>
- <INPUT TYPE="text" NAME="username" VALUE="<? echo htmlspecialchars($username); ?>"><? if ( $single_domain ) { echo '@'.$single_domain; } ?>
- </TD>
-</TR>
-
<? if ( $single_domain ) { ?>
+ <TR>
+ <TH ALIGN="right">Username </TH>
+ <TD>
+ <INPUT TYPE="text" NAME="username" VALUE="<? echo htmlspecialchars($username); ?>"><? if ( $single_domain ) { echo '@'.$single_domain; } ?>
+ </TD>
+< /TR>
+
<INPUT TYPE="hidden" NAME="domain" VALUE="<? echo $single_domain ?>">
<? } else { ?>
<TR>
- <TH ALIGN="right">Domain </TH>
+ <TH ALIGN="right">Email address </TH>
<TD>
- <INPUT TYPE="text" NAME="domain" VALUE="<? echo htmlspecialchars($domain); ?>">
+ <INPUT TYPE="text" NAME="email" VALUE="<? echo htmlspecialchars($email); ?>">
</TD>
</TR>
$freeside = new FreesideSelfService();
$response = $freeside->login( array(
+ 'email' => strtolower($_POST['email']),
'username' => strtolower($_POST['username']),
'domain' => strtolower($_POST['domain']),
'password' => $_POST['password'],
if ( $error ) {
header('Location:index.php?username='. urlencode($username).
- '&domain='. urlencode($domain).
- '&error='. urlencode($error)
+ '&domain='. urlencode($domain).
+ '&email='. urlencode($email).
+ '&error='. urlencode($error)
);
die();
error_log("[login] logged into freeside with session_id=$session_id, setting cookie");
-// now what? for now, always redirect to the main page.
+// now what? for now, always redirect to the main page (or the select a
+// customer diversion).
// eventually, other options?
setcookie('session_id', $session_id);
-header("Location:main.php")
-#die();
+if ( $response['custnum'] || $response['svcnum'] ) {
+
+ header("Location:main.php");
+ die();
+
+} elseif ( $response['customers'] ) {
+var_dump($response['customers']);
+?>
+
+ <? $title ='Select customer'; include('elements/header.php'); ?>
+ <? include('elements/error.php'); ?>
+
+ <FORM NAME="SelectCustomerForm" ACTION="process_select_cust.php" METHOD=POST>
+ <INPUT TYPE="hidden" NAME="action" VALUE="switch_cust">
+
+ <TABLE BGCOLOR="#c0c0c0" BORDER=0 CELLSPACING=2 CELLPADDING=0>
+
+ <TR>
+ <TH ALIGN="right">Customer </TH>
+ <TD>
+ <SELECT NAME="custnum" ID="custnum" onChange="custnum_changed()">
+ <OPTION VALUE="">Select a customer
+ <? foreach ( $response['customers'] AS $custnum => $customer ) { ?>
+ <OPTION VALUE="<? echo $custnum ?>"><? echo htmlspecialchars( $customer ) ?>
+ <? } ?>
+ </SELECT>
+ </TD>
+ </TR>
+
+ <TR>
+ <TD COLSPAN=2 ALIGN="center"><INPUT TYPE="submit" ID="submit" VALUE="Select customer" DISABLED></TD>
+ </TR>
+
+ </TABLE>
+ </FORM>
+
+ <SCRIPT TYPE="text/javascript">
+
+ function custnum_changed () {
+ var form = document.SelectCustomerForm;
+ if ( form.custnum.selectedIndex > 0 ) {
+ form.submit.disabled = false;
+ } else {
+ form.submit.disabled = true;
+ }
+ }
+
+ </SCRIPT>
+
+ <? include('elements/footer.php'); ?>
+
+<?
+
+// } else {
+//
+// die 'login successful, but unrecognized info (no custnum, svcnum or customers)';
+
+}
?>
--- /dev/null
+<?
+
+require('freeside.class.php');
+$freeside = new FreesideSelfService();
+
+$response = $freeside->switch_cust( array(
+ 'session_id' => $_COOKIE['session_id'],
+ 'custnum' => $_POST['custnum'],
+) );
+
+#error_log("[switch_cust] received response from freeside: $response");
+
+$error = $response['error'];
+
+if ( $error ) {
+
+ //this isn't well handled... but the only possible error is a session timeout?
+
+ header('Location:index.php?username='. urlencode($username).
+ '&domain='. urlencode($domain).
+ '&email='. urlencode($email).
+ '&error='. urlencode($error)
+ );
+ die();
+
+}
+
+// sucessful customer selection
+
+header("Location:main.php");
+
+?>