'View package definition costs', #NEWNEW
'Change package start date',
'Change package contract end date',
+ 'Unmask customer DL',
+ 'Unmask customer SSN',
],
###
=cut
1;
-
for (@cust_main_editable_fields) {
$return{$_} = $cust_main->get($_);
}
+ $return{$_} = $cust_main->masked($_) for qw/ss stateid/;
+
#maybe a little more expensive, but it should be cached by now
for (@location_editable_fields) {
$return{$_} = $cust_main->bill_location->get($_)
})
or return { 'error' => 'unknown custpaybynum '. $p->{'custpaybynum'} };
+ my $cust_main = qsearchs( 'cust_main', {custnum => $cust_payby->custnum} )
+ or return { 'error' => 'unknown custnum '.$cust_payby->custnum };
+
foreach my $field (
qw( weight payby payinfo paycvv paydate payname paystate paytype payip )
) {
next unless exists($p->{$field});
$cust_payby->set($field,$p->{$field});
}
+ $cust_payby->set( 'paymask' => $cust_payby->mask_payinfo );
- my $error = $cust_payby->replace;
- if ( $error ) {
- return { 'error' => $error };
- } else {
- return { 'custpaybynum' => $cust_payby->custpaybynum };
+ # Update column if given a value, and the given value wasn't
+ # the value generated by $cust_main->masked($column);
+ $cust_main->set( $_, $p->{$_} )
+ for grep{ $p->{$_} !~ /^x/i; }
+ grep{ exists $p->{$_} }
+ qw/ss stateid/;
+
+ # Perform updates within a transaction
+ local $FS::UID::AutoCommit = 0;
+
+ if ( my $error = $cust_payby->replace || $cust_main->replace ) {
+ dbh->rollback;
+ return { error => $error };
}
-
+
+ dbh->commit;
+ return { custpaybynum => $cust_payby->custpaybynum };
}
sub verify_payby {
}
1;
-
},
{
+ 'key' => 'manual_process-single_invoice_amount',
+ 'section' => 'deprecated',
+ 'description' => 'When entering manual credit card and ACH payments, amount will not autofill if the customer has more than one open invoice',
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'manual_process-pkgpart',
'section' => 'payments',
'description' => 'Package to add to each manual credit card and ACH payment entered by employees from the backend. WARNING: Although recently permitted to US merchants in general, specific consumer protection laws may prohibit or restrict this practice in California, Colorado, Connecticut, Florda, Kansas, Maine, Massachusetts, New York, Oklahome, and Texas. Surcharging is also generally prohibited in most countries outside the US, AU and UK.',
$self->ip_addr('');
}
+ # strip user-entered leading 0's from IPv4 addresses
+ # Parsers like NetAddr::IP interpret them as octal instead of decimal
+ $self->ip_addr(
+ join( '.', (
+ map{ int($_) }
+ split( /\./, $self->ip_addr )
+ ))
+ ) if $self->ip_addr =~ /\./ && $self->ip_addr =~ /[\.^]0/;
+
if ( $self->ip_addr
and !$self->router
and $self->conf->exists('auto_router') ) {
FS::router->by_key($self->routernum);
}
-=item used_addresses [ BLOCK ]
+=item used_addresses [ FS::addr_block ]
+
+Returns a list of all addresses in use within the given L<FS::addr_block>.
-Returns a list of all addresses (in BLOCK, or in all blocks)
-that are in use. If called as an instance method, excludes
-that instance from the search.
+If called as an instance method, excludes that instance from the search.
=cut
sub used_addresses {
- my $self = shift;
- my $block = shift;
- return ( map { $_->_used_addresses($block, $self) } @subclasses );
+ my ($self, $block) = @_;
+
+ (
+ $block->ip_gateway ? $block->ip_gateway : (),
+ $block->NetAddr->broadcast->addr,
+ map { $_->_used_addresses($block, $self ) } @subclasses
+ );
}
sub _used_addresses {
--- /dev/null
+package FS::Misc::FixIPFormat;
+use strict;
+use warnings;
+use FS::Record qw(dbh qsearchs);
+use FS::upgrade_journal;
+
+=head1 NAME
+
+FS::Misc::FixIPFormat - Functions to repair bad IP address input
+
+=head1 DESCRIPTION
+
+Provides functions for freeside_upgrade to check IP address storage for
+user-entered leading 0's in IP addresses. When read from database, NetAddr::IP
+would treat the number as octal isntead of decimal. If a user entered
+10.0.0.052, this may get invisibly translated to 10.0.0.42 when exported.
+Base8:52 = Base0:42
+
+Tied to freeside_upgrade with journal name TABLE__fixipformat
+
+see: RT# 80555
+
+=head1 SYNOPSIS
+
+Usage:
+
+ # require, not use - this module is only run once
+ require FS::Misc::FixIPFormat;
+
+ my $error = FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'svc_broadband', 'svcnum', 'ip_addr'
+ );
+ die "oh no!" if $error;
+
+=head2 fix_bad_addresses_in_table TABLE, ID_COLUMN, IP_COLUMN
+
+$error = fix_bad_addresses_in_table( 'svc_broadband', 'svcnum', 'ip_addr' );
+
+=cut
+
+sub fix_bad_addresses_in_table {
+ my ( $table ) = @_;
+ return if FS::upgrade_journal->is_done("${table}__fixipformat");
+ for my $id ( find_bad_addresses_in_table( @_ )) {
+ if ( my $error = fix_ip_for_record( $id, @_ )) {
+ die "fix_bad_addresses_in_table(): $error";
+ }
+ }
+ FS::upgrade_journal->set_done("${table}__fixipformat");
+ 0;
+}
+
+=head2 find_bad_addresses_in_table TABLE, ID_COLUMN, IP_COLUMN
+
+@id = find_bad_addresses_in_table( 'svc_broadband', 'svcnum', 'ip_addr' );
+
+=cut
+
+sub find_bad_addresses_in_table {
+ my ( $table, $id_col, $ip_col ) = @_;
+ my @fix_ids;
+
+ # using DBI directly for performance
+ my $sql_statement = "
+ SELECT $id_col, $ip_col
+ FROM $table
+ WHERE $ip_col IS NOT NULL
+ ";
+ my $sth = dbh->prepare( $sql_statement ) || die "SQL ERROR ".dbh->errstr;
+ $sth->execute || die "SQL ERROR ".dbh->errstr;
+ while ( my $row = $sth->fetchrow_hashref ) {
+ push @fix_ids, $row->{ $id_col }
+ if $row->{ $ip_col } =~ /[\.^]0\d/;
+ }
+ @fix_ids;
+}
+
+=head2 fix_ip_for_record ID, TABLE, ID_COLUMN, IP_COLUMN
+
+Attempt to strip the leading 0 from a stored IP address record. If
+the corrected IP address would be a duplicate of another record in the
+same table, thow an exception.
+
+$error = fix_ip_for_record( 1001, 'svc_broadband', 'svcnum', 'ip_addr', );
+
+=cut
+
+sub fix_ip_for_record {
+ my ( $id, $table, $id_col, $ip_col ) = @_;
+
+ my $row = qsearchs($table, {$id_col => $id})
+ || die "Error finding $table record for id $id";
+
+ my $ip = $row->getfield( $ip_col );
+ my $fixed_ip = join( '.',
+ map{ int($_) }
+ split( /\./, $ip )
+ );
+
+ return undef unless $ip ne $fixed_ip;
+
+ if ( my $dupe_row = qsearchs( $table, {$ip_col => $fixed_ip} )) {
+ if ( $dupe_row->getfield( $id_col ) != $row->getfield( $id_col )) {
+ # Another record in the table has this IP address
+ # Eg one ip is provisioned as 10.0.0.51 and another is
+ # provisioned as 10.0.0.051. Cannot auto-correct by simply
+ # trimming leading 0. Die, let support decide how to fix.
+
+ die "Invalid IP address could not be auto-corrected - ".
+ "($table - $id_col = $id, $ip_col = $ip) ".
+ "colission with another reocrd - ".
+ "($table - $id_col = ".$dupe_row->getfield( $id_col )." ".
+ "$ip_col = ",$dupe_row->getfield( $ip_col )." ) - ".
+ "The entry must be corrected to continue";
+ }
+ }
+
+ warn "Autocorrecting IP address problem for ".
+ "($table - $id_col = $id, $ip_col = $ip) $fixed_ip\n";
+ $row->setfield( $ip_col, $fixed_ip );
+ $row->replace;
+}
+
+1;
sub ut_ip {
my( $self, $field ) = @_;
$self->setfield($field, '127.0.0.1') if $self->getfield($field) eq '::1';
- $self->getfield($field) =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
- or return "Illegal (IP address) $field: ". $self->getfield($field);
- for ( $1, $2, $3, $4 ) { return "Illegal (IP address) $field" if $_ > 255; }
- $self->setfield($field, "$1.$2.$3.$4");
- '';
+ return "Illegal (IP address) $field: ".$self->getfield($field)
+ unless $self->getfield($field) =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
+ $self->ut_ip46($field);
}
=item ut_ipn COLUMN
sub ut_ip46 {
my( $self, $field ) = @_;
- my $ip = NetAddr::IP->new($self->getfield($field))
+ my $ip_addr = $self->getfield( $field );
+
+ # strip user-entered leading 0's from IPv4 addresses
+ # Parsers like NetAddr::IP interpret them as octal instead of decimal
+ $ip_addr = join( '.', (
+ map{ int($_) }
+ split( /\./, $ip_addr )
+ )
+ ) if $ip_addr =~ /\./ && $ip_addr =~ /[\.^]0/;
+
+ my $ip = NetAddr::IP->new( $ip_addr )
or return "Illegal (IP address) $field: ".$self->getfield($field);
$self->setfield($field, lc($ip->addr));
return '';
#$tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
#$tax_section->{'sort_weight'} = $tax_weight;
+ my $invoice_sections_with_taxes = $conf->config_bool(
+ 'invoice_sections_with_taxes', $cust_main->agentnum
+ );
+
foreach my $tax ( @items_tax ) {
- $taxtotal += $tax->{'amount'};
my $description = &$escape_function( $tax->{'description'} );
my $amount = sprintf( '%.2f', $tax->{'amount'} );
if ( $multisection ) {
+ if ( !$invoice_sections_with_taxes ) {
- push @detail_items, {
- ext_description => [],
- ref => '',
- quantity => '',
- description => $description,
- amount => $money_char. $amount,
- product_code => '',
- section => $tax_section,
- };
+ $taxtotal += $tax->{'amount'};
+
+ push @detail_items, {
+ ext_description => [],
+ ref => '',
+ quantity => '',
+ description => $description,
+ amount => $money_char. $amount,
+ product_code => '',
+ section => $tax_section,
+ };
+ }
} else {
+ $taxtotal += $tax->{'amount'};
+
push @total_items, {
'total_item' => $description,
'total_amount' => $other_money_char. $amount,
$other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
if ( $multisection ) {
+
+ if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) {
+ # If all tax items are displayed in location/category sections,
+ # remove the empty tax section
+ @sections = grep{ $_ ne $tax_section } @sections
+ unless grep{ $_->{section} eq $tax_section } @detail_items;
+ }
+
if ( $taxtotal > 0 ) {
# there are taxes, so prepare the section to be displayed.
# $taxtotal already includes any line items that were already in the
$tax_section->{'description'} = $self->mt($tax_description);
$tax_section->{'summarized'} = '';
- if ( $conf->config_bool('invoice_sections_with_taxes', $cust_main->agentnum) ) {
-
- # remove tax section if taxes are itemized within other sections
- @sections = grep{ $_ ne $tax_section } @sections;
+ # append tax section unless it's already there
+ push @sections, $tax_section
+ unless grep {$_ eq $tax_section} @sections;
- } elsif ( !grep $tax_section, @sections ) {
-
- # append it if it's not already there
- push @sections, $tax_section;
- push @summary_subtotals, $tax_section;
-
- }
+ push @summary_subtotals, $tax_section
+ unless grep {$_ eq $tax_section} @summary_subtotals;
}
} else {
warn "$me generating plain text invoice"
if $DEBUG;
- # 'print_text' argument is no longer used
- @text = map Encode::encode_utf8($_), $self->print_text(\%args);
+ @text = $self->print_text(\%args);
} else {
'Encoding' => 'quoted-printable',
'Charset' => 'UTF-8',
#'Encoding' => '7bit',
- 'Data' => \@text,
+ 'Data' => [
+ map
+ { Encode::encode('UTF-8', $_, Encode::FB_WARN | Encode::LEAVE_SRC ) }
+ @text
+ ],
'Disposition' => 'inline',
);
' </title>',
' </head>',
' <body bgcolor="#e8e8e8">',
- Encode::encode_utf8($html),
+ Encode::encode(
+ 'UTF-8',
+ $html,
+ Encode::FB_WARN | Encode::LEAVE_SRC
+ ),
' </body>',
'</html>',
],
my @cust_bill_pkg = grep { $_->feepart } $self->cust_bill_pkg;
my $escape_function = $options{escape_function};
- my $locale = $self->cust_main->locale;
+ my $locale = $self->cust_main
+ ? $self->cust_main->locale
+ : $self->prospect_main->locale;
my @items;
foreach my $cust_bill_pkg (@cust_bill_pkg) {
warn "fee definition not found for line item #".$cust_bill_pkg->billpkgnum."\n";
next;
}
- if ( exists($options{section}) and exists($options{section}{category}) )
- {
- my $categoryname = $options{section}{category};
- # then filter for items that have that section
- if ( $part_fee->categoryname ne $categoryname ) {
- warn "skipping fee '".$part_fee->itemdesc."'--not in section $categoryname\n" if $DEBUG;
- next;
- }
- } # otherwise include them all in the main section
- # XXX what to do when sectioning by location?
+
+ # If _items_fee is called while building a sectioned invoice,
+ # - invoice_sections_method: category
+ # Skip fee records that do not match the section category.
+ # - invoice_sections_method: location
+ # Skip fee records always for location sections.
+ # The fee records will be presented in the tax/fee section instead.
+ if (
+ exists( $options{section} )
+ and
+ (
+ (
+ exists( $options{section}{category} )
+ and
+ $part_fee->categoryname ne $options{section}{category}
+ )
+ or
+ exists( $options{section}{location})
+ )
+ ) {
+ warn "skipping fee '".$part_fee->itemdesc.
+ "'--not in section $options{section}{category}\n" if $DEBUG;
+ next;
+ }
my @ext_desc;
my %base_invnums; # invnum => invoice date
push @items,
{ feepart => $cust_bill_pkg->feepart,
+ billpkgnum => $cust_bill_pkg->billpkgnum,
amount => sprintf('%.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
description => $desc,
pkg_tax => \@pkg_tax,
#'compliance solutions' -> 'compliance_solutions'
'tax_rate' => [],
'tax_rate_location' => [],
+
+ #upgrade part_event_condition_option agentnum to a multiple hash value
+ 'part_event_condition_option' =>[],
+
+ #fix ip format
+ 'svc_circuit' => [],
+
+ #fix ip format
+ 'svc_hardware' => [],
+
+ #fix ip format
+ 'svc_pbx' => [],
+
+ #fix ip format
+ 'tower_sector' => [],
+
+
;
\%hash;
=cut
1;
-
$self->NetAddr->cidr;
}
+=item free_addrs
+
+Returns an aref sorted list of free addresses in the block.
+
+=cut
+
+sub free_addrs {
+ my $self = shift;
+
+ my %used_addr_map =
+ map {$_ => 1}
+ FS::IP_Mixin->used_addresses($self),
+ FS::Conf->new()->config('exclude_ip_addr');
+
+ [
+ grep { !exists $used_addr_map{$_} }
+ map { $_->addr }
+ $self->NetAddr->hostenum
+ ];
+}
+
=item next_free_addr
Returns a NetAddr::IP object corresponding to the first unassigned address
=cut
1;
-
}
}
- my $cardtype_search = "AND cardtype != 'ACH'";
- $cardtype_search = "AND cardtype = 'ACH'" if $options{method} eq 'ECHECK';
+ my $cardtype_search = "AND ( cardtype IS NULL OR cardtype <> 'ACH')";
+ $cardtype_search = "AND ( cardtype IS NULL OR cardtype = 'ACH' )" if $options{method} eq 'ECHECK';
my $override =
qsearchs({
# Duplicates can be identified by billpkgtaxlocationnum column.
my $self = shift;
- return unless $self->pkgnum;
+
+ my $search_selector;
+ if ( $self->pkgnum ) {
+
+ # For taxes applied to normal billing items
+ $search_selector =
+ ' cust_bill_pkg_tax_location.pkgnum = '
+ . dbh->quote( $self->pkgnum );
+
+ } elsif ( $self->feepart ) {
+
+ # For taxes applied to fees, when the fee is not attached to a package
+ # i.e. late fees, billing events fees
+ $search_selector =
+ ' cust_bill_pkg_tax_location.taxable_billpkgnum = '
+ . dbh->quote( $self->billpkgnum );
+
+ } else {
+ warn "_pkg_tax_list() unhandled case breaking taxes into sections";
+ warn "_pkg_tax_list() $_: ".$self->$_
+ for qw(pkgnum billpkgnum feepart);
+ return;
+ }
map +{
billpkgtaxlocationnum => $_->billpkgtaxlocationnum,
' WHERE '.
' cust_bill_pkg.invnum = ' . dbh->quote( $self->invnum ) .
' AND '.
- ' cust_bill_pkg_tax_location.pkgnum = ' . dbh->quote( $self->pkgnum ),
+ $search_selector
});
}
$name;
}
+=item batch_payment_payname
+
+Returns a name string for this customer, either "cust_batch_payment->payname" or "First Last" or "Company,
+based on if a company name exists and is the account being used a business account.
+
+=cut
+
+sub batch_payment_payname {
+ my $self = shift;
+ my $cust_pay_batch = shift;
+ my $name;
+
+ if ($cust_pay_batch->{Hash}->{payby} eq "CARD") { $name = $cust_pay_batch->payname; }
+ else { $name = $self->first .' '. $self->last; }
+
+ $name = $self->company
+ if (($cust_pay_batch->{Hash}->{paytype} eq "Business checking" || $cust_pay_batch->{Hash}->{paytype} eq "Business savings") && $self->company);
+
+ $name;
+}
+
=item service_contact
Returns the L<FS::contact> object for this customer that has the 'Service'
}
}
- if ($cust_pkg->waive_setup && $part_pkg->plan eq "prorate") {
- $lineitems++;
- $setup = 0 if $part_pkg->prorate_setup($cust_pkg, $time);
- }
+ $lineitems++
+ if $cust_pkg->waive_setup && $part_pkg->can('prorate_setup') && $part_pkg->prorate_setup($cust_pkg, $time);
if ( $cust_pkg->get('setup') ) {
# don't change it
} );
foreach (qw( address1 address2 city state zip country latitude longitude
- payby payinfo paydate payname paycode ))
+ payby payinfo paydate payname paycode paytype ))
{
$options{$_} = '' unless exists($options{$_});
}
'payname' => $options{payname} || $cust_payby->payname,
'paytype' => $options{paytype} || $cust_payby->paytype,
'amount' => $amount, # consolidating
- 'paycode' => $options{paycode} || $cust_payby->paycode,
+ 'paycode' => $options{paycode} || '',
} );
$cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
my $payment_gateway =
$self->agent->payment_gateway( 'method' => $options{method} );
- my( $processor, $login, $password, $namespace ) =
+ ( $processor, $login, $password, $namespace ) =
map { my $method = "gateway_$_"; $payment_gateway->$method }
qw( module username password namespace );
$keep_dates = 0;
$hash{'last_bill'} = '';
$hash{'bill'} = '';
+
+ # Optionally, carry over the next bill date from the changed cust_pkg
+ # so an invoice isn't generated until the customer's usual billing date
+ if ( $self->part_pkg->option('prorate_defer_change_bill', 1) ) {
+ $hash{bill} = $self->bill;
+ }
}
if ( $keep_dates ) {
sub option_fields {
(
- 'agentnum' => { label=>'Agent', type=>'select-agent', },
+ 'agentnum' => { label=>'Agent', type=>'select-agent', multiple => '1' },
);
}
my $cust_main = $self->cust_main($object);
- my $agentnum = $self->option('agentnum');
-
- $cust_main->agentnum == $agentnum;
+ my $hashref = $self->option('agentnum') || {};
+ grep $hashref->{ $_->agentnum }, $cust_main->agent;
}
sub condition_sql {
my( $class, $table, %opt ) = @_;
- "cust_main.agentnum = " . $class->condition_sql_option_integer('agentnum', $opt{'driver_name'});
+ "cust_main.agentnum IN " . $class->condition_sql_option_option_integer('agentnum', $opt{'driver_name'});
}
1;
--- /dev/null
+package FS::part_event::Condition::cust_birthdate;
+use base qw( FS::part_event::Condition );
+use strict;
+use warnings;
+use DateTime;
+
+=head2 NAME
+
+FS::part_event::Condition::cust_birthdate
+
+=head1 DESCRIPTION
+
+Billing event triggered by the time until the customer's next
+birthday (cust_main.birthdate)
+
+=cut
+
+sub description {
+ 'Customer birthdate occurs within the given timeframe';
+}
+
+sub option_fields {
+ (
+ timeframe => {
+ label => 'Timeframe',
+ type => 'freq',
+ value => '1m',
+ }
+ );
+}
+
+sub condition {
+ my( $self, $object, %opt ) = @_;
+ my $cust_main = $self->cust_main($object);
+
+ my $birthdate = $cust_main->birthdate || return 0;
+
+ my %timeframe;
+ if ( $self->option('timeframe') =~ /(\d+)([mwdh])/ ) {
+ my $k = {qw|m months w weeks d days h hours|}->{$2};
+ $timeframe{ $k } = $1;
+ } else {
+ die "Unparsable timeframe given: ".$self->option('timeframe');
+ }
+
+ my $ck_dt = DateTime->from_epoch( epoch => $opt{time} );
+ my $bd_dt = DateTime->from_epoch( epoch => $birthdate );
+
+ # Find the birthday for this calendar year. If customer birthday
+ # has already passed this year, find the birthday for next year.
+ my $next_bd_dt = DateTime->new(
+ month => $bd_dt->month,
+ day => $bd_dt->day,
+ year => $ck_dt->year,
+ );
+ $next_bd_dt->add( years => 1 )
+ if DateTime->compare( $next_bd_dt, $ck_dt ) == -1;
+
+ # Does next birthday occur between now and specified duration?
+ $ck_dt->add( %timeframe );
+ DateTime->compare( $next_bd_dt, $ck_dt ) != 1 ? 1 : 0;
+}
+
+1;
}
}
+use FS::upgrade_journal;
+sub _upgrade_data { #class method
+ my ($class, %opts) = @_;
+
+ # migrate part_event_condition_option agentnum to part_event_condition_option_option agentnum
+ unless ( FS::upgrade_journal->is_done('agentnum_to_hash') ) {
+
+ foreach my $condition_option (qsearch('part_event_condition_option', { optionname => 'agentnum', })) {
+ my %options;
+ my $optionvalue = $condition_option->get("optionvalue");
+ if ($optionvalue eq 'HASH' ) { next; }
+ elsif ($optionvalue eq '') {
+ foreach my $agent (qsearch('agent', {})) {
+ $options{$agent->agentnum} = '1';
+ }
+
+ }
+ else {
+ $options{$optionvalue} = '1';
+ }
+
+ $condition_option->optionvalue(ref(\%options));
+ my $error = $condition_option->replace(\%options);
+ die $error if $error;
+
+ }
+
+ FS::upgrade_journal->set_done('agentnum_to_hash');
+
+ }
+
+}
+
=back
=head1 SEE ALSO
'the customer\'s next bill date',
'type' => 'checkbox',
},
+ 'prorate_defer_change_bill' => {
+ 'name' => 'When synchronizing, defer bill for '.
+ 'package changes until the customer\'s '.
+ 'next bill date',
+ 'type' => 'checkbox',
+ },
'prorate_round_day' => {
'name' => 'When synchronizing, round the prorated '.
'period',
},
'fieldorder' => [ qw( recur_temporality
start_1st
- sync_bill_date prorate_defer_bill prorate_round_day
+ sync_bill_date prorate_defer_bill
+ prorate_defer_change_bill prorate_round_day
suspend_bill unsuspend_adjust_bill
bill_recur_on_cancel
bill_suspend_as_cancel
}
## set custname to business name if business checking or savings account is used otherwise leave as first and last name.
- my $custname = $cust_pay_batch->cust_main->first . ' ' . $cust_pay_batch->cust_main->last;
- $custname = $cust_pay_batch->cust_main->company
- if (($cust_pay_batch->{Hash}->{paytype} eq "Business checking" || $cust_pay_batch->{Hash}->{paytype} eq "Business savings") && $cust_pay_batch->cust_main->company);
+ my $custname = $cust_pay_batch->cust_main->batch_payment_payname($cust_pay_batch);
$i++;
},
);
+## this format can handle credit transactions
+sub can_handle_credits {
+ 1;
+}
+
1;
use strict;
use NEXT;
-use FS::Record qw(qsearchs qsearch);
+use Carp qw(croak carp);
+use FS::Record qw(qsearchs qsearch dbh);
use FS::Conf;
use FS::router;
use FS::part_svc_router;
}
sub _used_addresses {
- my ($class, $block, $exclude) = @_;
- my $ip_field = $class->table_info->{'ip_field'}
- or return ();
- # if the service doesn't have an ip_field, then it has no IP addresses
- # in use, yes?
-
- my %hash = ( $ip_field => { op => '!=', value => '' } );
- #$hash{'blocknum'} = $block->blocknum if $block;
- $hash{'svcnum'} = { op => '!=', value => $exclude->svcnum } if ref $exclude;
- map { my $na = $_->NetAddr; $na ? $na->addr : () }
- qsearch({
- table => $class->table,
- hashref => \%hash,
- extra_sql => " AND $ip_field != '0e0'",
- });
+ my ($class, $block, $exclude_svc) = @_;
+
+ croak "_used_addresses() requires an FS::addr_block parameter"
+ unless ref $block && $block->isa('FS::addr_block');
+
+ my $ip_field = $class->table_info->{'ip_field'};
+ if ( !$ip_field ) {
+ carp "_used_addresses() skipped, no ip_field";
+ return;
+ }
+
+ my %qsearch = ( $ip_field => { op => '!=', value => '' });
+ $qsearch{svcnum} = { op => '!=', value => $exclude_svc->svcnum }
+ if ref $exclude_svc && $exclude_svc->svcnum;
+
+ my $block_na = $block->NetAddr;
+
+ my $octets;
+ if ($block->ip_netmask >= 24) {
+ $octets = 3;
+ } elsif ($block->ip_netmask >= 16) {
+ $octets = 2;
+ } elsif ($block->ip_netmask >= 8) {
+ $octets = 1;
+ }
+
+ # e.g.
+ # SELECT ip_addr
+ # FROM svc_broadband
+ # WHERE ip_addr != ''
+ # AND ip_addr != '0e0'
+ # AND ip_addr LIKE '10.0.2.%';
+ #
+ # For /24, /16 and /8 this approach is fast, even when svc_broadband table
+ # contains 650,000+ ip records. For other allocations, this approach is
+ # not speedy, but usable.
+ #
+ # Note: A use case like this would could greatly benefit from a qsearch()
+ # parameter to bypass FS::Record objects creation and just
+ # return hashrefs from DBI. 200,000 hashrefs are many seconds faster
+ # than 200,000 FS::Record objects
+ my %qsearch_param = (
+ table => $class->table,
+ select => $ip_field,
+ hashref => \%qsearch,
+ extra_sql => " AND $ip_field != '0e0' ",
+ );
+ if ( $octets ) {
+ my $block_str = join('.', (split(/\D/, $block_na->first))[0..$octets-1]);
+ $qsearch_param{extra_sql}
+ .= " AND $ip_field LIKE ".dbh->quote("${block_str}.%");
+ }
+
+ if ( $block->ip_netmask % 8 ) {
+ # Some addresses returned by qsearch may be outside the network block,
+ # so each ip address is tested to be in the block before it's returned.
+ return
+ grep { $block_na->contains( NetAddr::IP->new( $_ ) ) }
+ map { $_->$ip_field }
+ qsearch( \%qsearch );
+ }
+
+ return
+ map { $_->$ip_field }
+ qsearch( \%qsearch_param );
}
sub _is_used {
#select_table => 'radius_group',
#select_key => 'groupnum',
#select_label => 'groupname',
+ disable_select => 1,
disable_inventory => 1,
multiple => 1,
},
#next SVC;
}
+ require FS::Misc::FixIPFormat;
+ FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'svc_broadband', 'svcnum', 'ip_addr',
+ );
+
'';
}
=cut
1;
-
'LEFT JOIN circuit_type USING ( typenum )';
}
+sub _upgrade_data {
+
+ require FS::Misc::FixIPFormat;
+ FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'svc_circuit', 'svcnum', 'endpoint_ip_addr',
+ );
+
+ '';
+
+}
+
=back
=head1 SEE ALSO
=cut
1;
-
join(':', $self->hw_addr =~ /../g) : $self->hw_addr)
}
+sub _upgrade_data {
+
+ require FS::Misc::FixIPFormat;
+ FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'svc_hardware', 'svcnum', 'ip_addr',
+ );
+
+ '';
+
+}
+
=back
=head1 SEE ALSO
=cut
1;
-
qsearchs ( $psearch->{query} );
}
+sub _upgrade_data {
+
+ require FS::Misc::FixIPFormat;
+ FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'svc_pbx', 'svcnum', 'ip_addr',
+ );
+
+ '';
+
+}
+
=back
=head1 BUGS
=cut
1;
-
$self->ut_numbern('sectornum')
|| $self->ut_number('towernum', 'tower', 'towernum')
|| $self->ut_text('sectorname')
- || $self->ut_textn('ip_addr')
+ || $self->ut_ip46n('ip_addr')
|| $self->ut_floatn('height')
|| $self->ut_numbern('freq_mhz')
|| $self->ut_numbern('direction')
die $error if $error;
}
+sub _upgrade_data {
+
+ require FS::Misc::FixIPFormat;
+ FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
+ 'tower_sector', 'sectornum', 'ip_addr',
+ );
+
+ '';
+
+}
+
=head1 BUGS
=head1 SEE ALSO
=cut
1;
-
<% include( 'elements/browse.html',
'title' => 'Discounts',
'name' => 'discounts',
- 'menubar' => [ 'Add a new discount' =>
- $p.'edit/discount.html',
- ],
- 'query' => { 'table' => 'discount', },
+ 'menubar' => \@menubar,
+ 'query' => \%query,
+ 'order_by_sql' => { description => 'discountnum' },
'count_query' => 'SELECT COUNT(*) FROM discount',
'disableable' => 1,
'disabled_statuspos' => 1,
- 'header' => [ 'Name', 'Comment', 'Class', 'Discount', ],
+ 'header' => [ 'Name', 'Class', 'Discount', ],
'fields' => [ 'name',
- 'comment',
'classname',
'description',
],
- 'links' => [ $link,
- $link,
- ],
+ 'links' => \@links
)
%>
<%init>
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
-my $link = [ "${p}edit/discount.html?", 'discountnum' ];
+my @links = (
+ [ "${p}edit/discount.html?", 'discountnum' ],
+ [ "${p}edit/discount_class.html?", 'classnum' ],
+);
+
+my %query = (
+ select => 'discount.*, discount_class.*',
+ table => 'discount',
+ addl_from => 'LEFT JOIN discount_class USING(classnum)',
+);
+
+my @menubar = (
+ 'Add a new discount' => $p.'edit/discount.html',
+ 'Discount classes' => $p.'browse/discount_class.html',
+);
</%init>
<OPTION VALUE="<% $payment_gateway->gatewaynum %>"><% $payment_gateway->gateway_module %> (<% $payment_gateway->gateway_username %>)
% }
-
</SELECT>
-<BR><BR>
+<BR>
+
+<INPUT TYPE="checkbox" NAME="cardtype" VALUE="ACH"> for ACH only.
+<BR>
+<BR>
<INPUT TYPE="submit" VALUE="Add gateway override">
</FORM>
<%def .namepart>
-% my ($field, $value, $label, $extra) = @_;
+% my ($field, $value, $label, $extra, $unmask_field) = @_;
<DIV STYLE="display: inline-block" ID="<% $field %>_input">
<INPUT TYPE="text" NAME="<% $field %>" VALUE="<% $value |h %>" <%$extra%>>
+% if (
+% $value
+% && ref $unmask_field
+% && !$unmask_field->{unmask_ss}
+% && $FS::CurrentUser::CurrentUser->access_right( $unmask_field->{access_right} )
+% ) {
+ <& /elements/link-replace_element_text.html, {
+ target_id => $unmask_field->{target_id},
+ replace_text => $unmask_field->{replace_text},
+ } &>
+% }
<BR><FONT SIZE="-1" COLOR="#333333"><% emt($label) %></FONT>
</DIV>
</%def>
<& .namepart, 'first', $cust_main->first, 'First' &>
% if ( $conf->exists('show_ss') ) {
- <& .namepart, 'ss', $ss, 'SS#', "SIZE=11" &>
+ <& .namepart, 'ss', $ss, 'SS#', "SIZE=11 ID='ss'", {
+ target_id => 'ss',
+ replace_text => $cust_main->ss,
+ access_right => 'Unmask customer SSN',
+ unmask_ss => $conf->exists('unmask_ss'),
+ } &>
% } else {
<INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>">
% }
% if ( $conf->exists('show_stateid') ) {
<TR>
<TH ALIGN="right"><% $stateid_label %></TH>
- <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>" SIZE=12></TD>
+ <TD>
+ <INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>" SIZE=12 ID="stateid">
+% if ( $stateid && $FS::CurrentUser::CurrentUser->access_right( 'Unmask customer DL' )) {
+ <& /elements/link-replace_element_text.html, {target_id => 'stateid', replace_text => $cust_main->stateid} &>
+% }
+ </TD>
<TD><& /elements/select-state.html,
state => $cust_main->stateid_state,
country => $cust_main->country, # how does this work on new customer?
% }
<BR>Payment
- <% ntable("#cccccc", 2) %>
+ <TABLE class="fsinnerbox">
<TR>
<TD ALIGN="right">Amount</TD><TD BGCOLOR="#ffffff">$<% $cust_pay->paid %></TD>
<BR>Refund
-<% ntable("#cccccc", 2) %>
+
+<TABLE class="fsinnerbox">
<TR>
<TD ALIGN="right">Date</TD>
<TD ALIGN="right">Check #</TD>
<TD COLSPAN=2><INPUT TYPE="text" NAME="payinfo" VALUE="<% $payinfo %>" SIZE=10></TD>
</TR>
+ </TABLE>
% }
-% elsif ($payby eq 'CHEK') {
+% elsif ($payby eq 'CHEK' || $payby eq 'CARD') {
%
+<SCRIPT TYPE="text/javascript">
+ function cust_payby_changed (what) {
+ var custpaybynum = what.options[what.selectedIndex].value
+ if ( custpaybynum == '' || custpaybynum == '0' ) {
+ //what.form.payinfo.disabled = false;
+ $('#cust_payby').slideDown();
+ } else {
+ //what.form.payinfo.value = '';
+ //what.form.payinfo.disabled = true;
+ $('#cust_payby').slideUp();
+ }
+ }
+</SCRIPT>
% my @cust_payby = ();
% if ( $payby eq 'CARD' ) {
% @cust_payby = $cust_main->cust_payby('CARD','DCRD');
% my $custpaybynum = length(scalar($cgi->param('custpaybynum')))
% ? scalar($cgi->param('custpaybynum'))
% : scalar(@cust_payby) && $cust_payby[0]->custpaybynum;
-<& /elements/tr-select-cust_payby.html,
+
+% if ($cust_pay) {
+ <INPUT TYPE="hidden" NAME="payinfo" VALUE="<% $payinfo %>" SIZE=10>
+% }
+% else {
+ <& /elements/tr-select-cust_payby.html,
'cust_payby' => \@cust_payby,
'curr_value' => $custpaybynum,
'onchange' => 'cust_payby_changed(this)',
-&>
- <INPUT TYPE="hidden" NAME="batch" VALUE="1">
+ &>
+% }
+
+% if ( $conf->exists("batch-enable")
+% || grep $payby eq $_, $conf->config('batch-enable_payby')
+% ) {
+% if ( grep $payby eq $_, $conf->config('realtime-disable_payby') ) {
+ <INPUT TYPE="hidden" NAME="batch" VALUE="1">
+% } else {
+ <TR>
+ <TD ALIGN="right"><INPUT TYPE="checkbox" NAME="batch" VALUE="1" ID="batch" <% ($batchnum || $batch) ? 'checked' : '' %> ></TD>
+ <TH ALIGN="left"> <% mt('Add to current batch') |h %></TH>
+ </TR>
+% }
+% }
+
+ </TABLE>
+<P>
+
+% if ( !$cust_pay ) {
+<DIV ID="cust_payby"
+ <% $custpaybynum ? 'STYLE="display:none"'
+ : ''
+ %>
+>
+<TABLE class="fsinnerbox">
+
+ <& /elements/cust_payby_new.html,
+ 'cust_payby' => \@cust_payby,
+ 'curr_value' => $custpaybynum,
+ &>
+
+</TABLE>
+</DIV>
+% } # end if cust_pay
+
% } else {
<INPUT TYPE="hidden" NAME="payinfo" VALUE="">
+ </TABLE>
% }
+<P>
+<TABLE class="fsinnerbox">
<& /elements/tr-select-reason.html,
'field' => 'reasonnum',
'reason_class' => 'F',
my $payinfo = $cgi->param('payinfo');
my $reason = $cgi->param('reason');
my $link = $cgi->param('popup') ? 'popup' : '';
+my $batch = $cgi->param('batch');
die "access denied"
unless $FS::CurrentUser::CurrentUser->refund_access_right($payby);
-my( $paynum, $cust_pay ) = ( '', '' );
+my( $paynum, $cust_pay, $batchnum ) = ( '', '', '' );
if ( $cgi->param('paynum') =~ /^(\d+)$/ ) {
$paynum = $1;
$cust_pay = qsearchs('cust_pay', { paynum=>$paynum } )
or die "unknown payment # $paynum";
$refund ||= $cust_pay->unrefunded;
+ $batchnum = $cust_pay->batchnum;
if ( $custnum ) {
die "payment # $paynum is not for specified customer # $custnum"
unless $custnum == $cust_pay->custnum;
'CHEK' => 'electronic check (ACH)',
);
-my( $cust_payby, $payinfo, $paycvv, $month, $year, $payname );
+my( $cust_pay, $cust_payby, $payinfo, $paycvv, $month, $year, $payname );
my $paymask = '';
if ( (my $custpaybynum = scalar($cgi->param('custpaybynum'))) > 0 ) {
$paycvv = $cust_payby->paycvv; # pass it if we got it, running a transaction will clear it
( $month, $year ) = $cust_payby->paydate_mon_year;
$payname = $cust_payby->payname;
+ $cgi->param(-name=>"paytype", -value=>$cust_payby->paytype) unless $cgi->param("paytype");
+
+} elsif ( $cgi->param('paynum') > 0) {
+
+ $cust_pay = qsearchs({
+ 'table' => 'cust_pay',
+ 'hashref' => { 'paynum' => $cgi->param('paynum') },
+ 'select' => 'cust_pay.*, cust_pay_batch.payname ',
+ 'addl_from' => "left join cust_pay_batch on cust_pay_batch.batchnum = cust_pay.batchnum and cust_pay_batch.custnum = $custnum ",
+ });
+ $payinfo = $cust_pay->payinfo;
+ $payname = $cust_pay->payname;
} else {
my $refund = "$1$2";
$cgi->param('paynum') =~ /^(\d*)$/ or die "Illegal paynum!";
my $paynum = $1;
- my $paydate = $cgi->param('exp_year'). '-'. $cgi->param('exp_month'). '-01';
- $options{'paydate'} = $paydate if $paydate =~ /^\d{2,4}-\d{1,2}-01$/;
+ my $paydate;
+ unless ($paynum) {
+ if ($cust_payby->paydate) { $paydate = "$year-$month-01"; }
+ else { $paydate = "2037-12-01"; }
+ }
if ( $cgi->param('batch') ) {
-
+ $paydate = "2037-12-01" unless $paydate;
$error ||= $cust_main->batch_card(
'payby' => $payby,
'amount' => $refund,
'payinfo' => $payinfo,
- 'paydate' => "$year-$month-01",
+ 'paydate' => $paydate,
'payname' => $payname,
'paycode' => 'C',
map { $_ => scalar($cgi->param($_)) }
);
errorpage($error) if $error;
-#### post refund #####
my %hash = map {
$_, scalar($cgi->param($_))
} fields('cust_refund');
- $paynum = $cgi->param('paynum');
- $paynum =~ /^(\d*)$/ or die "Illegal paynum!";
- if ($paynum) {
- my $cust_pay = qsearchs('cust_pay',{ 'paynum' => $paynum });
- die "Could not find paynum $paynum" unless $cust_pay;
- $error = $cust_pay->refund(\%hash);
- } else {
- my $new = new FS::cust_refund ( \%hash );
- $error = $new->insert;
- }
- # if not a batch refund run realtime.
+
+ my $new = new FS::cust_refund ( { 'paynum' => $paynum,
+ %hash,
+ } );
+ $error = $new->insert;
+
+ # if not a batch refund run realtime.
} else {
$error = $cust_main->realtime_refund_bop( $bop, 'amount' => $refund,
'paynum' => $paynum,
'reasonnum' => scalar($cgi->param('reasonnum')),
%options );
}
-} else {
+} else { # run cash refund.
my %hash = map {
$_, scalar($cgi->param($_))
} fields('cust_refund');
--- /dev/null
+% my $auto = 0;
+% if ( $payby eq 'CARD' ) {
+%
+% my( $payinfo, $paycvv, $month, $year ) = ( '', '', '', '' );
+% my $payname = $cust_main->first. ' '. $cust_main->getfield('last');
+% my $location = $cust_main->bill_location;
+
+ <TR>
+ <TH ALIGN="right"><% mt('Card number') |h %></TH>
+ <TD COLSPAN=7>
+ <TABLE>
+ <TR>
+ <TD>
+ <INPUT TYPE="text" NAME="payinfo" SIZE=20 MAXLENGTH=19 VALUE="<%$payinfo%>"> </TD>
+ <TH><% mt('Exp.') |h %></TH>
+ <TD>
+ <SELECT NAME="month">
+% for ( ( map "0$_", 1 .. 9 ), 10 .. 12 ) {
+
+ <OPTION<% $_ == $month ? ' SELECTED' : '' %>><% $_ %>
+% }
+
+ </SELECT>
+ </TD>
+ <TD> / </TD>
+ <TD>
+ <SELECT NAME="year">
+% my @a = localtime; for ( $a[5]+1900 .. $a[5]+1915 ) {
+
+ <OPTION<% $_ == $year ? ' SELECTED' : '' %>><% $_ %>
+% }
+
+ </SELECT>
+ </TD>
+ </TR>
+ </TABLE>
+ </TD>
+ </TR>
+ <TR>
+ <TH ALIGN="right"><% mt('CVV2') |h %></TH>
+ <TD><INPUT TYPE="text" NAME="paycvv" VALUE="<% $paycvv %>" SIZE=4 MAXLENGTH=4>
+ (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/cvv2.html', 480, 352, 'cvv2_popup' ), CAPTION, 'CVV2 Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>)
+ </TD>
+ </TR>
+ <TR>
+ <TH ALIGN="right"><% mt('Exact name on card') |h %></TH>
+ <TD><INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<%$payname%>"></TD>
+ </TR>
+
+ <& /elements/location.html,
+ 'object' => $location,
+ 'no_asterisks' => 1,
+ 'address1_label' => emt('Card billing address'),
+ &>
+
+% } elsif ( $payby eq 'CHEK' ) {
+%
+% my( $account, $aba, $branch, $payname, $ss, $paytype, $paystate,
+% $stateid, $stateid_state )
+% = ( '', '', '', '', '', '', '', '', '' );
+%
+% #false laziness w/{edit,view}/cust_main/billing.html
+% my $routing_label = $conf->config('echeck-country') eq 'US'
+% ? 'ABA/Routing number'
+% : 'Routing number';
+% my $routing_size = $conf->config('echeck-country') eq 'CA' ? 4 : 10;
+% my $routing_maxlength = $conf->config('echeck-country') eq 'CA' ? 3 : 9;
+
+ <INPUT TYPE="hidden" NAME="month" VALUE="12">
+ <INPUT TYPE="hidden" NAME="year" VALUE="2037">
+ <TR>
+ <TD ALIGN="right"><% mt('Account number') |h %></TD>
+ <TD><INPUT TYPE="text" SIZE=10 NAME="payinfo1" VALUE="<%$account%>"></TD>
+ <TD ALIGN="right"><% mt('Type') |h %></TD>
+ <TD><SELECT NAME="paytype"><% join('', map { qq!<OPTION VALUE="$_" !.($paytype eq $_ ? 'SELECTED' : '').">$_</OPTION>" } FS::cust_payby->paytypes) %></SELECT></TD>
+ </TR>
+ <TR>
+ <TD ALIGN="right"><% mt($routing_label) |h %></TD>
+ <TD>
+ <INPUT TYPE="text" SIZE="<% $routing_size %>" MAXLENGTH="<% $routing_maxlength %>" NAME="payinfo2" VALUE="<%$aba%>">
+ (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/ach.html', 380, 240, 'ach_popup' ), CAPTION, 'ACH Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>)
+ </TD>
+ </TR>
+% if ( $conf->config('echeck-country') eq 'CA' ) {
+ <TR>
+ <TD ALIGN="right"><% mt('Branch number') |h %></TD>
+ <TD>
+ <INPUT TYPE="text" NAME="payinfo3" VALUE="<%$branch%>" SIZE=6 MAXLENGTH=5>
+ </TD>
+ </TR>
+% }
+ <TR>
+ <TD ALIGN="right"><% mt('Bank name') |h %></TD>
+ <TD><INPUT TYPE="text" NAME="payname" VALUE="<%$payname%>"></TD>
+ </TR>
+
+% if ( $conf->exists('show_bankstate') ) {
+ <TR>
+ <TD ALIGN="right"><% mt('Bank state') |h %></TD>
+ <TD><& /elements/select-state.html,
+ 'disable_empty' => 0,
+ 'empty_label' => emt('(choose)'),
+ 'state' => $paystate,
+ 'country' => $cust_main->country,
+ 'prefix' => 'pay',
+ &>
+ </TD>
+ </TR>
+% } else {
+ <INPUT TYPE="hidden" NAME="paystate" VALUE="<% $paystate %>">
+% }
+
+% if ( $conf->exists('show_ss') ) {
+ <TR>
+ <TD ALIGN="right">
+ <% mt('Account holder') |h %><BR>
+ <% mt('Social security or tax ID #') |h %>
+ </TD>
+ <TD><INPUT TYPE="text" NAME="ss" VALUE="<% $ss %>"></TD>
+ </TR>
+% } else {
+ <INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>"></TD>
+% }
+
+% if ( $conf->exists('show_stateid') ) {
+ <TR>
+ <TD ALIGN="right">
+ <% mt('Account holder') |h %><BR>
+ <% mt("Driver's license or state ID #") |h %>
+ </TD>
+ <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>"></TD>
+ <TD ALIGN="right"><% mt('State') |h %></TD>
+ <TD><& /elements/select-state.html,
+ 'disable_empty' => 0,
+ 'empty_label' => emt('(choose)'),
+ 'state' => $stateid_state,
+ 'country' => $cust_main->country,
+ 'prefix' => 'stateid_',
+ &>
+ </TD>
+ </TR>
+% } else {
+ <INPUT TYPE="hidden" NAME="stateid" VALUE="<% $stateid %>">
+ <INPUT TYPE="hidden" NAME="stateid_state" VALUE="<% $stateid_state %>">
+% }
+
+% } #end CARD/CHEK-specific section
+
+
+<TR>
+ <TD COLSPAN=8>
+ <INPUT TYPE="checkbox" CHECKED NAME="save" VALUE="1">
+ <% mt('Remember this information') |h %>
+ </TD>
+</TR>
+
+<TR>
+ <TD COLSPAN=8>
+ <INPUT TYPE="checkbox"<% $auto ? ' CHECKED' : '' %> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }">
+ <% mt("Charge future payments to this [_1] automatically",$type{$payby}) |h %>
+% if ( @cust_payby ) {
+ <% mt('as') |h %>
+ <SELECT NAME="weight">
+% for ( 1 .. 1+scalar(grep { $_->payby =~ /^(CARD|CHEK)$/ } @cust_payby) ) {
+ <OPTION VALUE="<%$_%>"><% mt( $weight{$_} ) |h %>
+% }
+ </SELECT>
+% } else {
+ <INPUT TYPE="hidden" NAME="weight" VALUE="1">
+% }
+ </TD>
+</TR>
+
+<%once>
+
+my %weight = (
+ 1 => 'Primary',
+ 2 => 'Secondary',
+ 3 => 'Tertiary',
+ 4 => 'Fourth',
+ 5 => 'Fifth',
+ 6 => 'Sixth',
+ 7 => 'Seventh',
+);
+
+</%once>
+
+<%init>
+
+my %opt = @_;
+
+my @cust_payby = @{$opt{cust_payby}};
+
+my %type = ( 'CARD' => 'credit card',
+ 'CHEK' => 'electronic check (ACH)',
+ );
+
+$cgi->param('payby') =~ /^(CARD|CHEK)$/
+ or die "unknown payby ". $cgi->param('payby');
+my $payby = $1;
+
+$cgi->param('custnum') =~ /^(\d+)$/
+ or die "illegal custnum ". $cgi->param('custnum');
+my $custnum = $1;
+
+my $cust_main = qsearchs( 'cust_main', { 'custnum'=>$custnum } );
+die "unknown custnum $custnum" unless $cust_main;
+
+my $balance = $cust_main->balance;
+
+my $payinfo = '';
+
+my $conf = new FS::Conf;
+
+#false laziness w/selfservice make_payment.html shortcut for one-country
+my %states = map { $_->state => 1 }
+ qsearch('cust_main_county', {
+ 'country' => $conf->config('countrydefault') || 'US'
+ } );
+my @states = sort { $a cmp $b } keys %states;
+
+</%init>
\ No newline at end of file
--- /dev/null
+<%doc>
+
+Display a link with javascript to replace text within a element.
+
+Usage:
+
+<& /elements/link-replace_element_text.html, {
+ target_id => 'input_id',
+ replace_text => 'hello',
+
+ element_type => 'input', # Uses jquery val() method to replace text
+ element_type => 'div', # Uses jquery text() method to replace text
+
+ href => ...
+ style => ...
+ class => ...
+ }
+&>
+
+</%doc>
+<a href="<% $param{href} %>"
+ style="<% $param{style} %>"
+% if ($param{class}) {
+ class="<% $param{class} %>"
+% }
+ onClick="$('#<% $param{target_id} %>').<% $param{jmethod} %>('<% $param{replace_text} |h %>');">◁</a>
+<%init>
+
+die "template call requires a parameter hashref" unless ref $_[0];
+
+# Defaults that can be overridden in param hashref
+my %param = (
+ target_id => 'SPECIFY_AN_INPUT_ELEMENT_ID',
+ replace_text => 'REPLACEMENT_TEXT_FOR_INPUT_ELEMENT',
+ element_type => 'input',
+
+ link_text => '%#x25C1;', # ◁
+ href => 'javascript:void(0)',
+ style => 'text-decoration:none;',
+ class => undef,
+
+ %{ $_[0] },
+);
+$param{jmethod} = $param{element_type} eq 'input' ? 'val' : 'text';
+</%init>
-% if ( scalar(@{ $opt{'cust_payby'} }) == 0 ) {
+% if ( scalar(@{ $opt{'cust_payby'} }) == 0 ) {
<INPUT TYPE="hidden" NAME="<% $opt{'element_name'} || $opt{'field'} || 'custpaybynum' %>" VALUE="">
var manual_addr_routernum = <% encode_json(\%manual_addr_routernum) %>;
var ip_addr_curr_value = <% $opt{'ip_addr'} |js_string %>;
var blocknum_curr_value = <% $opt{'blocknum'} |js_string %>;
-function update_ip_addr(obj, i) {
- var routernum = document.getElementById('router_select_0').value;
- var select_blocknum = document.getElementById('router_select_1');
- var blocknum = select_blocknum.value;
- var input_ip_addr = document.getElementById('input_ip_addr');
+
+function update_ip_addr() {
+ var routernum = $('#router_select_0').val() || "";
+ var blocknum = $('#router_select_1').val() || "";
+ var e_input_ip_addr = $('#input_ip_addr');
+ var e_router_select_1 = $('#router_select_1');
+
+ <% # Is block is automatically selected for this router? %>
if ( manual_addr_routernum[routernum] == 'Y' ) {
-%# hide block selection and default ip address to its previous value
- select_blocknum.style.display = 'none';
- input_ip_addr.value = ip_addr_curr_value;
- }
- else {
-%# the reverse
- select_blocknum.style.display = '';
-%# default ip address to null, unless the router/block are set to the
-%# previous value, in which case default it to current value
+ show_ip_input();
+ hide_ip_select();
+ e_router_select_1.hide();
+ e_input_ip_addr.val( ip_addr_curr_value );
+ } else {
+ e_router_select_1.show();
+ e_input_ip_addr.attr('placeholder', <% mt('(automatic)') | js_string %> );
if ( routernum == router_curr_values[0] &&
- blocknum == router_curr_values[1] ) {
- input_ip_addr.value = ip_addr_curr_value;
+ blocknum == router_curr_values[1] ) {
+ e_input_ip_addr.val( ip_addr_curr_value );
} else {
- input_ip_addr.value = <% mt('(automatic)') |js_string %>;
+ e_input_ip_addr.val('');
}
}
+ show_or_hide_toggle_ip();
+ populate_ip_select();
+}
+
+function toggle_ip_input() {
+ if ( $('#input_ip_addr').is(':hidden') ) {
+ show_ip_input();
+ } else {
+ show_ip_select();
+ }
+}
+
+function show_ip_input() {
+ $('#input_ip_addr').show();
+ $('#select_ip_addr').hide();
+ depopulate_ip_select();
+}
+
+function show_ip_select() {
+ var e_input_ip_addr = $('#input_ip_addr');
+ var e_select_ip_addr = $('#select_ip_addr');
+
+ e_select_ip_addr.width( e_input_ip_addr.width() );
+ e_input_ip_addr.hide();
+ e_select_ip_addr.show();
+ populate_ip_select();
+}
+
+function populate_ip_select() {
+ depopulate_ip_select();
+ var e = $('#select_ip_addr');
+ var blocknum = $('#router_select_1').val();
+
+ var opts = [ '<option value="">loading...</option>' ];
+ e.html(opts.join(''));
+
+% if ( $opt{ip_addr} ) {
+ opts = [
+ '<option value="<% $opt{ip_addr} |h %>"><% $opt{ip_addr} |h %></option>',
+ '<option value="">-----------</option>'
+ ];
+% } else {
+ opts = [ '<option value=""><% mt('(automatic)') |h %></option>' ];
+% }
+ if ( blocknum && $.isNumeric(blocknum) && ! e.is(':hidden')) {
+ $.getJSON(
+ '<% $p %>misc/xmlhttp-free_addresses_in_block.json.html',
+ {blocknum: blocknum},
+ function(ip_json) {
+ $.each( ip_json, function(idx, val) {
+ opts.push(
+ '<option' + (val == ip_addr_curr_value ? 'selected' : '') + '>'
+ + val
+ + '</option>'
+ );
+ });
+ e.html(opts.join(''));
+ }
+ );
+ }
}
-function clearhint_ip_addr (what) {
- if ( what.value == <% mt('(automatic)') |js_string %> )
- what.value = '';
+
+function depopulate_ip_select() {
+ $('#select_ip_addr').children().remove();
}
+
+function propogate_ip_select() {
+ $('#input_ip_addr').val( $('#select_ip_addr').val() );
+}
+
+function show_or_hide_toggle_ip() {
+ if ( $('#router_select_1').val() ) {
+ $('#toggle_ip').show();
+ } else {
+ show_ip_input();
+ $('#toggle_ip').hide();
+ }
+}
+
</script>
+
<& /elements/tr-td-label.html, label => ($opt{'label'} || 'Router'), required => $opt{'required'} &>
<td>
<& /elements/select-tiered.html, prefix => 'router_', tiers => [
</td></tr>
<& /elements/tr-td-label.html, label => ($opt{'ip_addr_label'} || 'IP address'), required => $opt{'ip_addr_required'} &>
<td>
-% #warn Dumper \%fixed;
% if ( exists $fixed{$ip_field} ) {
<input type="hidden" id="input_ip_addr" name="<% $ip_field %>"
value="<% $opt{'ip_addr'} |h%>"><% $opt{'ip_addr'} || '' %>
% }
% else {
- <input type="text" id="input_ip_addr" name="<% $ip_field %>"
- value="<% $opt{'ip_addr'} |h%>" onfocus="clearhint_ip_addr(this)">
+ <input type="text"
+ id="input_ip_addr"
+ name="<% $ip_field %>"
+ value="<% $opt{'ip_addr'} | h %>"
+ onfocus="clearhint_ip_addr(this)">
+ <select id="select_ip_addr" style="display: none;" onChange='javascript:propogate_ip_select();'>
+ <option><% mt('loading') |h %>...</option>
+ </select>
+ <button type="button" onClick='javascript:toggle_ip_input();' id="toggle_ip" style="display: none;">▼</button>
% }
</td> </tr>
<script type="text/javascript">
$opt{'format'} = $1;
}
-my $pay_batch = qsearchs('pay_batch', { batchnum => $batchnum } );
+my $credit_transactions = "EXISTS (SELECT 1 FROM cust_pay_batch WHERE batchnum = $batchnum AND paycode = 'C') AS arecredits";
+my $pay_batch = qsearchs({ 'select' => "*, $credit_transactions",
+ 'table' => 'pay_batch',
+ 'hashref' => { batchnum => $batchnum },
+ });
die "Batch not found: '$batchnum'" if !$pay_batch;
+if ($pay_batch->{Hash}->{arecredits}) {
+ my $export_format = "FS::pay_batch::".$opt{'format'};
+ die "This format can not handle refunds." unless $export_format->can('can_handle_credits');
+}
+
my $exporttext = $pay_batch->export_batch(%opt);
unless ($exporttext) {
http_header('Content-Type' => 'text/html' );
>
<TABLE class="fsinnerbox">
-% my $auto = 0;
-% if ( $payby eq 'CARD' ) {
-%
-% my( $payinfo, $paycvv, $month, $year ) = ( '', '', '', '' );
-% my $payname = $cust_main->first. ' '. $cust_main->getfield('last');
-% my $location = $cust_main->bill_location;
-
- <TR>
- <TH ALIGN="right"><% mt('Card number') |h %></TH>
- <TD COLSPAN=7>
- <TABLE>
- <TR>
- <TD>
- <INPUT TYPE="text" NAME="payinfo" SIZE=20 MAXLENGTH=19 VALUE="<%$payinfo%>"> </TD>
- <TH><% mt('Exp.') |h %></TH>
- <TD>
- <SELECT NAME="month">
-% for ( ( map "0$_", 1 .. 9 ), 10 .. 12 ) {
-
- <OPTION<% $_ == $month ? ' SELECTED' : '' %>><% $_ %>
-% }
-
- </SELECT>
- </TD>
- <TD> / </TD>
- <TD>
- <SELECT NAME="year">
-% my @a = localtime; for ( $a[5]+1900 .. $a[5]+1915 ) {
-
- <OPTION<% $_ == $year ? ' SELECTED' : '' %>><% $_ %>
-% }
-
- </SELECT>
- </TD>
- </TR>
- </TABLE>
- </TD>
- </TR>
- <TR>
- <TH ALIGN="right"><% mt('CVV2') |h %></TH>
- <TD><INPUT TYPE="text" NAME="paycvv" VALUE="<% $paycvv %>" SIZE=4 MAXLENGTH=4>
- (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/cvv2.html', 480, 352, 'cvv2_popup' ), CAPTION, 'CVV2 Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>)
- </TD>
- </TR>
- <TR>
- <TH ALIGN="right"><% mt('Exact name on card') |h %></TH>
- <TD><INPUT TYPE="text" SIZE=32 MAXLENGTH=80 NAME="payname" VALUE="<%$payname%>"></TD>
- </TR>
-
- <& /elements/location.html,
- 'object' => $location,
- 'no_asterisks' => 1,
- 'address1_label' => emt('Card billing address'),
- &>
-
-% } elsif ( $payby eq 'CHEK' ) {
-%
-% my( $account, $aba, $branch, $payname, $ss, $paytype, $paystate,
-% $stateid, $stateid_state )
-% = ( '', '', '', '', '', '', '', '', '' );
-%
-% #false laziness w/{edit,view}/cust_main/billing.html
-% my $routing_label = $conf->config('echeck-country') eq 'US'
-% ? 'ABA/Routing number'
-% : 'Routing number';
-% my $routing_size = $conf->config('echeck-country') eq 'CA' ? 4 : 10;
-% my $routing_maxlength = $conf->config('echeck-country') eq 'CA' ? 3 : 9;
-
- <INPUT TYPE="hidden" NAME="month" VALUE="12">
- <INPUT TYPE="hidden" NAME="year" VALUE="2037">
- <TR>
- <TD ALIGN="right"><% mt('Account number') |h %></TD>
- <TD><INPUT TYPE="text" SIZE=10 NAME="payinfo1" VALUE="<%$account%>"></TD>
- <TD ALIGN="right"><% mt('Type') |h %></TD>
- <TD><SELECT NAME="paytype"><% join('', map { qq!<OPTION VALUE="$_" !.($paytype eq $_ ? 'SELECTED' : '').">$_</OPTION>" } FS::cust_payby->paytypes) %></SELECT></TD>
- </TR>
- <TR>
- <TD ALIGN="right"><% mt($routing_label) |h %></TD>
- <TD>
- <INPUT TYPE="text" SIZE="<% $routing_size %>" MAXLENGTH="<% $routing_maxlength %>" NAME="payinfo2" VALUE="<%$aba%>">
- (<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('../docs/ach.html', 380, 240, 'ach_popup' ), CAPTION, 'ACH Help', STICKY, AUTOSTATUSCAP, CLOSECLICK, DRAGGABLE ); return false;"><% mt('help') |h %></A>)
- </TD>
- </TR>
-% if ( $conf->config('echeck-country') eq 'CA' ) {
- <TR>
- <TD ALIGN="right"><% mt('Branch number') |h %></TD>
- <TD>
- <INPUT TYPE="text" NAME="payinfo3" VALUE="<%$branch%>" SIZE=6 MAXLENGTH=5>
- </TD>
- </TR>
-% }
- <TR>
- <TD ALIGN="right"><% mt('Bank name') |h %></TD>
- <TD><INPUT TYPE="text" NAME="payname" VALUE="<%$payname%>"></TD>
- </TR>
-
-% if ( $conf->exists('show_bankstate') ) {
- <TR>
- <TD ALIGN="right"><% mt('Bank state') |h %></TD>
- <TD><& /elements/select-state.html,
- 'disable_empty' => 0,
- 'empty_label' => emt('(choose)'),
- 'state' => $paystate,
- 'country' => $cust_main->country,
- 'prefix' => 'pay',
- &>
- </TD>
- </TR>
-% } else {
- <INPUT TYPE="hidden" NAME="paystate" VALUE="<% $paystate %>">
-% }
-
-% if ( $conf->exists('show_ss') ) {
- <TR>
- <TD ALIGN="right">
- <% mt('Account holder') |h %><BR>
- <% mt('Social security or tax ID #') |h %>
- </TD>
- <TD><INPUT TYPE="text" NAME="ss" VALUE="<% $ss %>"></TD>
- </TR>
-% } else {
- <INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>"></TD>
-% }
-
-% if ( $conf->exists('show_stateid') ) {
- <TR>
- <TD ALIGN="right">
- <% mt('Account holder') |h %><BR>
- <% mt("Driver's license or state ID #") |h %>
- </TD>
- <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>"></TD>
- <TD ALIGN="right"><% mt('State') |h %></TD>
- <TD><& /elements/select-state.html,
- 'disable_empty' => 0,
- 'empty_label' => emt('(choose)'),
- 'state' => $stateid_state,
- 'country' => $cust_main->country,
- 'prefix' => 'stateid_',
- &>
- </TD>
- </TR>
-% } else {
- <INPUT TYPE="hidden" NAME="stateid" VALUE="<% $stateid %>">
- <INPUT TYPE="hidden" NAME="stateid_state" VALUE="<% $stateid_state %>">
-% }
-
-% } #end CARD/CHEK-specific section
-
-
-<TR>
- <TD COLSPAN=8>
- <INPUT TYPE="checkbox" CHECKED NAME="save" VALUE="1">
- <% mt('Remember this information') |h %>
- </TD>
-</TR>
-
-<TR>
- <TD COLSPAN=8>
- <INPUT TYPE="checkbox"<% $auto ? ' CHECKED' : '' %> NAME="auto" VALUE="1" onClick="if (this.checked) { document.OneTrueForm.save.checked=true; }">
- <% mt("Charge future payments to this [_1] automatically",$type{$payby}) |h %>
-% if ( @cust_payby ) {
- <% mt('as') |h %>
- <SELECT NAME="weight">
-% for ( 1 .. 1+scalar(grep { $_->payby =~ /^(CARD|CHEK)$/ } @cust_payby) ) {
- <OPTION VALUE="<%$_%>"><% mt( $weight{$_} ) |h %>
-% }
- </SELECT>
-% } else {
- <INPUT TYPE="hidden" NAME="weight" VALUE="1">
-% }
- </TD>
-</TR>
+<& /elements/cust_payby_new.html,
+ 'cust_payby' => \@cust_payby,
+ 'curr_value' => $custpaybynum,
+&>
</TABLE>
</DIV>
my $conf = new FS::Conf;
-#false laziness w/selfservice make_payment.html shortcut for one-country
-my %states = map { $_->state => 1 }
- qsearch('cust_main_county', {
- 'country' => $conf->config('countrydefault') || 'US'
- } );
-my @states = sort { $a cmp $b } keys %states;
-
my $payunique = "webui-payment-". time. "-$$-". rand() * 2**32;
</%init>
$paycvv = $cust_payby->paycvv; # pass it if we got it, running a transaction will clear it
( $month, $year ) = $cust_payby->paydate_mon_year;
$payname = $cust_payby->payname;
+ $cgi->param(-name=>"paytype", -value=>$cust_payby->paytype) unless $cgi->param("paytype");
} else {
my $error = '';
my $paynum = '';
+my $paydate;
+if ($cust_payby->paydate) { $paydate = "$year-$month-01"; }
+else { $paydate = "2037-12-01"; }
+
if ( $cgi->param('batch') ) {
$error = 'Prepayment discounts not supported with batched payments'
'payby' => $payby,
'amount' => $amount,
'payinfo' => $payinfo,
- 'paydate' => "$year-$month-01",
+ 'paydate' => $paydate,
'payname' => $payname,
map { $_ => scalar($cgi->param($_)) }
@{$payby2fields{$payby}}
--- /dev/null
+<%doc>
+ Return a json array containing all free ip addresses within a given block
+ Unless block is larger than /24 - Does somebody really want to populate
+ 65k addresses into a HTML selectbox?
+</%doc>
+<% encode_json($json) %>\
+<%init>
+
+my $json = [];
+
+my $blocknum = $cgi->param('blocknum');
+
+my $addr_block = qsearchs( addr_block => { blocknum => $blocknum });
+
+$json = $addr_block->free_addrs
+ if ref $addr_block && $addr_block->ip_netmask >= 24;
+
+</%init>
+<%doc>
+
+ E911 Fee Report
+
+ Finds billing totals for a given pkgpart where the bill item matches
+ cust_pkg.pkgpart or cust_bill_pkg.pkgpart_override columns.
+
+ Given date range, filter by when the invoice was paid.
+
+ * E911 access lines - SUM(cust_bill_pkg.quantity)
+ * Total fees charged - SUM(cust_bill_pay_pkg.amount)
+ * Fee payments collected - SUM(cust_bill_pkg.setup) + SUM(cust_bill_pkg.recur)
+
+ * Administrative fee (1%) - 1% of Fee Payments Collected
+ * Amount due - 99% of Fee Payments Collected
+
+</%doc>
% if ( $row ) {
-%# pretty minimal report
<& /elements/header.html, 'E911 Fee Report' &>
+
<& /elements/table-grid.html &>
<STYLE TYPE="text/css">
table.grid TD:first-child { font-weight: normal }
text-align: right;
padding: 1px 2px }
</STYLE>
+
<TR><TH COLSPAN=2><% $legend %></TH></TR>
<TR>
- <TD>E911 access lines:</TD>
- <TD><% $row->{quantity} || 0 %></TD>
+ <TD><% mt('E911 access lines') %>:</TD>
+ <TD><% $report{e911_access_lines} %></TD>
</TR>
<TR>
- <TD>Total fees charged: </TD>
- <TD><% $money_char.sprintf('%.2f', $row->{charged_amount}) %></TD>
+ <TD><% mt('Total fees charged') %>: </TD>
+ <TD><% $money_char.$report{fees_charged} %></TD>
</TD>
<TR>
- <TD>Fee payments collected: </TD>
- <TD><% $money_char.sprintf('%.2f', $row->{paid_amount}) %></TD>
+ <TD><% mt('Fee payments collected') %>: </TD>
+ <TD><% $money_char.$report{fees_collected} %></TD>
</TR>
<TR>
- <TD>Administrative fee (1%): </TD>
- <TD><% $money_char.sprintf('%.2f', $row->{paid_amount} * $admin_fee) %></TD>
+ <TD><% mt('Administrative fee') %> (1%): </TD>
+ <TD><% $money_char.$report{admin_fee} %></TD>
</TR>
<TR>
- <TD>Amount due: </TD>
- <TD><% $money_char.sprintf('%.2f', $row->{paid_amount} * (1-$admin_fee) ) %>
- </TD>
+ <TD><% mt('Amount due') %>: </TD>
+ <TD><% $money_char.$report{e911_amount_due} %></TD>
</TR>
</TABLE>
<& /elements/footer.html &>
% }
<%init>
+our $DEBUG;
+
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
# package classes, etc.), do NOT simply loop through this and do a
# bazillion scalar_sql queries. Use a properly grouped aggregate query.
-my $select = 'SELECT cust_bill_pkg.billpkgnum, cust_bill_pkg.quantity, '.
-'cust_bill_pkg.setup, SUM(cust_bill_pay_pkg.amount) AS paid_amount';
-
-my $from = 'FROM cust_pkg
- JOIN cust_bill_pkg USING (pkgnum)
- JOIN cust_bill USING (invnum)
- LEFT JOIN cust_bill_pay_pkg USING (billpkgnum)
- LEFT JOIN cust_bill_pay USING (billpaynum)
-';
-# going by payment application date here, which should be
-# max(invoice date, payment date)
-my $where = "WHERE cust_pkg.pkgpart = $pkgpart
-AND ( (cust_bill_pay._date >= $begin AND cust_bill_pay._date < $end)
- OR cust_bill_pay.paynum IS NULL )";
+my $sql_statement = "
+ SELECT
+ sum(cust_bill_pkg.quantity) as quantity,
+ sum(cust_bill_pay_pkg.amount) as amount,
+ sum(cust_bill_pkg.setup) as setup,
+ sum(cust_bill_pkg.recur) as recur
+ FROM cust_pkg
+ LEFT JOIN cust_bill_pkg USING (pkgnum)
+ LEFT JOIN cust_bill_pay_pkg USING (billpkgnum)
+ LEFT JOIN cust_bill_pay USING (billpaynum)
+";
if ( $agentnum ) {
- $from .= ' JOIN cust_main ON (cust_pkg.custnum = cust_main.custnum)';
- $where .= "\n AND cust_main.agentnum = $agentnum";
+ $sql_statement .= "
+ LEFT JOIN cust_main USING (custnum)
+ WHERE
+ cust_main.agentnum = ?
+ AND ";
+} else {
+ $sql_statement .= "
+ WHERE
+ "
}
+$sql_statement .= "
+ ( cust_bill_pkg.pkgpart_override = ? OR cust_pkg.pkgpart = ? )
+ AND (
+ ( cust_bill_pay._date >= ? AND cust_bill_pay._date < ? )
+ OR cust_bill_pay.paynum IS NULL
+ );
+";
+
+# Preserving this oddball, unexplained epoch substitution
+$end = '' if $end == 4294967295;
-my $subquery = "$select $from $where
-GROUP BY cust_bill_pkg.billpkgnum, cust_bill_pkg.quantity";
-# This has one row for each E911 line item that has any payments applied.
-# Fields are the billpkgnum of the item (currently unused), the number of
-# E911 charges, and the total amount paid (always > 0).
+my @bind_values = (
+ $agentnum ? $agentnum : (),
+ $pkgpart,
+ $pkgpart,
+ $begin || 0,
+ $end || time(),
+);
-# now sum those rows.
-my $sql = "SELECT SUM(quantity) AS quantity, SUM(setup) AS charged_amount,
-SUM(paid_amount) AS paid_amount FROM ($subquery) AS paid_fees"; # no grouping
+if ( $DEBUG ) {
+ warn "\$sql_statement: $sql_statement\n";
+ warn "\@bind_values: ".join(', ',@bind_values)."\n";
+}
-my $sth = dbh->prepare($sql);
-$sth->execute;
+my $sth = dbh->prepare( $sql_statement );
+$sth->execute( @bind_values ) || die $sth->errstr;
my $row = $sth->fetchrow_hashref;
-my $admin_fee = 0.01; # 1% admin fee, allowed in Texas
+my %report = (
+ e911_access_lines => $row->{quantity} || 0,
-$end = '' if $end == 4294967295;
-my $legend = '';
-if ( $agentnum ) {
- $legend = FS::agent->by_key($agentnum)->agent . ', ';
-}
-if ( $begin and $end ) {
- $legend .= time2str('%h %o %Y', $begin) . '—' .
- time2str('%h %o %Y', $end);
+ fees_charged => sprintf(
+ "%.2f",
+ ( $row->{setup} + $row->{recur} ) || 0,
+ ),
+
+ fees_collected => sprintf(
+ "%.2f",
+ ( $row->{amount} || 0 ),
+ ),
+);
+
+# Does everybody use this 1% admin fee? Should this be configurable?
+$report{admin_fee} = sprintf( "%.2f", $report{fees_collected} * 0.01 );
+$report{e911_amount_due} = $report{fees_collected} - $report{admin_fee};
+
+my $begin_text =
+ $begin
+ ? DateTime->from_epoch(epoch => $begin)->mdy('/')
+ : mt('Anytime');
+
+my $end_text = DateTime->from_epoch(epoch => ( $end || time ))->mdy('/');
+
+my $legend = FS::agent->by_key($agentnum)->agent . ', ' if $agentnum;
+if ( $begin && $end ) {
+ $legend .= "$begin_text ↔ $end_text";
} elsif ( $begin ) {
- $legend .= time2str('after %h %o %Y', $begin);
-} elsif ( $end ) {
- $legend .= time2str('before %h %o %Y', $end);
+ $legend .= mt('After')." $begin_text";
} else {
- $legend .= 'any time';
+ $legend .= mt('Through')." $end_text"
}
-$legend = ucfirst($legend);
+
</%init>
}
$pm->prospect_contact
];
- ''
},
sub {
my $pr = shift->part_referral;
<TD COLSPAN=5><% $cust_main->contact |h %></TD>
% if ( $conf->exists('show_ss') ) {
<TH ALIGN="right"><% mt('SS#') |h %></TH>
- <TD><% $conf->exists('unmask_ss')
- ? $cust_main->ss
- : $cust_main->masked('ss') || ' ' %></TD>
+ <TD>
+ <span id="ss_span" style="white-space:nowrap;">
+ <% $conf->exists('unmask_ss')
+ ? $cust_main->ss
+ : $cust_main->masked('ss') || ' ' %>
+% if (
+% $cust_main->ss
+% && !$conf->exists('unmask_ss')
+% && $FS::CurrentUser::CurrentUser->access_right('Unmask customer SSN')
+% ) {
+ <& /elements/link-replace_element_text.html, {
+ target_id => 'ss_span',
+ replace_text => $cust_main->ss,
+ element_type => 'span'
+ } &>
+% }
+ </span>
+ </TD>
% }
</TR>
% if ( $conf->exists('cust_main-enable_spouse') and
<TR>
<TH ALIGN="right"><% $stateid_label %></TH>
- <TD><% $cust_main->masked('stateid') || ' ' %></TD>
+ <TD>
+ <span id="stateid_span" style="white-space:nowrap;">
+ <% $cust_main->masked('stateid') || ' ' %>
+% if (
+% $cust_main->stateid
+% && $FS::CurrentUser::CurrentUser->access_right('Unmask customer DL')
+% ) {
+ <& /elements/link-replace_element_text.html, {
+ target_id => 'stateid_span',
+ replace_text => $cust_main->stateid,
+ element_type => 'span'
+ } &>
+% }
+ </span>
+ </TD>
<TH ALIGN="right"><% $stateid_state_label %></TH>
<TD><% $cust_main->stateid_state || ' ' %></TD>
</TR>
## condition => sub { $payby{MCHK} },
#},
{
- label => 'Batch Electronic check refund',
+ label => 'Enter electronic check refund',
popup => "edit/cust_refund.cgi?popup=1;payby=CHEK;custnum=$custnum",
actionlabel => 'Enter electronic check refund',
width => 440,
% foreach my $prospect_contact ( $prospect_main->prospect_contact ) {
% my $contact = $prospect_contact->contact;
<TR>
- <TH ALIGN="right"><% $prospect_contact->contact_classname %> Contact</TD>
- <TD BGCOLOR="#FFFFFF"><% $contact->line %></TD>
+ <TH ALIGN="right" VALIGN="top"><% $prospect_contact->contact_classname %> Contact</TH>
+ <TD BGCOLOR="#FFFFFF">
+ <% $contact->line %><br>
+ <table>
+% for my $row ( $contact->contact_email ) {
+ <tr><th>E-Mail:</th><td><% $row->emailaddress %></td></tr>
+% }
+% for my $row ( $contact->contact_phone ) {
+ <tr><th><% $row->phone_type->typename %>:</th><td><% $row->phonenum_pretty %></td></tr>
+% }
+% if ( $prospect_contact->comment ) {
+ <tr><th>Comment:</th><td><% $prospect_contact->comment %></td></tr>
+% }
+ </table>
+ </TD>
</TR>
%}