},
{
+ 'key' => 'processing-fee_on_separate_invoice',
+ 'section' => 'payments',
+ 'description' => 'Places the processing fee on a separate invoice by itself. Only works with real time processing.',
+ 'type' => 'checkbox',
+ 'validate' => sub {
+ my $conf = new FS::Conf;
+ !$conf->config('batch-enable_payby') ? '' : 'You can not set this option while batch processing is enabled.';
+ },
+ },
+
+ {
'key' => 'banned_pay-pad',
'section' => 'credit_cards',
'description' => 'Padding for encrypted storage of banned credit card hashes. If you already have new-style SHA512 entries in the banned_pay table, do not change as this will invalidate the old entries.',
'description' => 'Enable batch processing for the specified payment types.',
'type' => 'selectmultiple',
'select_enum' => [qw( CARD CHEK )],
+ 'validate' => sub {
+ ## can not create a new invoice and pay it silently with batch processing, only realtime processing.
+ my $conf = new FS::Conf;
+ !$conf->exists('processing-fee_on_separate_invoice') ? '' : 'You can not enable batch processing while processing-fee_on_separate_invoice option is enabled.';
+ },
},
{
'Agent | Agent Cust# or Cust# | Cust. Status | Customer' =>
'Agent | Agent Cust# | Status | Last, First or Company (Last, First)',
- 'Customer | Day phone | Night phone | Mobile phone | Fax number' =>
+ 'Customer | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s)' =>
'Customer | (all phones)',
- 'Cust# | Customer | Day phone | Night phone | Mobile phone | Fax number' =>
+ 'Cust# | Customer | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s)' =>
'custnum | Customer | (all phones)',
'Cust. Status | Name | Company' =>
'Cust# | Cust. Status | Name | Company' =>
'custnum | Status | Last, First | Company',
- 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Contact email(s) | Invoices | Messages' =>
+ 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | Contact email(s) | Invoices | Messages' =>
'custnum | Status | Last, First | Company | (address) | (all phones) | Contact email(s)',
- 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Invoicing email(s)' =>
+ 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | Invoicing email(s)' =>
'custnum | Status | Last, First | Company | (address) | (all phones) | Invoicing email(s)',
- 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Invoicing email(s) | Current Balance' =>
+ 'Cust# | Cust. Status | Name | Company | Address 1 | Address 2 | City | State | Zip | Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | Invoicing email(s) | Current Balance' =>
'custnum | Status | Last, First | Company | (address) | (all phones) | Invoicing email(s) | Current Balance',
- 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s)' =>
+ 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s)' =>
'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s)',
- 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Current Balance' =>
+ 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Current Balance' =>
'custnum | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Current Balance',
- 'Cust# | Agent Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Current Balance' =>
+ 'Cust# | Agent Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | Invoicing email(s) | Current Balance' =>
'custnum | Agent Cust# | Status | Last, First | Company | (address) | (all phones) | (service address) | Invoicing email(s) | Current Balance',
- 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Latitude | (bill) Longitude | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Latitude | (service) Longitude | Invoicing email(s) | Current Balance' =>
+ 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Latitude | (bill) Longitude | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Latitude | (service) Longitude | Invoicing email(s) | Current Balance' =>
'custnum | Status | Last, First | Company | (address+coord) | (all phones) | (service address+coord) | Invoicing email(s) | Current Balance',
- 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Latitude | (bill) Longitude | Day phone | Night phone | Mobile phone | Fax number | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Latitude | (service) Longitude | Invoicing email(s) | Current Balance | Advertising Source' =>
+ 'Cust# | Cust. Status | Name | Company | (bill) Address 1 | (bill) Address 2 | (bill) City | (bill) State | (bill) Zip | (bill) Country | (bill) Latitude | (bill) Longitude | Day phone | Night phone | Mobile phone | Fax number | Contact phone(s) | (service) Address 1 | (service) Address 2 | (service) City | (service) State | (service) Zip | (service) Country | (service) Latitude | (service) Longitude | Invoicing email(s) | Current Balance | Advertising Source' =>
'custnum | Status | Last, First | Company | (address+coord) | (all phones) | (service address+coord) | Invoicing email(s) | Current Balance | Advertising Source',
'Invoicing email(s)' => 'Invoicing email(s)',
)
);
+ unless ( wa_sales_update_tax_table_sanity_check() ) {
+ log_error_and_die(
+ 'Duplicate district rows exist in the Washington state sales tax table. '.
+ 'These must be resolved before updating the tax tables. '.
+ 'See "freeside-wa-tax-table-resolve --check" to repair the tax tables. '
+ );
+ }
+
$args->{temp_dir} ||= tempdir();
$args->{filename} ||= wa_sales_fetch_xlsx_file( $args );
my $update_count = 0;
my $same_count = 0;
+ $args->{taxname} ||= 'State Sales Tax';
+
# Work within a SQL transaction
local $FS::UID::AutoCommit = 0;
cust_main_county => {
source => 'wa_sales',
district => { op => '!=', value => undef },
- tax_class => $taxclass,
+ taxclass => $taxclass,
}
)
) {
$cust_main_county{$district} = $row;
}
- # Merge any dupes, place resulting non-dupe row in %cust_main_county
- # Merge, even if one of the dupes has a $0 tax, or some other
- # variation on tax row data. Data for this row will get corrected
- # during the following tax import
- for my $dupe_district_aref ( values %cust_main_county_dupe ) {
- my $row_to_keep = shift @$dupe_district_aref;
- while ( my $row_to_merge = shift @$dupe_district_aref ) {
- $row_to_merge->_merge_into(
- $row_to_keep,
- { identical_record_check => 0 },
- );
- }
- $cust_main_county{$row_to_keep->district} = $row_to_keep;
+ # # Merge any dupes, place resulting non-dupe row in %cust_main_county
+ # # Merge, even if one of the dupes has a $0 tax, or some other
+ # # variation on tax row data. Data for this row will get corrected
+ # # during the following tax import
+ # for my $dupe_district_aref ( values %cust_main_county_dupe ) {
+ # my $row_to_keep = shift @$dupe_district_aref;
+ # while ( my $row_to_merge = shift @$dupe_district_aref ) {
+ # $row_to_merge->_merge_into(
+ # $row_to_keep,
+ # { identical_record_check => 0 },
+ # );
+ # }
+ # $cust_main_county{$row_to_keep->district} = $row_to_keep;
+ # }
+
+ # If there are duplicate rows, it may be unsafe to auto-resolve them
+ if ( %cust_main_county_dupe ) {
+ warn "Unable to continue!";
+ log_error_and_die( sprintf(
+ 'Tax district duplicate rows detected(%s) - '.
+ 'WA Sales tax tables cannot be updated without resolving duplicates - '.
+ 'Please use tool freeside-wa-tax-table-resolve for tax table repair',
+ join( ',', keys %cust_main_county_dupe )
+ ));
}
- for my $district ( @{ $args->{tax_districts} } ) {
+ DIST: for my $district ( @{ $args->{tax_districts} } ) {
if ( my $row = $cust_main_county{ $district->{district} } ) {
+ # Strip whitespace from input
+ $district->{$_} =~ s/(^\s+|\s+$)//g for keys %$district;
+
# District already exists in this taxclass, update if necessary
#
# If admin updates value of conf tax_district_taxname, instead of
no warnings 'uninitialized';
if (
- $row->tax == ( $district->{tax_combined} * 100 )
+ sprintf('%.4f',$row->tax) == sprintf('%.4f',($district->{tax_combined} * 100))
&& $row->taxname eq $args->{taxname}
&& uc $row->county eq uc $district->{county}
&& uc $row->city eq uc $district->{city}
) {
$same_count++;
- next;
+ next DIST;
}
}
$row->city( uc $district->{city} );
$row->county( uc $district->{county} );
$row->taxclass( $taxclass );
- $row->taxname( $args->{taxname} || undef );
+ $row->taxname( $args->{taxname} );
$row->tax( $district->{tax_combined} * 100 );
if ( my $error = $row->replace ) {
$insert_count++;
}
+ update_non_sales_tax_rows( $taxclass, $district );
+
} # /foreach $district
} # /foreach $taxclass
}
+=head2 update_non_sales_tax_rows tax_class, $district_href
+
+The customer may have created additional taxes, such as Universal Service Fund.
+
+Ensure the columns for city and county are consistant between
+the user-created tax rows and the wa-sales-managed tax rows.
+
+=cut
+
+sub update_non_sales_tax_rows {
+ my ( $taxclass, $district ) = @_;
+
+ return unless ref $district && $district->{district};
+
+ my @rows = qsearch( cust_main_county => {
+ taxclass => $taxclass,
+ district => $district->{district},
+ state => 'WA',
+ country => 'US',
+ source => { op => '!=', value => 'wa_sales' },
+ });
+
+ for my $row ( @rows ) {
+ $row->city( uc $district->{city} );
+ $row->county( uc $district->{county} );
+
+ if ( my $error = $row->replace ) {
+ dbh->rollback;
+ local $FS::UID::AutoCommit = 1;
+ log_error_and_die(
+ sprintf
+ "Error updating cust_main_county row %s for district %s: %s",
+ $row->taxnum,
+ $district->{district},
+ $error
+ );
+ }
+ }
+
+}
+
=head2 wa_sales_parse_xlsx_file \%args
Parse given XLSX file for tax district information
}
+=head2 wa_sales_update_tax_table_sanity_check
+
+There should be no duplicate tax table entries in the tax table,
+with the same district value, within a tax class, where source=wa_sales.
+
+If there are, custome taxes may have been user-entered in the
+freeside UI, and incorrectly labelled as source=wa_sales. Or, the
+dupe record may have been created by issues with older wa_sales code.
+
+If these dupes exist, the sysadmin must solve the problem by hand
+with the freeeside-wa-tax-table-resolve script
+
+Returns 1 unless problem sales tax entries are detected
+
+=cut
+
+sub wa_sales_update_tax_table_sanity_check {
+ FS::cust_main_county->find_wa_tax_dupes ? 0 : 1;
+}
+
sub log {
state $log = FS::Log->new('tax_rate_update');
$log;
sub log_error_and_die {
my $log_message = shift;
&log()->error( $log_message );
+ warn( "$log_message\n" );
die( "$log_message\n" );
}
my @taxes = (); # entries are cust_main_county objects
my %taxhash_elim = %taxhash;
my @elim = qw( district city county state );
+
+ # WA state district city names are not stable in the WA tax tables
+ # Allow districts to match with just a district id
+ if ( $taxhash{district} ) {
+ @taxes = qsearch( cust_main_county => {
+ district => $taxhash{district},
+ taxclass => $taxhash{taxclass},
+ });
+ if ( !scalar(@taxes) && $taxhash{taxclass} ) {
+ qsearch( cust_main_county => {
+ district => $taxhash{district},
+ taxclass => '',
+ });
+ }
+ }
+
do {
#first try a match with taxclass
- @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
+ if ( !scalar(@taxes) ) {
+ @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
+ }
if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
#then try a match without taxclass
use HTML::Entities;
use FS::Conf;
use FS::Misc::DateTime qw( parse_datetime day_end );
-use FS::Record qw(dbdef);
+use FS::Record qw(dbdef qsearch);
use FS::cust_main; # are sql_balance and sql_date_balance in the right module?
#use vars qw(@ISA);
$header2method{'Cust#'} = 'display_custnum'
if $conf->exists('cust_main-default_agent_custid');
+foreach my $phone_type ( FS::phone_type->get_phone_types() ) {
+ $header2method{'Contact '.$phone_type->typename.' phone(s)'} = sub {
+ my $self = shift;
+ my $num = $phone_type->phonetypenum;
+
+ my @phones;
+ foreach (FS::cust_main::contact_list_name_phones($self)) {
+ my $data = [
+ {
+ 'data' => $_->first.' '.$_->last.' '.FS::contact_phone::phonenum_pretty($_),
+ },
+ ];
+ push @phones, $data if $_->phonetypenum eq $phone_type->phonetypenum;
+ }
+ return \@phones;
+ };
+}
+
my %header2colormethod = (
'Cust. Status' => 'cust_statuscolor',
);
}
}
- foreach my $field (qw(daytime night mobile fax )) {
+ foreach my $field (qw(daytime night mobile fax)) {
push @fields, $field if (grep { $_ eq $field } @cust_fields);
}
push @fields, 'agent_custid';
use FS::cdr qw( _cdr_date_parser_maker _cdr_min_parser_maker );
%info = (
- 'name' => 'telapi_voip',
+ 'name' => 'telapi_voip (csv file)',
'weight' => 601,
'header' => 1,
'type' => 'csv',
'import_fields' => [
- _cdr_date_parser_maker('startdate'), #'date gmt'
+ skip(1), # Inbound/Outbound
+ _cdr_date_parser_maker('startdate'), # date
+ skip(1), # cost per minute
+ 'upstream_price', # call cost
+ 'billsec', # duration
'src', # source
'dst', # destination
- 'clid', # callerid
skip(1), # hangup code
- skip(1), # sip account
- 'src_ip_addr', # orig ip
- 'duration', # duration
- skip(1), # per minute
- 'upstream_price', # callcost
- sub {
- my($cdr, $cdrtypename, $conf, $param) = @_;
- return unless length($cdrtypename);
- _init_cdr_types();
- unless (defined $CDR_TYPES->{$cdrtypename}) {
- warn "Skipping Record: CDR type name $cdrtypename does not exist!";
- $param->{skiprow} = 1;
- }
- $cdr->cdrtypenum($CDR_TYPES->{$cdrtypename});
- }, # type
- _cdr_min_parser_maker('billsec'), #PriceDurationMins
],
);
sub skip { map {''} (1..$_[0]) }
-sub _init_cdr_types {
- unless ($CDR_TYPES) {
- $CDR_TYPES = {};
- foreach my $cdr_type ( qsearch('cdr_type') ) {
- die "multiple cdr_types with same cdrtypename".$cdr_type->cdrtypename
- if defined $CDR_TYPES->{$cdr_type->cdrtypename};
- $CDR_TYPES->{$cdr_type->cdrtypename} = $cdr_type->cdrtypenum;
- }
- }
-}
-
1;
\ No newline at end of file
});
}
+=item contact_list_name_phones
+
+Returns a list of contact phone numbers.
+{ phonetypenum => '1', phonenum => 'xxxxxxxxxx', first => 'firstname', last => 'lastname', countrycode => '1' }
+
+=cut
+
+sub contact_list_name_phones {
+ my $self = shift;
+ my $phone_type = shift;
+
+ warn "$me contact_list_phones" if $DEBUG;
+
+ return () if !$self->custnum; # not yet inserted
+ return map { $_ }
+ qsearch({
+ table => 'cust_contact',
+ select => 'phonetypenum, phonenum, first, last, countrycode',
+ addl_from => ' JOIN contact USING (contactnum) '.
+ ' JOIN contact_phone USING (contactnum)',
+ hashref => { 'custnum' => $self->custnum, 'phonetypenum' => $phone_type, },
+ order_by => 'ORDER BY custcontactnum DESC',
+ extra_sql => '',
+ });
+}
+
=item contact_list_emailonly
Returns an array of hashes containing the emails. Used for displaying contact email field in advanced customer reports.
my @taxes = (); # entries are cust_main_county objects
my %taxhash_elim = %taxhash;
my @elim = qw( district city county state );
+
+ # WA state district city names are not stable in the WA tax tables
+ # Allow districts to match with just a district id
+ if ( $taxhash{district} ) {
+ @taxes = qsearch( cust_main_county => {
+ district => $taxhash{district},
+ taxclass => $taxhash{taxclass},
+ });
+ if ( !scalar(@taxes) && $taxhash{taxclass} ) {
+ qsearch( cust_main_county => {
+ district => $taxhash{district},
+ taxclass => '',
+ });
+ }
+ }
+
do {
#first try a match with taxclass
- @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
+ if ( !scalar(@taxes) ) {
+ @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
+ }
if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
#then try a match without taxclass
if ( $cust_payby->locationnum ) {
my $cust_location = $cust_payby->cust_location;
- $options->{$_} = $cust_location->$_() for qw( address1 address2 city state zip );
+ $options->{$_} = $cust_location->$_()
+ for qw( address1 address2 city state zip country );
}
}
}
savepoint_create( $savepoint_label );
#start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
-
- my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
+ my $error = $cust_pay->insert(
+ $options{'manual'} ? ( 'manual' => 1 ) : (),
+ $options{'processing-fee'} > 0 ? ( 'processing-fee' => $options{'processing-fee'} ) : (),
+ );
if ( $error ) {
savepoint_rollback( $savepoint_label );
$cust_pay->invnum(''); #try again with no specific invnum
$cust_pay->paynum('');
- my $error2 = $cust_pay->insert( $options{'manual'} ?
- ( 'manual' => 1 ) : ()
- );
+ my $error2 = $cust_pay->insert(
+ $options{'manual'} ? ( 'manual' => 1 ) : (),
+ $options{'processing-fee'} > 0 ? ( 'processing-fee' => $options{'processing-fee'} ) : (),
+ );
if ( $error2 ) {
# gah. but at least we have a record of the state we had to abort in
# from cust_pay_pending now.
if ($options{'processing-fee'} > 0) {
my $pf_cust_pkg;
my $processing_fee_text = 'Payment Processing Fee';
+
+ my $conf = new FS::Conf;
+
+ my $pf_seperate_bill;
+ my $pf_bill_now;
+ if ($conf->exists('processing-fee_on_separate_invoice')) {
+ $pf_seperate_bill = 'Y';
+ $pf_bill_now = '1';
+ }
+
my $pf_change_error = $self->charge({
'amount' => $options{'processing-fee'},
'pkg' => $processing_fee_text,
'setuptax' => 'Y',
'cust_pkg_ref' => \$pf_cust_pkg,
+ 'separate_bill' => $pf_seperate_bill,
+ 'bill_now' => $pf_bill_now,
});
if($pf_change_error) {
# but keep going...
}
- my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
- unless ( $cust_bill ) {
- warn "race condition + invoice deletion just happened";
- return '';
- }
+ if ($conf->exists('processing-fee_on_separate_invoice')) {
+ my $cust_bill_pkg = qsearchs( 'cust_bill_pkg', { 'pkgnum' => $pf_cust_pkg->pkgnum } );
+
+ my $pf_cust_bill = qsearchs('cust_bill', { 'invnum' => $cust_bill_pkg->invnum });
+ unless ( $pf_cust_bill ) {
+ warn "no processing fee inv found!";
+ return '';
+ }
+
+ my $pf_apply_error = $pf_cust_bill->apply_payments_and_credits;
+
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
+ unless ( $cust_bill ) {
+ warn "race condition + invoice deletion just happened";
+ return '';
+ }
+
+ my $grand_pf_error = $cust_bill->apply_payments_and_credits;
+
+ warn "cannot apply Processing fee to invoice #$invnum: $grand_pf_error - $pf_apply_error"
+ if $grand_pf_error || $pf_apply_error;
+ } ## processing-fee_on_separate_invoice
+ else {
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
+ unless ( $cust_bill ) {
+ warn "race condition + invoice deletion just happened";
+ return '';
+ }
- my $grand_pf_error =
- $cust_bill->add_cc_surcharge($pf_cust_pkg->pkgnum,$options{'processing-fee'});
+ my $grand_pf_error =
+ $cust_bill->add_cc_surcharge($pf_cust_pkg->pkgnum,$options{'processing-fee'});
- warn "cannot add Processing fee to invoice #$invnum: $grand_pf_error"
- if $grand_pf_error;
+ warn "cannot add Processing fee to invoice #$invnum: $grand_pf_error"
+ if $grand_pf_error;
+ } ## no processing-fee_on_separate_invoice
} #end if $options{'processing-fee'}
} #end if ( $options{'cc_surcharge'} > 0 || $options{'processing-fee'} > 0)
)";
}
+ ##
+ # phones
+ ##
+
+ foreach my $phonet (qw(daytime night mobile fax)) {
+ if ($params->{$phonet}) {
+ $params->{$phonet} =~ s/\D//g;
+ $params->{$phonet} =~ /^(\d{3})(\d{3})(\d{4})(\d*)$/
+ or next;
+ my $phonen = "$1-$2-$3";
+ if ($4) { push @where, "cust_main.".$phonet." = '".$phonen." x$4'"; }
+ else { push @where, "cust_main.".$phonet." like '".$phonen."%'"; }
+ }
+ }
+
###
# refnum
###
) ";
}
- if ($contact_params->{'contacts_homephone'} || $contact_params->{'contacts_workphone'} || $contact_params->{'contacts_mobilephone'}) {
- foreach my $phone (qw( contacts_homephone contacts_workphone contacts_mobilephone )) {
+ if ( grep { /^contacts_phonetypenum(\d+)$/ } keys %{ $contact_params } ) {
+ my $phone_query;
+ foreach my $phone ( grep { /^contacts_phonetypenum(\d+)$/ } keys %{ $contact_params } ) {
+ $phone =~ /^contacts_phonetypenum(\d+)$/ or die "No phone type num $1 from $phone";
+ my $phonetypenum = $1;
(my $num = $contact_params->{$phone}) =~ s/\W//g;
if ( $num =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { $contact_params->{$phone} = "$1$2$3"; }
+ $phone_query .= " AND ( contact_phone.phonetypenum = '".$phonetypenum."' AND contact_phone.phonenum = '" . $contact_params->{$phone} . "' )"
+ unless !$contact_params->{$phone};
}
- my $home_query = " AND ( contact_phone.phonetypenum = '2' AND contact_phone.phonenum = '" . $contact_params->{'contacts_homephone'} . "' )"
- unless !$contact_params->{'contacts_homephone'};
- my $work_query = " AND ( contact_phone.phonetypenum = '1' AND contact_phone.phonenum = '" . $contact_params->{'contacts_workphone'} . "' )"
- unless !$contact_params->{'contacts_workphone'};
- my $mobile_query = " AND ( contact_phone.phonetypenum = '3' AND contact_phone.phonenum = '" . $contact_params->{'contacts_mobilephone'} . "' )"
- unless !$contact_params->{'contacts_mobilephone'};
push @where,
"EXISTS ( SELECT 1 FROM contact_phone
JOIN cust_contact USING (contactnum)
WHERE cust_contact.custnum = cust_main.custnum
- $home_query $work_query $mobile_query
+ $phone_query
) ";
}
-}
+ }
##
return $tax_item;
}
+=head1 find_wa_tax_dupes
+
+Return a list of cust_main_county Record objects that are detected
+as duplicate washington state sales tax rows (source=wa_state)
+within their respective tax classes
+
+=cut
+
+sub find_wa_tax_dupes {
+ my %cust_main_county;
+ my @dupes;
+
+ for my $row ( qsearch( cust_main_county => { source => 'wa_sales' } ) ) {
+ my $taxclass = $row->taxclass || 'none';
+ $cust_main_county{$taxclass} ||= {};
+
+ my $district = $row->district || 'none';
+ $cust_main_county{$taxclass}->{$district} ||= [];
+
+ push @{ $cust_main_county{$taxclass}->{$district} }, $row;
+ }
+
+ for my $taxclass ( keys %cust_main_county ) {
+ for my $district ( keys %{ $cust_main_county{$taxclass} } ) {
+ my $tax_rows = $cust_main_county{$taxclass}->{$district};
+ if ( scalar @$tax_rows > 1 ) {
+ push @dupes, @$tax_rows;
+ }
+ }
+ }
+
+ @dupes;
+}
+
=back
=head1 SUBROUTINES
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ $self->{'processing_fee'} = $options{'processing-fee'};
+
#payment receipt
my $trigger = $conf->config('payment_receipt-trigger',
$self->cust_main->agentnum) || 'cust_pay';
my %substitutions = ();
$substitutions{invnum} = $cust_bill->invnum if $cust_bill;
+ $substitutions{'processing_fee'} = $self->{'processing_fee'};
+
my $msg_template = qsearchs('msg_template',{ msgnum => $msgnum});
unless ($msg_template) {
if ( my %option_fields = $self->option_fields ) {
if ( my $option_field = $option_fields{ $self->optionname } ) {
- if ( my $validation_method = $option_field->{validation} ) {
+ if ( ref $option_field && $option_field->{validation} ) {
+ my $validation_method = $option_field->{validation};
$error = $self->$validation_method('optionvalue');
}
}
Make sure you have set the up and down rate limit for the tower and the sector. This is required to be able to export the access point.
The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector. They will be modified at Saisei when modified in freeside.
Each sector will be attached to its tower access point using the Saisei uplink field.
+Each access point will be attached to the interface set in the export config. If left blank access point will be attached to the default interface. Most setups can leave this blank.
Create a package for the above created service, and order this package for a customer.
error_url => '/edit/part_export.cgi?',
success_message => 'Saisei export of provisioned services successful',
},
+ 'export_all_towers_sectors' => { component => '/elements/popup_link.html',
+ label => 'Export of all towers and sectors',
+ description => 'Will force an export of all towers and sectors to Saisei as access points.',
+ html_label => '<b>Export all towers and sectors.</b>',
+ error_url => '/edit/part_export.cgi?',
+ success_message => 'Saisei export of towers and sectors as access points successful',
+ },
+ 'force_export_all_users' => { component => '/elements/popup_link.html',
+ label => 'Force update of all Saisei users from freeside provisioned services',
+ description => 'Will force an update of Saisei users description and map location from freeside provisioned services.',
+ html_label => '<b>Force update of all Saisei users from freeside provisioned services</b>',
+ error_url => '/edit/part_export.cgi?',
+ success_message => 'Export of freeside provisioned services as Saisei users was successful',
+ },
+ 'force_export_all_virtual_ap' => { component => '/elements/popup_link.html',
+ label => 'Force update of all virtual Access Points',
+ description => 'Will force an update of all virtual access points.',
+ html_label => '<b>Force update of all virtual Access Points</b>',
+ error_url => '/edit/part_export.cgi?',
+ success_message => 'Export of all virtual access points to Saisei was successful',
+ },
;
tie my %options, 'Tie::IxHash',
default => '' },
'password' => { label => 'Saisei API Password',
default => '' },
+ 'interface' => { label => 'Saisei Access Point Interface',
+ default => '' },
'debug' => { type => 'checkbox',
label => 'Enable debug warnings' },
;
Make sure you have set the up and down rate limit for the tower and the sector. This is required to be able to export the access point.
The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector. They will be modified at Saisei when modified in freeside.
Each sector will be attached to its tower access point using the Saisei uplink field.
+Each access point will be attached to the interface set in the export config. If left blank access point will be attached to the default interface. Most setups can leave this blank.
</LI>
<P>
<LI>
);
sub _export_insert {
- my ($self, $svc_broadband) = @_;
+ my ($self, $svc_broadband, $force_update) = @_;
my $rateplan_name = $self->get_rateplan_name($svc_broadband);
my $existing_rateplan;
$existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
+ die ("Please double check your credentials as ".$existing_rateplan->{message}."\n") if $existing_rateplan->{message};
+
# if no existing rate plan create one and modify it.
- $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
- $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
+ $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan->{collection};
+ $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan->{collection});
return $self->api_error if $self->{'__saisei_error'};
# set rateplan to existing one or newly created one.
- my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
+ my $rateplan = $existing_rateplan->{collection} ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
my $username = $svc_broadband->{Hash}->{svcnum};
my $description = $svc_broadband->{Hash}->{description};
+ my $svc_location = get_svc_location($self, $svc_broadband);
if (!$username) {
$self->{'__saisei_error'} = 'no username - can not export';
$existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
# if no existing user create one.
- $self->api_create_user($username, $description) unless $existing_user;
+ $self->api_create_user($username, $description, $svc_location) unless $existing_user;
return $self->api_error if $self->{'__saisei_error'};
# set user to existing one or newly created one.
tower_sector.sectorname,
tower_sector.towernum,
tower_sector.up_rate_limit as sector_upratelimit,
- tower_sector.down_rate_limit as sector_downratelimit ',
+ tower_sector.down_rate_limit as sector_downratelimit,
+ tower.latitude,
+ tower.longitude',
'addl_from' => 'LEFT JOIN tower USING ( towernum )',
'hashref' => {
'sectornum' => $svc_broadband->{Hash}->{sectornum},
},
});
+ my $tower_location;
+ $tower_location = $tower_sector->{Hash}->{latitude}.','.$tower_sector->{Hash}->{longitude} if ($tower_sector->{Hash}->{latitude} && $tower_sector->{Hash}->{longitude});
+
my $tower_name = $tower_sector->{Hash}->{towername};
$tower_name =~ s/\s/_/g;
'tower_uprate_limit' => $tower_sector->{Hash}->{tower_upratelimit},
'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
};
+ $tower_opt->{'location'} = $tower_location if $tower_location;
my $tower_ap = process_tower($self, $tower_opt);
return $self->api_error if $self->{'__saisei_error'};
'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
'rateplan' => $rateplan_name,
};
+ $sector_opt->{'location'} = $tower_location if $tower_location;
+
my $accesspoint = process_sector($self, $sector_opt);
return $self->api_error if $self->{'__saisei_error'};
'table' => 'cust_pkg',
'hashref' => { 'pkgnum' => $svc_broadband->{Hash}->{pkgnum}, },
});
+
my $virtual_ap_name = $cust_pkg->{Hash}->{custnum}.'_'.$cust_pkg->{Hash}->{pkgpart}.'_'.$svc_broadband->{Hash}->{speed_down}.'_'.$svc_broadband->{Hash}->{speed_up};
+ my $modify_existing_virtual_ap = '1' if $force_update->{'update_virtual_ap'};
my $virtual_ap_opt = {
'virtual_name' => $virtual_ap_name,
'sector_name' => $sector_name,
'virtual_uprate_limit' => $svc_broadband->{Hash}->{speed_up},
'virtual_downrate_limit' => $svc_broadband->{Hash}->{speed_down},
+ 'location' => $svc_location,
+ 'modify_existing' => $modify_existing_virtual_ap,
};
my $virtual_ap = process_virtual_ap($self, $virtual_ap_opt);
return $self->api_error if $self->{'__saisei_error'};
## tie host to user add sector name as access point.
- $self->api_add_host_to_user(
- $user->{collection}->[0]->{name},
- $rateplan->{collection}->[0]->{name},
- $svc_broadband->{Hash}->{ip_addr},
- $virtual_ap->{collection}->[0]->{name},
- ) unless $self->{'__saisei_error'};
+ my $host_opt = {
+ 'user' => $user->{collection}->[0]->{name},
+ 'rateplan' => $rateplan->{collection}->[0]->{name},
+ 'ip' => $svc_broadband->{Hash}->{ip_addr},
+ 'accesspoint' => $virtual_ap->{collection}->[0]->{name},
+ 'location' => $svc_location,
+ };
+ $self->api_add_host_to_user($host_opt)
+ unless $self->{'__saisei_error'};
}
return $self->api_error;
return;
}
+ my $tower_location;
+ $tower_location = $tower->{Hash}->{latitude}.','.$tower->{Hash}->{longitude} if ($tower->{Hash}->{latitude} && $tower->{Hash}->{longitude});
+
#modify tower or create it.
my $tower_name = $tower->{Hash}->{towername};
$tower_name =~ s/\s/_/g;
'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
'modify_existing' => '1', # modify an existing access point with this info
};
+ $tower_opt->{'location'} = $tower_location if $tower_location;
my $tower_access_point = process_tower($self, $tower_opt);
return $tower_access_point if $tower_access_point->{error};
#for each one modify or create it.
foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
+ next if $tower_sector->{Hash}->{sectorname} eq "_default";
my $sector_name = $tower_sector->{Hash}->{sectorname};
$sector_name =~ s/\s/_/g;
my $sector_opt = {
'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
'modify_existing' => '1', # modify an existing access point with this info
};
+ $sector_opt->{'location'} = $tower_location if $tower_location;
+
my $sector_access_point = process_sector($self, $sector_opt) unless ($sector_name eq "_default");
return $sector_access_point if $sector_access_point->{error};
}
return { error => $self->api_error, };
}
+sub export_user {
+ my ($self, $username, $description, $location) = @_;
+
+ $self->api_create_user($username, $description, $location);
+
+ return $self->api_error if $self->{'__saisei_error'};
+
+ return '';
+}
+
## creates the rateplan name
sub get_rateplan_name {
my ($self, $svc_broadband, $svc_name) = @_;
return;
}
else {
- $self->{'__saisei_error'} = "Received Bad response from server during $method , we received responce code: " . $client->responseCode();
+ $self->{'__saisei_error'} = "Received Bad response from server during $method $path $data, we received responce code: " . $client->responseCode() . " " . $client->responseContent;
warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
return;
}
=cut
sub api_create_user {
- my ($self,$user, $description) = @_;
+ my ($self,$user, $description, $location) = @_;
+
+ my $user_hash = {
+ 'description' => $description,
+ };
+ $user_hash->{'map_location'} = $location if $location;
my $new_user = $self->api_call(
- "PUT",
+ "PUT",
"/users/$user",
- {
- 'description' => $description,
- },
+ $user_hash,
);
$self->{'__saisei_error'} = "Saisei could not create the user $user"
}
+=head2 api_modify_user
+
+Modify a user.
+
+=cut
+
+sub api_modify_user {
+ my ($self,$user, $description, $location) = @_;
+
+ my $user_hash = {
+ 'description' => $description,
+ };
+ $user_hash->{'map_location'} = $location if $location;
+
+ my $modify_user = $self->api_call(
+ "PUT",
+ "/users/$user",
+ $user_hash,
+ );
+
+ $self->{'__saisei_error'} = "Saisei could not modify the user $user"
+ unless ($modify_user || $self->{'__saisei_error'}); # should never happen
+
+ return $modify_user;
+
+}
+
=head2 api_create_accesspoint
Creates a access point.
=cut
sub api_create_accesspoint {
- my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
+ my ($self,$accesspoint, $upratelimit, $downratelimit, $location) = @_;
+
+ my $ap_hash = {
+ 'downstream_rate_limit' => $downratelimit,
+ 'upstream_rate_limit' => $upratelimit,
+ 'interface' => $self->option('interface'),
+ };
+ $ap_hash->{'map_location'} = $location if $location;
my $new_accesspoint = $self->api_call(
"PUT",
"/access_points/$accesspoint",
- {
- 'downstream_rate_limit' => $downratelimit,
- 'upstream_rate_limit' => $upratelimit,
- },
+ $ap_hash,
);
$self->{'__saisei_error'} = "Saisei could not create the access point $accesspoint"
=cut
sub api_modify_accesspoint {
- my ($self, $accesspoint, $uplink) = @_;
+ my ($self, $accesspoint, $uplink, $location) = @_;
+
+ my $ap_hash = {
+ 'uplink' => $uplink,
+ 'interface' => $self->option('interface'),
+ };
+ $ap_hash->{'map_location'} = $location if $location;
my $modified_accesspoint = $self->api_call(
"PUT",
"/access_points/$accesspoint",
- {
- 'uplink' => $uplink, # name of attached access point
- },
+ $ap_hash,
);
$self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint after it was created."
=cut
sub api_modify_existing_accesspoint {
- my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
+ my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit, $location) = @_;
+
+ my $ap_hash = {
+ 'downstream_rate_limit' => $downratelimit,
+ 'upstream_rate_limit' => $upratelimit,
+ 'interface' => $self->option('interface'),
+# 'uplink' => $uplink, # name of attached access point
+ };
+ $ap_hash->{'map_location'} = $location if $location;
my $modified_accesspoint = $self->api_call(
"PUT",
"/access_points/$accesspoint",
- {
- 'downstream_rate_limit' => $downratelimit,
- 'upstream_rate_limit' => $upratelimit,
-# 'uplink' => $uplink, # name of attached access point
- },
+ $ap_hash,
);
- $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint."
- unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
+ $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint."
+ unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
return;
=cut
sub api_add_host_to_user {
- my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
+# my ($self,$user, $rateplan, $ip, $accesspoint, $location) = @_;
+ my ($self,$opt) = @_;
+ my $ip = $opt->{'ip'};
+ my $location = $opt->{'location'};
+
+ my $newhost_hash = {
+ 'user' => $opt->{'user'},
+ 'rate_plan' => $opt->{'rateplan'},
+ 'access_point' => $opt->{'accesspoint'},
+ };
+ $newhost_hash->{'map_location'} = $location if $location;
my $new_host = $self->api_call(
"PUT",
"/hosts/$ip",
- {
- 'user' => $user,
- 'rate_plan' => $rateplan,
- 'access_point' => $accesspoint,
- },
+ $newhost_hash,
);
$self->{'__saisei_error'} = "Saisei could not create the host $ip"
my $existing_tower_ap;
my $tower_name = $opt->{tower_name};
+ my $location = $opt->{location};
#check if tower has been set up as an access point.
$existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
'', # tower does not have a uplink on sectors.
$opt->{tower_uprate_limit},
$opt->{tower_downrate_limit},
+ $location,
) if $existing_tower_ap->{collection} && $opt->{modify_existing};
#if tower does not exist as an access point create it.
$tower_name,
$opt->{tower_uprate_limit},
$opt->{tower_downrate_limit},
+ $location,
) unless $existing_tower_ap->{collection};
my $accesspoint = $self->api_get_accesspoint($tower_name);
my $existing_sector_ap;
my $sector_name = $opt->{sector_name};
+ my $location = $opt->{location};
#check if sector has been set up as an access point.
$existing_sector_ap = $self->api_get_accesspoint($sector_name);
$opt->{tower_name},
$opt->{sector_uprate_limit},
$opt->{sector_downrate_limit},
+ $location,
) if $existing_sector_ap && $opt->{modify_existing};
#if sector does not exist as an access point create it.
$sector_name,
$opt->{sector_uprate_limit},
$opt->{sector_downrate_limit},
+ $location,
) unless $existing_sector_ap;
# Attach newly created sector to it's tower.
- $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
+ $self->api_modify_accesspoint($sector_name, $opt->{tower_name}, $location) unless ($self->{'__saisei_error'} || $existing_sector_ap);
# set access point to existing one or newly created one.
my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
return $accesspoint;
}
+=head2 get_svc_location
+
+sets location to lat and long from service, if no service location gets it from package, if still no location returns null.
+
+=cut
+
+sub get_svc_location {
+ my ($self, $svc) = @_;
+
+ my $svc_location = '';
+ $svc_location = $svc->{Hash}->{latitude}.','.$svc->{Hash}->{longitude} if ($svc->{Hash}->{latitude} && $svc->{Hash}->{longitude});
+
+ if (!$svc_location) {
+ my $pkg_location = FS::Record::qsearchs({
+ 'table' => 'cust_pkg',
+ 'addl_from' => 'LEFT JOIN cust_location USING (locationnum)',
+ 'hashref' => { 'pkgnum' => $svc->{Hash}->{pkgnum} },
+ });
+ $svc_location = $pkg_location->{Hash}->{latitude}.','.$pkg_location->{Hash}->{longitude} if ($pkg_location->{Hash}->{latitude} && $pkg_location->{Hash}->{longitude});
+ }
+
+ return $svc_location;
+}
+
=head2 require_tower_and_sector
sets whether the service export requires a sector with it's tower.
$opt->{sector_name},
$opt->{virtual_uprate_limit},
$opt->{virtual_downrate_limit},
+ $opt->{location},
) if $existing_virtual_ap && $opt->{modify_existing};
#if virtual ap does not exist as an access point create it.
$virtual_name,
$opt->{virtual_uprate_limit},
$opt->{virtual_downrate_limit},
+ $opt->{location},
) unless $existing_virtual_ap;
my $update_sector;
}
# Attach newly created virtual ap to tower sector ap or if sector has changed.
- $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
+ $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}, $opt->{location}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
# set access point to existing one or newly created one.
my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
sub export_provisioned_services {
my $job = shift;
my $param = shift;
+ my $force_update = shift;
my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n";
my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr});
die ("Please double check your credentials as ".$host->{message}."\n") if $host->{message};
warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
- my $export_error = _export_insert($part_export,$svc) unless $host->{collection};
+ my $export_error;
+ if ($force_update) { $export_error = _export_insert($part_export,$svc,$force_update); }
+ else { $export_error = _export_insert($part_export,$svc) unless $host->{collection}; }
if ($export_error) {
warn "Error exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
die ("$export_error\n");
}
+sub export_all_towers_sectors {
+ my $job = shift;
+ my $param = shift;
+
+ my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
+ or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n";
+ bless $part_export;
+
+ my @towers = FS::Record::qsearch({
+ 'table' => 'tower',
+ });
+ my $tower_count = scalar @towers;
+
+ my %status = {};
+ for (my $c=1; $c <=100; $c=$c+1) { $status{int($tower_count * ($c/100))} = $c; }
+
+ my $process_count=0;
+ foreach my $tower (@towers) {
+ if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
+ my $export_error = export_tower_sector($part_export,$tower);
+ if ($export_error->{'error'}) {
+ warn "Error exporting tower/sector (".$tower->{Hash}->{towername}.")\n" if ($part_export->option('debug'));
+ die ($export_error->{'error'}."\n");
+ }
+ $process_count++;
+ }
+
+ return;
+
+}
+
+sub force_export_all_virtual_ap {
+ my $job = shift;
+ my $param = shift;
+ my $force_update = { 'update_virtual_ap' => '1', };
+
+ export_provisioned_services($job,$param,$force_update);
+
+ return;
+}
+
+sub force_export_all_users {
+ my $job = shift;
+ my $param = shift;
+
+ my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
+ or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n";
+ bless $part_export;
+
+ my @svcparts = FS::Record::qsearch({
+ 'table' => 'export_svc',
+ 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ',
+ 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
+ });
+ my $part_count = scalar @svcparts;
+
+ my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
+
+ my @svcs = FS::Record::qsearch({
+ 'table' => 'cust_svc',
+ 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
+ 'extra_sql' => " WHERE svcpart in ('".$parts."')",
+ }) unless !$parts;
+
+ my $svc_count = scalar @svcs;
+
+ my %status = {};
+ for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
+
+ my $process_count=0;
+ foreach my $svc (@svcs) {
+ my $description = $svc->{Hash}->{description};
+ my $user = $svc->{Hash}->{svcnum};
+ my $svc_location = get_svc_location($job, $svc);
+ if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
+ warn "Exporting user ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
+ my $export_error = export_user($part_export,$user,$description, $svc_location);
+ if ($export_error) {
+ warn "Error exporting user ".$svc->{Hash}->{svcnum}."\n" if ($part_export->option('debug'));
+ die ($export_error->{'error'}."\n");
+ }
+ $process_count++;
+ }
+
+ return;
+
+}
+
sub test_export_report {
my ($self, $opts) = @_;
my @export_error;
$self->SUPER::check;
}
+=item get_phone_types
+
+returns a list of phone_types.
+
+=cut
+
+sub get_phone_types {
+ ## not using Home and Fax right now. false laziness with /elements/contact.html
+ my @phone_types = qsearch({table=>'phone_type', order_by=>'ORDER BY weight DESC', extra_sql => " WHERE typename NOT IN ('Home','Fax')"});
+ return @phone_types;
+}
+
# Used by FS::Setup to initialize a new database.
sub _populate_initial_data {
my ($class, %opts) = @_;
error_and_help( '--csv_dir is required' )
unless $csv_dir;
- error_and_help( '--start_date is required' )
+ error_and_help( '--start-date is required' )
unless $start_date;
error_and_help( '--end-date is required' )
unless $end_date;
error_and_help( '--taxnums is required' )
unless @taxnums;
- error_and_help( '--credit-reasonnum is required with --apply-credits' )
+ error_and_help( '--credit-reasonnum is required with --insert-credits' )
if $insert_credits && !$credit_reasonnum;
- error_and_help( '--credit-addlinfo is required with --apply-credits' )
+ error_and_help( '--credit-addlinfo is required with --insert-credits' )
if $insert_credits && !$credit_addlinfo;
- error_and_help( "csv dir ($csv_dir) is not a writable directoryu" )
+ error_and_help( "csv dir ($csv_dir) is not a writable directory" )
unless -d $csv_dir && -r $csv_dir;
error_and_help( "start_date($start_date) is not a valid date string")
sub usage { "
Usage:
- svc_broadband_update_speeds: [ -h help] [ -v verbose] [ -n only update services with a null up/down speed] [ -e export service ] [ -a update tower_sector_num ] [ -s service_part_num (required) ] [ -c sibling service_part_num ] [ -r (speed rate in KB 'up,down') ] [ -p (get speed from package fcc rate) ] [ -t tower_sector_num ] [ -d directory for exception file (required) ] user (required)\n
+ freeside-svcbroadband_update_speeds: [ -h help] [ -v verbose] [ -n only update services with a null up/down speed] [ -e export service ] [ -a update tower_sector_num ] [ -s service_part_num (required) ] [ -c sibling service_part_num ] [ -r (speed rate in KB 'up,down') ] [ -p (get speed from package fcc rate) ] [ -t tower_sector_num ] [ -d directory for exception file (required) ] user (required)\n
A directory for the exception file, freeside user name and a service to update is required.\n
Must set one or more of options p, c, or r. \n
Also must run this report as user freeside.\n
});
### get list of all unprovisioned services
-my $ups_extra_sql = "where cust_pkg.cancel is null and pkg_svc.quantity > 0 and pkg_svc.quantity > (select count(1) from cust_svc where cust_svc.pkgnum = cust_pkg.pkgnum and cust_svc.svcpart = pkg_svc.svcpart) and pkg_svc.svcpart = $opt_s";
-my @unprovisioned_services = qsearchs({
- 'table' => 'cust_pkg',
- 'addl_from' => 'JOIN pkg_svc using (pkgpart)',
- 'extra_sql' => $ups_extra_sql,
-});
+#my $ups_extra_sql = "where cust_pkg.cancel is null and pkg_svc.quantity > 0 and pkg_svc.quantity > (select count(1) from cust_svc where cust_svc.pkgnum = cust_pkg.pkgnum and cust_svc.svcpart = pkg_svc.svcpart) and pkg_svc.svcpart = $opt_s";
+#my @unprovisioned_services = qsearch({
+# 'table' => 'cust_pkg',
+# 'addl_from' => 'JOIN pkg_svc using (pkgpart)',
+# 'extra_sql' => $ups_extra_sql,
+#});
my $speed;
$speed = 'package' if $opt_p;
exit;
-=head2 svc_broadband_update_speeds
+=head2 freeside-svcbroadband_update_speeds
This script allows for the mas update of up and down speeds for a svc_broadband service.
Options -s, -d and freeside user are required.
example:
-sudo -u freeside ./svc_broadband_update_speeds -v -s 4 -c 2 -r 148000,248000 -p -d /home/freeside/ freesideuser
+sudo -u freeside ./freeside-svcbroadband_update_speeds -v -s 4 -c 2 -r 148000,248000 -p -d /home/freeside/ freesideuser
available options:
[ -h help]
--- /dev/null
+#!/usr/bin/env perl
+use v5.10;
+use strict;
+use warnings;
+
+our $VERSION = '1.0';
+
+use Data::Dumper;
+use FS::cust_main_county;
+use FS::Log;
+use FS::Record qw( qsearch qsearchs );
+use FS::UID qw( adminsuidsetup );
+use Getopt::Long;
+use Pod::Usage;
+
+# Begin transaction
+local $FS::UID::AutoCommit = 0;
+
+my(
+ $dbh,
+ $freeside_user,
+ $opt_check,
+ $opt_fix_usf,
+ @opt_merge,
+ $opt_merge_all,
+ @opt_set_source_null,
+);
+
+GetOptions(
+ 'check' => \$opt_check,
+ 'fix-usf' => \$opt_fix_usf,
+ 'merge=s' => \@opt_merge,
+ 'merge-all' => \$opt_merge_all,
+ 'set-source-null=s' => \@opt_set_source_null,
+);
+@opt_merge = split(',',join(',',@opt_merge));
+@opt_set_source_null = split(',',join(',',@opt_set_source_null));
+
+
+# say Dumper({
+# check => $opt_check,
+# merge => \@opt_merge,
+# set_source_numm => \@opt_set_source_null,
+# });
+
+validate_opts();
+
+$dbh = adminsuidsetup( $freeside_user )
+ or die "Bad username: $freeside_user\n";
+
+my $log = FS::Log->new('freeside-wa-tax-table-resolve');
+
+if ( $opt_check ) {
+ check();
+} elsif ( @opt_merge ) {
+ merge();
+} elsif ( @opt_set_source_null ) {
+ set_source_null();
+} elsif ( $opt_merge_all ) {
+ merge_all();
+} elsif ( $opt_fix_usf ) {
+ fix_usf();
+} else {
+ error_and_help('No options selected');
+}
+
+# Commit transaction
+$dbh->commit;
+local $FS::UID::AutoCommit = 1;
+
+exit;
+
+
+sub set_source_null {
+ my @cust_main_county;
+ for my $taxnum ( @opt_set_source_null ) {
+ my $row = qsearchs( cust_main_county => { taxnum => $taxnum } );
+ if ( $row ) {
+ push @cust_main_county, $row;
+ } else {
+ error_and_help("Invalid taxnum specified: $taxnum");
+ }
+ }
+
+ say "=== Specified tax rows ===";
+ print_taxnum($_) for @cust_main_county;
+
+ confirm_to_continue("
+
+ The source column will be set to NULL for each of the
+ tax rows listed. The tax row will no longer be managed
+ by the washington state sales tax table update utilities.
+
+ The listed taxes should be manually created taxes, that
+ were never intended to be managed by the auto updater.
+
+ ");
+
+ for my $row ( @cust_main_county ) {
+
+ $row->setfield( source => undef );
+ my $error = $row->replace;
+
+ if ( $error ) {
+ $dbh->rollback;
+
+ my $message = sprintf 'Error setting source=null taxnum %s: %s',
+ $row->taxnum, $error;
+
+ $log->error( $message );
+ say $message;
+
+ return;
+ }
+
+ my $message = sprintf 'Source column set to null for taxnum %s',
+ $row->taxnum;
+
+ $log->warn( $message );
+ say $message;
+ }
+}
+
+sub merge {
+ my $source = qsearchs( cust_main_county => { taxnum => $opt_merge[0] });
+ my $target = qsearchs( cust_main_county => { taxnum => $opt_merge[1] });
+
+ error_and_help("Invalid source taxnum: $opt_merge[0]")
+ unless $source;
+ error_and_help("Invalid target taxnum: $opt_merge[1]")
+ unless $target;
+
+ local $| = 1; # disable output buffering
+
+ say '==== source row ====';
+ print_taxnum( $source );
+
+ say '==== target row ====';
+ print_taxnum( $target );
+
+ confirm_to_continue("
+
+ The source tax will be merged into the target tax.
+ All references to the source tax on customer invoices
+ will be replaced with references to the target tax.
+ The source tax will be removed from the tax tables.
+
+ ");
+
+ merge_into( $source, $target );
+}
+
+sub merge_into {
+ my ( $source, $target ) = @_;
+
+ local $@;
+ eval { $source->_merge_into( $target, { identical_record_check => 0 } ) };
+ if ( $@ ) {
+ $dbh->rollback;
+
+ my $message = sprintf 'Failed to merge wa sales tax %s into %s: %s',
+ $source->taxnum, $target->taxnum, $@;
+
+ say $message;
+ $log->error( $message );
+
+ } else {
+ my $message = sprintf 'Merged wa sales tax %s into %s for district %s',
+ $source->taxnum, $target->taxnum, $source->district;
+
+ say $message;
+ $log->warn( $message );
+ }
+}
+
+sub merge_all {
+ my @dupes = FS::cust_main_county->find_wa_tax_dupes;
+
+ unless ( @dupes ) {
+ say 'No duplicate tax rows detected for WA sales tax districts';
+ return;
+ }
+
+ confirm_to_continue(sprintf "
+
+ %s blocking duplicate rows detected
+
+ Duplicate rows will be merged using FS::cust_main_county::_merge_into()
+
+ Rows are considered duplicates when they:
+ - Share the same tax class
+ - Share the same district
+ - Contain 'wa_sales' in the source column
+
+ ", scalar @dupes);
+
+ # Sort dupes into buckets to be merged, by taxclass and district
+ # $to_merge{taxclass}->{district} = [ @rows_to_merge ]
+ my %to_merge;
+ for my $row ( @dupes ) {
+ my $taxclass = $row->taxclass || 'none';
+ $to_merge{$taxclass} ||= {};
+ $to_merge{$taxclass}->{$row->district} ||= [];
+ push @{ $to_merge{$taxclass}->{$row->district} }, $row;
+ }
+
+ # Merge the duplicates
+ for my $taxclass ( keys %to_merge ) {
+ for my $district ( keys %{ $to_merge{$taxclass} }) {
+
+ # Keep the first row in the list as the target.
+ # Merge the remaining rows into the target
+ my $rows = $to_merge{$taxclass}->{$district};
+ my $target = shift @$rows;
+
+ while ( @$rows ) {
+ merge_into( shift(@$rows), $target );
+ }
+ }
+ }
+
+ say "
+
+ Merge operations completed
+
+ Please run freeside-wa-tax-table-update. This will update
+ the merged district rows with correct county and city names
+
+ ";
+
+}
+
+sub fix_usf {
+ confirm_to_continue("
+
+ Search for duplicate districts within the tax tables with
+ - duplicate district column values
+ - source = NULL
+ - district = NOT NULL
+ - taxclass = USF
+ - tax > 17
+
+ Merge these rows into a single USF row for each tax district
+
+ ");
+
+ my @rows = qsearch( cust_main_county => {
+ taxclass => 'USF',
+ source => undef,
+ state => 'WA',
+ country => 'US',
+ tax => { op => '>', value => 17 },
+ district => { op => '!=', value => undef },
+ });
+
+ my %to_merge;
+ for my $row (@rows) {
+ $to_merge{$row->district} ||= [];
+ push @{ $to_merge{$row->district} }, $row;
+ }
+
+ for my $dist_rows ( values %to_merge ) {
+ my $target = shift @$dist_rows;
+ while ( @$dist_rows ) {
+ merge_into( shift(@$dist_rows), $target );
+ }
+ }
+
+ say "
+
+ USF clean up completed
+
+ Please run freeside-wa-tax-table-update. This will update
+ the merged district rows with correct county and city names
+
+ ";
+}
+
+sub validate_opts {
+
+ $freeside_user = shift @ARGV
+ or error_and_help('freeside_user parameter required');
+
+ if ( @opt_merge ) {
+ error_and_help(( '--merge requires a comma separated list of two taxnums'))
+ unless scalar(@opt_merge) == 2
+ && $opt_merge[0] =~ /^\d+$/
+ && $opt_merge[1] =~ /^\d+$/;
+ }
+
+ for my $taxnum ( @opt_set_source_null ) {
+ if ( $taxnum =~ /\D/ ) {
+ error_and_help( "Invalid taxnum ($taxnum)" );
+ }
+ }
+}
+
+sub check {
+ my @dupes = FS::cust_main_county->find_wa_tax_dupes;
+
+ unless ( @dupes ) {
+ say 'No duplicate tax rows detected for WA sales tax districts';
+ return;
+ }
+
+ say sprintf '=== Detected %s duplicate tax rows ===', scalar @dupes;
+
+ print_taxnum($_) for sort { $a->district <=> $b->district } @dupes;
+
+ $log->error(
+ sprintf 'Detected %s duplicate wa sales tax rows: %s',
+ scalar( @dupes ),
+ join( ',', map{ $_->taxnum } @dupes )
+ );
+
+ say "
+
+ Rows are considered duplicates when they:
+ - Share the same tax class
+ - Share the same district
+ - Contain 'wa_sales' in the source column
+
+ ";
+}
+
+sub print_taxnum {
+ my $taxnum = shift;
+ die unless ref $taxnum;
+
+ say 'taxnum: '.$taxnum->taxnum;
+ say join "\n" => (
+ map { sprintf(' %s:%s', $_, $taxnum->$_ ) }
+ qw/district city county state tax taxname taxclass source/
+ );
+ print "\n";
+}
+
+sub confirm_to_continue {
+ say shift;
+ print "Confirm: [y/N]: ";
+ my $yn = <STDIN>;
+ chomp $yn;
+ if ( lc $yn ne 'y' ) {
+ say "\nAborted\n";
+ exit;
+ }
+}
+
+sub error_and_help {
+ pod2usage({
+ -message => sprintf( "\n\nError:\n\t%s\n\n", shift ),
+ -exitval => 2,
+ verbose => 1,
+ });
+ exit;
+}
+
+__END__
+
+=head1 name
+
+freeside-wa-tax-table-resolve
+
+=head1 SYNOPSIS
+
+ freeside-wa-tax-table-resolve --help
+ freeside-wa-tax-table-resolve --check [freeside_user]
+ freeside-wa-tax-table-resolve --merge 123,234 [freeside_user]
+ freeside-wa-tax-table-resolve --set-source-null 1337,6553 [freeside_user]
+ freeside-wa-tax-table-resolve --merge-all [freeside_user]
+ freeside-wa-tax-table-resolve --fix-usf [freeside_user]
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<--help>
+
+Display help and exit
+
+=item B<--check>
+
+Display info on any taxnums considered blocking duplicates
+
+=item B<--merge> [source-taxnum],[target-taxnum]
+
+Update all records referring to [source-taxnum], so they now
+refer to [target-taxnum]. [source-taxnum] is deleted.
+
+Used to merge duplicate taxnums
+
+=item B<--set-source-null> [taxnum],[taxnum],...
+
+Update all records for the given taxnums, by setting the
+I<source> column to NULL.
+
+Used for manually entered tax entries, incorrectly labelled
+as created and managed for Washington State Sales Taxes
+
+=item B<--merge-all>
+
+Automatically merge all blocking duplicate taxnums.
+
+If after reviewing all blocking duplicate taxnum rows with --check,
+if all duplicate rows are safe to merge, this option will merge them all.
+
+=item B<--fix-usf>
+
+Fix routine for a particular USF issue
+
+Search for duplicate districts within the tax tables with
+
+ - duplicate district column values
+ - source = NULL
+ - district = NOT NULL
+ - taxclass = USF
+ - tax > 17
+
+Merge these rows into a single USF row for each tax district
+
+=back
+
+=head1 DESCRIPTION
+
+Tool to resolve tax table issues for customer using Washington state
+sales tax districts.
+
+If Freeside detects duplicate rows within the wa sales tax tables,
+tax table updates are blocked, and a log message directs the
+sysadmin to this tool.
+
+Duplicate rows may be manually entered taxes, not related
+to WA sales tax. Or duplicate rows may have been manually entered
+into freeside for other tax purposes.
+
+Use --check to display which tax entries were detected as dupes.
+
+For each tax entry, decide if it is a duplicate wa sales tax entry,
+or some other manually entered tax.
+
+if the row is a duplicate, merge the duplicates with the --merge
+option of this script
+
+If the row is a manually entered tax, not for WA state sales taxes,
+keep the tax but remove the flag incorrectly labeling it as WA state
+sales taxes with the --set-source-null option of this script
+
+Once --check no longer returns problematic tax entries, the
+wa state tax tables will be able to complete their automatic
+tax rate updates
+
+=cut
https://dor.wa.gov/sites/default/files/legacy/downloads/Add_DataRates2018Q4.zip
+=item Other district tax rows
+
+When this tool updates the tax tables, any additional tax table rows with
+a district set, where the 'source' column is not 'wa_sales', will have the
+country, state, county, and city values kept updated to match the data
+provided in the state tax tables
=item Address lookup API tool
};
if ( $@ ) {
- $log->error( "Error: $@" );
warn "Error: $@\n";
+ $log->error( "Error: $@" );
} else {
$log->info( 'Finished wa_tax_rate_update' );
warn "Finished wa_tax_rate_update\n";
my $prefix = exists($param->{'prefix'}) ? $param->{'prefix'} : '';
- my $countyflag = 0;
+ my $disabled = $param->{'disabled'};
+
+ my $countyflag = $param->{selected_county} ? 1 : 0;
+ my $cityflag = $param->{selected_city} ? 1 : 0;
my %cust_main_county;
foreach my $c ( @{ $param->{'locales'} } ) {
#$countyflag=1 if $c->county;
$countyflag=1 if $c->{county};
+ $cityflag=1 if ($c->{city} && $cityflag);
#push @{$cust_main_county{$c->country}{$c->state}}, $c->county;
#$cust_main_county{$c->country}{$c->state}{$c->county} = 1;
- $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}} = 1;
+ $cust_main_county{$c->{country}}{$c->{state}}{$c->{county}}{$c->{city}} = 1;
}
# }
- $countyflag=1 if $param->{selected_county};
my $script_html = <<END;
<SCRIPT>
- function opt(what,value,text) {
- var optionName = new Option(text, value, false, false);
+ function opt(what,value,text,selected) {
+ var optionName = new Option(text, value, false, selected);
var length = what.length;
what.options[length] = optionName;
}
#foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
my $text = $county || '(n/a)';
- $script_html .=
- qq!opt(what.form.${prefix}county, "$county", "$text");\n!;
+ if (!$county) {
+ if ( $cityflag) {
+ $script_html .= qq!what.form.${prefix}city.style.display='';\n
+ what.form.${prefix}city_select.style.display='none';\n!
+ }
+ $script_html .= qq!opt(what.form.${prefix}county, "$county", "$text");\n!
+ #$script_html .= qq!what.form.${prefix}county.style.display='none';\n!
+ }
+ else {
+ $script_html .= qq!var countySelected = false; if ("$param->{selected_county}" == "$text") { countySelected = true; }\n
+ opt(what.form.${prefix}county, "$county", "$text", countySelected);\n
+ what.form.${prefix}county.style.display='';\n
+ county = what.form.${prefix}county.options[what.form.${prefix}county.selectedIndex].text;\n!;
+ if ( $cityflag) {
+ $script_html .= qq!\nif ( county == \"$county\" ) {\n!;
+ foreach my $city ( sort keys %{$cust_main_county{$country}{$state}{$county}} ) {
+ my $text = $city || '(n/a)';
+ if (!$city) {
+ $script_html .= qq!what.form.${prefix}city.style.display='';\n
+ what.form.${prefix}city_select.style.display='none';\n!
+ }
+ else {
+ $script_html .= qq!var citySelected = false; if ("$param->{selected_city}" == "$text") { citySelected = true; }\n
+ opt(what.form.${prefix}city_select, "$city", "$text", citySelected);\n
+ what.form.${prefix}city.style.display='none';\n
+ what.form.${prefix}city_select.style.display='';\n!
+ }
+ }
+ $script_html .= "}\n";
+ }
+ }
}
$script_html .= "}\n";
}
$script_html .= <<END;
}
+ function ${prefix}county_changed(what) {
+END
+
+ if ( $cityflag) {
+ $script_html .= <<END;
+ saved_city = "$param->{selected_city}";
+ county = what.options[what.selectedIndex].text;
+ state = what.form.${prefix}state.options[what.form.${prefix}state.selectedIndex].text;
+ country = what.form.${prefix}country.options[what.form.${prefix}country.selectedIndex].text;
+ for ( var i = what.form.${prefix}city_select.length; i >= 0; i-- )
+ what.form.${prefix}city_select.options[i] = null;
+END
+
+ foreach my $country ( sort keys %cust_main_county ) {
+ $script_html .= "\nif ( country == \"$country\" ) {\n";
+ foreach my $state ( sort keys %{$cust_main_county{$country}} ) {
+ $script_html .= "\nif ( state == \"$state\" ) {\n";
+ #foreach my $county ( sort @{$cust_main_county{$country}{$state}} ) {
+ foreach my $county ( sort keys %{$cust_main_county{$country}{$state}} ) {
+ $script_html .= "\nif ( county == \"$county\" ) {\n";
+ foreach my $city ( sort keys %{$cust_main_county{$country}{$state}{$county}} ) {
+ my $text = $city || '(n/a)';
+ if (!$city) {
+ $script_html .= qq!what.form.${prefix}city.style.display='';\n
+ what.form.${prefix}city_select.style.display='none';\n!
+ }
+ else {
+ $script_html .= qq!var citySelected = false; if (saved_city == "$text") { citySelected = true; }\n
+ opt(what.form.${prefix}city_select, "$city", "$text", citySelected);\n
+ what.form.${prefix}city.style.display='none';\n
+ what.form.${prefix}city_select.style.display='';\n!
+ }
+ }
+ $script_html .= "}\n";
+ }
+ $script_html .= "}\n";
+ }
+ $script_html .= "}\n";
+ }
+ }
+
+ $script_html .= <<END;
+ }
+ function ${prefix}city_select_changed(what) {
+END
+
+ if ( $cityflag ) {
+ $script_html .= <<END;
+ what.form.${prefix}city.value = what.options[what.selectedIndex].value;
+END
+ }
+
+ $script_html .= <<END;
+ }
</SCRIPT>
END
+ my $city_html = '';
+ if ( $cityflag ) {
+ if ( scalar (keys %{ $cust_main_county{$param->{'selected_country'}}{$param->{'selected_state'}}{$param->{'selected_county'}} }) > 1 ) {
+ $city_html .= qq!<SELECT NAME="${prefix}city_select" onChange="${prefix}city_select_changed(this); $param->{'onchange'}">!;
+ foreach my $city (
+ sort keys %{ $cust_main_county{$param->{'selected_country'}}{$param->{'selected_state'}}{$param->{'selected_county'}} }
+ ) {
+ my $text = $city || '(n/a)';
+ $city_html .= qq!<OPTION VALUE="$city"!.
+ ($city eq $param->{'selected_city'} ?
+ ' SELECTED>' :
+ '>'
+ ).
+ $text;
+ }
+ $city_html .= qq!</OPTION><INPUT TYPE="text" ID="${prefix}city" NAME="${prefix}city" VALUE="$param->{'selected_city'}" style="display:none">!;
+ } else {
+ $city_html .= qq!<SELECT NAME="${prefix}city_select" onChange="${prefix}city_select_changed(this); $param->{'onchange'}" style="display:none"></SELECT>
+ <INPUT TYPE="text" ID="${prefix}city" NAME="${prefix}city" VALUE="$param->{'selected_city'}" style="display:''">!;
+ }
+ }
+
my $county_html = $script_html;
if ( $countyflag ) {
- $county_html .= qq!<SELECT NAME="${prefix}county" onChange="$param->{'onchange'}">!;
+ $county_html .= qq!<SELECT NAME="${prefix}county" !.
+ qq!onChange="${prefix}county_changed(this); $param->{'onchange'}">!;
foreach my $county (
sort keys %{ $cust_main_county{$param->{'selected_country'}}{$param->{'selected_state'}} }
) {
}
- ($county_html, $state_html, $country_html);
+ ($county_html, $state_html, $country_html, $city_html);
}
</TR>
<TR>
- <TH ALIGN="right"><%=$r%>City</TH>
- <TD>
- <INPUT TYPE="text" ID="<%=$pre%>city" NAME="<%=$pre%>city" VALUE="<%= encode_entities(${$pre.'city'}) %>" onChange="<%= $onchange %>" <%=$disabled%>>
- </TD>
<%=
- ($county_html, $state_html, $country_html) =
+ ($county_html, $state_html, $country_html, $city_html) =
FS::SelfService::regionselector( {
prefix => $pre,
+ selected_city => ${$pre.'city'},
selected_county => ${$pre.'county'},
selected_state => ${$pre.'state'},
selected_country => ${$pre.'country'},
locales => \@cust_main_county,
} );
+ $OUT .= qq!<TH ALIGN="right">${r}City</TH>!;
+ $OUT .= qq!<TD>$city_html</TD>!;
$OUT .= qq!<TH ALIGN="right">${r}State/County</TH>!;
$OUT .= qq!<TD>$county_html $state_html</TD>!;
$OUT .= qq!<TH>${r}Zip</TH>!;
<%=
if ( $disabled ) {
$OUT .= qq!var what = document.getElementById("${pre}city");!;
- for (qw( county state country ) ) {
+ for (qw( city county state country ) ) {
$OUT .= "what.form.$pre$_.disabled = true;";
$OUT .= "what.form.$pre$_.style.backgroundColor = '#dddddd';";
}
}
} elsif ( $type eq 'checkbox' ) {
if ( defined $cgi->param($i->key.$n) ) {
- push @touch, $i->key;
+ my $error = &{$i->validate}('', $n) if $i->validate;
+ push @error, $error if $error;
+ push @touch, $i->key if !$error;
} else {
push @delete, $i->key;
}
if ( scalar(@{[ $cgi->param($i->key.$n) ]}) && $cgi->param($i->key.$n) ne '' ) {
my $error = &{$i->validate}([ $cgi->param($i->key.$n) ], $n) if $i->validate;
push @error, $error if $error;
- $conf->set($i->key, join("\n", @{[ $cgi->param($i->key.$n) ]} ), $agentnum);
+ $conf->set($i->key, join("\n", @{[ $cgi->param($i->key.$n) ]} ), $agentnum) if !$error;
} else {
$conf->delete($i->key, $agentnum);
}
'cust_pay' => [
'$paynum' => 'Payment#',
'$paid' => 'Amount',
+ '$processing_fee' => 'Processing fee',
'$payby' => 'Payment method',
'$date' => 'Payment date',
'$payinfo' => 'Card/account# (masked)',
Enclose substitutions and other Perl expressions in braces:
<BR>{ $name } = ExampleCo (Smith, John)
<BR>{ time2str("%D", time) } = '.time2str("%D", time).'
+<BR>{ "processing fee of $processing_fee" if $processing_fee; } = Will display text if there is a processing fee
</P>';
$sidebar .= include('/elements/template_image-dialog.html',
'callback' => 'insertHtml'
$html .= '<TR><TD ALIGN="left" COLSPAN=2>' .
include('/elements/progress-init.html',
$part_export->exporttype,
- [ $script.'_exportnum', $script.'_script' ],
+ [ $script.'_exportnum' ],
rooturl().'view/svc_export/run_script.cgi',
{
'error_url' => rooturl().$exports->{$layer}{scripts}{$script}->{error_url}."exportnum=".$part_export->{Hash}->{exportnum},
$script,
) .
'<INPUT TYPE="hidden" NAME="'.$script.'_exportnum" VALUE="'.$part_export->{Hash}->{exportnum}.'">
- <INPUT TYPE="hidden" NAME="'.$script.'_script" VALUE="'.$script.'">
<A HREF="#" onClick="'.$script.'process();">'.$exports->{$layer}{scripts}{$script}->{html_label}.'</A></TD></TR>';
}
$1;
} @expansion;
-foreach ( @expansion ) {
- my(%hash)=$cust_main_county->hash;
- my($new)=new FS::cust_main_county \%hash;
- $new->setfield('taxnum','');
- $new->setfield('taxclass', '');
- if ( $cgi->param('what') eq 'state' ) { #??
- $new->setfield('state',$_);
- $new->setfield('county', '');
- $new->setfield('city', '');
- } elsif ( $cgi->param('what') eq 'county' ) {
- $new->setfield('county',$_);
- $new->setfield('city', '');
- } elsif ( $cgi->param('what') eq 'city' ) {
- #uppercase cities in the US to try and agree with USPS validation
- $new->setfield('city', $new->country eq 'US' ? uc($_) : $_ );
- } else { #???
- die 'unknown what '. $cgi->param('what');
+my $what = $cgi->param('what');
+foreach my $new_tax_area ( @expansion ) {
+
+ # Clone specific tax columns from original tax row
+ #
+ # UI Note: Preserving original behavior, of cloning
+ # tax amounts into new tax record, against better
+ # judgement. If the new city/county/state has a
+ # different tax value than the one being populated
+ # (rather likely?) now the user must remember to
+ # revisit each newly created tax row, and correct
+ # the possibly incorrect tax values that were populated.
+ # Values would be easier to identify and correct if
+ # they were initially populated with 0% tax rates
+ # District Note: The 'district' column is NOT cloned
+ # to the new tax row. Manually entered taxes
+ # are not be divided into road maintenance districts
+ # like Washington state sales taxes
+ my $new = FS::cust_main_county->new({
+ map { $_ => $cust_main_county->getfield($_) }
+ qw/
+ charge_prediscount
+ exempt_amount
+ exempt_amount_currency
+ recurtax
+ setuptax
+ tax
+ taxname
+ /
+ });
+
+ # Clone additional location columns, based on the $what value
+ my %clone_cols_for = (
+ state => [qw/country /],
+ county => [qw/country state/],
+ city => [qw/country state county/],
+ );
+
+ die "unknown what: $what"
+ unless grep { $_ eq $what } keys %clone_cols_for;
+
+ $new->setfield( $_ => $cust_main_county->getfield($_) )
+ for @{ $clone_cols_for{ $cgi->param('what') } };
+
+ # In the US, store cities upper case for USPS validation
+ $new_tax_area = uc($new_tax_area)
+ if $what eq 'city'
+ && $new->country eq 'US';
+
+ $new->setfield( $what, $new_tax_area );
+ if ( my $error = $new->insert ) {
+ die $error;
}
- my $error = $new->insert;
- die $error if $error;
}
</%init>
<% $select_style %>
>
+% if ( $opt{city} ) {
+ <OPTION VALUE="<% $opt{city} %>" SELECTED><% $opt{city} %></OPTION>
+% }
+
% unless ( $opt{'disable_empty'} ) {
<OPTION VALUE="" <% $opt{city} eq '' ? 'SELECTED' : '' %>><% $opt{empty_label} %></OPTION>
% }
}
my $first = 0;
-foreach my $phone_type ( qsearch({table=>'phone_type', order_by=>'weight'}) ) {
+foreach my $phone_type ( FS::phone_type->get_phone_types() ) {
next if $phone_type->typename =~ /^(Home|Fax)$/;
my $f = 'phonetypenum'.$phone_type->phonetypenum;
$label{$f} = $phone_type->typename. ' phone';
}
}
+ Hash.push('key', '<%$key%>');
+
// jsrsPOST = true;
// jsrsExecute( '<% $action %>', <%$key%>myCallback, 'start_job', Hash );
<%init>
my( $cust_fields, %opt ) = @_;
- use FS::ConfDefaults;
- $opt{'avail_fields'} ||= [ FS::ConfDefaults->cust_fields_avail() ];
+ my @fields = FS::ConfDefaults->cust_fields_avail();
+ my $contact_phone_list;
+ foreach my $phone_type ( FS::phone_type->get_phone_types() ) {
+ $contact_phone_list .= " | Contact ".$phone_type->typename." phone(s)";
+ }
+ @fields = map {s/ \| Contact phone\(s\)/$contact_phone_list/g; $_; } @fields;
+
+ $opt{'avail_fields'} ||= [ @fields ];
tie my %hash, 'Tie::IxHash', @{ $opt{'avail_fields'} };
</%init>
<% $money_char %><INPUT NAME = "amount"
ID = "amount"
TYPE = "text"
- VALUE = "<% $amount %>"
+ VALUE = "0.00"
SIZE = 8
STYLE = "text-align:right;"
% if ( $fee || $surcharge_percentage || $surcharge_flatfee || $processing_fee) {
<TD>
<TABLE><TR>
<TD BGCOLOR="#ffffff">
- <INPUT TYPE="checkbox" NAME="processing_fee" ID="processing_fee" VALUE="<% $processing_fee %>" onclick="<% $opt{prefix} %>process_fee_changed()">
+ <INPUT TYPE="checkbox" NAME="processing_fee" ID="processing_fee" VALUE="<% $processing_fee %>" onclick="<% $opt{prefix} %>process_fee_changed()" checked>
</TD>
<TD ID="ajax_processingfee_cell" BGCOLOR="#dddddd" STYLE="border:1px solid blue">
<FONT SIZE="+1">A processing fee of <% $processing_fee %> is being applied to this transaction.</FONT>
$amount += $surcharge;
+ $amount += $processing_fee; ## needed if processing fee is checked on default.
+
$amount = sprintf("%.2f", $amount);
}
my( $cust_fields, %opt ) = @_;
-$opt{'avail_fields'} ||= [ FS::ConfDefaults->cust_fields_avail() ];
+my @fields = FS::ConfDefaults->cust_fields_avail();
+my $contact_phone_list;
+foreach my $phone_type ( FS::phone_type->get_phone_types() ) {
+ $contact_phone_list .= " | Contact ".$phone_type->typename." phone(s)";
+}
+@fields = map {s/ \| Contact phone\(s\)/$contact_phone_list/g; $_; } @fields;
+
+$opt{'avail_fields'} ||= [ @fields ];
</%init>
$('#payment_option_row').<% $payment_option_row %>();
$('#payment_amount_row').<% $payment_amount_row %>();
- $('#ajax_processingfee_cell').hide();
+ $('#ajax_processingfee_cell').show();
if($('#payment_amount_row').is(':visible')) {
var surcharge;
%saveopt = map { $_ => scalar($cgi->param($_)) } @{$payby2fields{$payby}};
}
- my $error = $cust_main->save_cust_payby(
- 'saved_cust_payby' => \$cust_payby,
- 'payment_payby' => $payby,
- 'auto' => scalar($cgi->param('auto')),
- 'weight' => scalar($cgi->param('weight')),
- 'payinfo' => $payinfo,
- 'payname' => $payname,
- %saveopt
- );
+ my $error;
+ {
+ local $@;
+ eval {
+ $error = $cust_main->save_cust_payby(
+ 'saved_cust_payby' => \$cust_payby,
+ 'payment_payby' => $payby,
+ 'auto' => scalar($cgi->param('auto')),
+ 'weight' => scalar($cgi->param('weight')),
+ 'payinfo' => $payinfo,
+ 'payname' => $payname,
+ %saveopt
+ );
+ };
+ $error ||= $@;
+ }
errorpage("error saving info, payment not processed: $error")
if $error;
# Catch destination values from dest multi-checkbox, default to message
# irrelevant to prospect contacts
my @dest = grep{ /^(message|invoice)$/ } $cgi->param('dest');
-@dest = ('message') unless @dest;
# Cache the contact_class table
my %classname =
if (@dest && $link eq 'cust_main') {
my @stm;
push @stm, "cust_contact.${_}_dest IS NOT NULL" for @dest;
- $extra_sql .= "\nAND (".join(' OR ',@stm).') ';
+ $extra_sql .= "\nAND (".join(' AND ',@stm).') ';
}
if ($DEBUG) {
# Prepare to display phone numbers
# adds 3 additional queries per table record :-(
-my %phonetype = (qw/1 Work 2 Home 3 Mobile 4 Fax/);
-my %phoneid = (qw/Work 1 Home 2 Mobile 3 Fax 4/);
my $get_phone_sub = sub {
my $type = shift;
return sub {
my $rec = shift;
my @p = qsearch('contact_phone', {
contactnum => $rec->contact_contactnum,
- phonetypenum => $phoneid{$type}
+ phonetypenum => $type,
});
- @p ? (join ', ',map{$_->phonenum} @p) : undef;
+ @p ? (join ', ',map{$_->phonenum_pretty} @p) : undef;
};
};
+my @phones;
+foreach my $phone_type ( FS::phone_type->get_phone_types() ) {
+ push @phones, { label => $phone_type->typename.' Phone', field => $get_phone_sub->($phone_type->phonetypenum), };
+}
+
# Cache contact types
my %classname =
map {$_->classnum => $_->classname}
{ label => 'Last', field => 'contact_last' },
{ label => 'Title', field => 'contact_title' },
{ label => 'E-Mail', field => 'contact_email_emailaddress' },
- { label => 'Work Phone', field => $get_phone_sub->('Work') },
- { label => 'Mobile Phone', field => $get_phone_sub->('Mobile') },
- { label => 'Home Phone', field => $get_phone_sub->('Home') },
+ @phones,
{ label => 'Type',
field => sub {
my $rec = shift;
my @scalars = qw (
agentnum salesnum status
address city county state zip country location_history
+ daytime night mobile fax
invoice_terms
no_censustract with_geocode with_email tax no_tax POST no_POST
custbatch usernum
--- /dev/null
+<TR>
+ <TH VALIGN="top" ALIGN="right"><% mt('Phones') |h %></TD>
+ <TD COLSPAN=6>
+ <TABLE CELLSPACING=0 CELLPADDING=0>
+ <TR>
+% foreach my $phone (qw(daytime night mobile fax)) {
+ <TD>
+ <INPUT TYPE="text"
+ NAME="<% $phone %>"
+ VALUE=""
+ SIZE=18
+ >
+ <BR><FONT SIZE=-1 COLOR="#333333"><% mt($phone_label{$phone}) |h %></FONT>
+ </TD>
+ <TD> </TD>
+% }
+ </TR>
+ </TABLE>
+ </TD>
+</TR>
+<%init>
+my %phone_label = (
+ daytime => 'Day Phone',
+ night => 'Night Phone',
+ mobile => 'Mobile Phone',
+ fax => 'Fax Number',
+);
+</%init>
\ No newline at end of file
<TH ALIGN="right" VALIGN="center"><% mt('Email') |h %></TH>
<TD><INPUT TYPE="text" NAME="<%$field_prefix%>email" SIZE=54></TD>
</TR>
-
- <TR>
- <TH ALIGN="right" VALIGN="center"><% mt('Home Phone') |h %></TH>
- <TD><INPUT TYPE="text" NAME="<%$field_prefix%>homephone" SIZE=54></TD>
- </TR>
-
- <TR>
- <TH ALIGN="right" VALIGN="center"><% mt('Work Phone') |h %></TH>
- <TD><INPUT TYPE="text" NAME="<%$field_prefix%>workphone" SIZE=54></TD>
- </TR>
-
+% foreach my $phone_type ( FS::phone_type->get_phone_types() ) {
<TR>
- <TH ALIGN="right" VALIGN="center"><% mt('Mobile Phone') |h %></TH>
- <TD><INPUT TYPE="text" NAME="<%$field_prefix%>mobilephone" SIZE=54></TD>
+ <TH ALIGN="right" VALIGN="center"><% $phone_type->typename. ' Phone' |h %></TH>
+ <TD><INPUT TYPE="text" NAME="<% $field_prefix %>phonetypenum<% $phone_type->phonetypenum %>" SIZE=54></TD>
</TR>
+% }
<%init>
<FONT CLASS="fsinnerbox-title"><% emt('Location search options') %></FONT>
<TABLE CLASS="fsinnerbox">
<& elements/options_cust_location.html &>
+ <& elements/cust_main_phones.html &>
</TABLE>
<BR>
<%$th%>Send messages</TH>
<%$th%>Self-service</TH>
% foreach my $phone_type (@phone_type) {
- <%$th%><% $phone_type->typename |h %></TH>
+ <%$th%><% $phone_type->typename |h %> phone</TH>
% }
<%$th%>Comment</TH>
</TR>
%}
<%once>
-my @phone_type = qsearch({table=>'phone_type', order_by=>'weight'});
+my @phone_type = FS::phone_type->get_phone_types();
</%once>
<%init>
}
}
-my $exportnum;
-my $method;
-for (grep /^*_script$/, keys %param) {
- $exportnum = $param{$param{$_}.'_exportnum'};
- $method = $param{$param{$_}.'_script'};
-}
+my $run_script = $param{'key'};
+my $exportnum = $param{$run_script.'_exportnum'};
my $part_export = qsearchs('part_export', { 'exportnum'=> $exportnum, } )
or die "unknown exportnum $exportnum";
-my $class = 'FS::part_export::'.$part_export->{Hash}->{exporttype}.'::'.$method;
+my $class = 'FS::part_export::'.$part_export->{Hash}->{exporttype}.'::'.$run_script;
my $server = new FS::UI::Web::JSRPC $class, $cgi;