Option date for payment
+=item order_number
+
+Optional order number
+
=back
Example:
#optional
'_date' => 1397977200, #UNIX timestamp
+ 'order_number' => '12345',
);
if ( $result->{'error'} ) {
# basic checks using Data::Password;
# options for Data::Password
- $DICTIONARY = 4; # minimum length of disallowed words
+ $DICTIONARY = 0; # minimum length of disallowed words, false value disables dictionary checking
$MINLEN = $conf->config('passwordmin') || 6;
$MAXLEN = $conf->config('passwordmax') || 8;
$GROUPS = 4; # must have all 4 'character groups': numbers, symbols, uppercase, lowercase
# # lists of disallowed words
# @DICTIONARIES = qw( /usr/share/dict/web2 /usr/share/dict/words /usr/share/dict/linux.words );
+ # first, no dictionary checking but require 4 char groups
my $error = IsBadPassword($password);
- $error = 'must contain at least one each of numbers, symbols, and lowercase and uppercase letters'
- if $error eq 'contains less than 4 character groups'; # avoid confusion
+
+ # but they can get away with 3 char groups, so long as they're not using a word
+ if ($error eq 'contains less than 4 character groups') {
+ $DICTIONARY = 4; # default from Data::Password is 5
+ $GROUPS = 3;
+ $error = IsBadPassword($password);
+ # take note--we never actually report dictionary word errors;
+ # 4 char groups is the rule, 3 char groups and no dictionary words is an acceptable exception
+ $error = 'should contain at least one each of numbers, symbols, lowercase and uppercase letters'
+ if $error;
+ }
+
+ # maybe also at some point add an exception for any passwords of sufficient length,
+ # see https://xkcd.com/936/
+
$error = 'Invalid password - ' . $error if $error;
return $error if $error;
'gatewaynum', 'int', 'NULL', '', '', '', # payment_gateway FK
'processor', 'varchar', 'NULL', $char_d, '', '', # module name
'auth', 'varchar','NULL',16, '', '', # CC auth number
- 'order_number','varchar','NULL',$char_d, '', '', # transaction number
+ 'order_number','varchar','NULL',256, '', '', # transaction number
],
'primary_key' => 'paynum',
#i guess not now, with cust_pay_pending, if we actually make it here, we _do_ want to record it# 'unique' => [ [ 'payunique' ] ],
'gatewaynum', 'int', 'NULL', '', '', '', # payment_gateway FK
'processor', 'varchar', 'NULL', $char_d, '', '', # module name
'auth', 'varchar','NULL',16, '', '', # CC auth number
- 'order_number', 'varchar','NULL',$char_d, '', '', # transaction number
+ 'order_number', 'varchar','NULL',256, '', '', # transaction number
#void fields
'void_date', @date_type, '', '',
'src', # caller
'dst', # called
skip(2), # reason
- # call id
+ # call id
_cdr_date_parser_maker('startdate'), # time
'billsec', # duration
- skip(3), # ringtime
- # status
- # resller_charge
+ skip(2), # ringtime
+ # reseller_charge
'upstream_price',# customer_charge
],
);
+++ /dev/null
-package FS::cdr::vss;
-
-use strict;
-use vars qw( @ISA %info $tmp_mon $tmp_mday $tmp_year );
-use Time::Local;
-use FS::cdr qw(_cdr_date_parser_maker);
-
-@ISA = qw(FS::cdr);
-
-%info = (
- 'name' => 'VSS',
- 'weight' => 120,
- 'header' => 1,
- 'import_fields' => [
-
- skip(1), # i_customer
- 'accountcode', # account_id
- 'src', # caller
- 'dst', # called
- skip(2), # reason
- # call id
- _cdr_date_parser_maker('startdate'), # time
- 'billsec', # duration
- skip(3), # ringtime
- # status
- # resller_charge
- 'upstream_price',# customer_charge
- ],
-);
-
-sub skip { map {''} (1..$_[0]) }
-
-1;
=item recur_show_zero
-=cut
+Whether to show a zero recurring amount. This is true if the package or its
+definition has the recur_show_zero flag, and the recurring fee is actually
+zero for this period.
-sub recur_show_zero { shift->_X_show_zero('recur'); }
-sub setup_show_zero { shift->_X_show_zero('setup'); }
+=cut
-sub _X_show_zero {
+sub recur_show_zero {
my( $self, $what ) = @_;
- return 0 unless $self->$what() == 0 && $self->pkgnum;
+ return 0 unless $self->get('recur') == 0 && $self->pkgnum;
+
+ $self->cust_pkg->_X_show_zero('recur');
+}
+
+=item setup_show_zero
- $self->cust_pkg->_X_show_zero($what);
+Whether to show a zero setup charge. This requires the package or its
+definition to have the setup_show_zero flag, but it also returns false if
+the package's setup date is before this line item's start date.
+
+=cut
+
+sub setup_show_zero {
+ my $self = shift;
+ return 0 unless $self->get('setup') == 0 && $self->pkgnum;
+ my $cust_pkg = $self->cust_pkg;
+ return 0 if ( $self->sdate || 0 ) > ( $cust_pkg->setup || 0 );
+ return $cust_pkg->_X_show_zero('setup');
}
=item credited [ BEFORE, AFTER, OPTIONS ]
my $unitrecur = 0;
my @recur_discounts = ();
my $sdate;
+
+ my $override_quantity;
+
# Conditions for billing the recurring fee:
# - the package doesn't have a future start date
# - and it's not suspended
#base_cancel???
$unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better
+ if ( $param{'override_quantity'} ) {
+ $override_quantity = $param{'override_quantity'};
+ }
+
if ( $increment_next_bill ) {
my $next_bill;
}
}
- }
+ } # end of recurring fee
warn "\$setup is undefined" unless defined($setup);
warn "\$recur is undefined" unless defined($recur);
'unitsetup' => sprintf('%.2f', $unitsetup),
'recur' => $recur,
'unitrecur' => sprintf('%.2f', $unitrecur),
- 'quantity' => $cust_pkg->quantity,
+ 'quantity' => $override_quantity || $cust_pkg->quantity,
'details' => \@details,
'discounts' => [ @setup_discounts, @recur_discounts ],
'hidden' => $part_pkg->hidden,
? uc($options{'paytype'})
: uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
+ $content{company} = $self->company if $self->company;
+
if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
$content{account_name} = $self->company;
} else {
use DBI;
#use FS::Record qw(qsearch qsearchs);
+tie our %query_style, 'Tie::IxHash', (
+ 'simple' => 'Simple (a single value for the recurring charge)',
+ 'detailed' => 'Detailed (multiple rows for invoice details)',
+);
+
+our @detail_cols = ( qw(amount format duration phonenum accountcode
+ startdate regionname detail)
+ );
%info = (
'name' => 'Base charge plus additional fees for external services from a configurable SQL query',
'shortname' => 'External SQL query',
'query' => { 'name' => 'SQL query',
'default' => '',
},
+
+ 'query_style' => {
+ 'name' => 'Query output style',
+ 'type' => 'select',
+ 'select_options' => \%query_style,
+ },
+
},
'fieldorder' => [qw( recur_method cutoff_day ),
FS::part_pkg::prorate_Mixin::fieldorder,
- qw( datasrc db_username db_password query
+ qw( datasrc db_username db_password query query_style
)],
'weight' => '58',
);
my $self = shift;
my($cust_pkg, $sdate, $details, $param ) = @_;
my $price = 0;
+ my $quantity; # can be overridden; if not we use the default
my $dbh = DBI->connect( map { $self->option($_) }
qw( datasrc db_username db_password )
) {
my $id = $cust_svc->svc_x->id;
$sth->execute($id) or die $sth->errstr;
- $price += $sth->fetchrow_arrayref->[0];
+
+ if ( $self->option('query_style') eq 'detailed' ) {
+
+ while (my $row = $sth->fetchrow_hashref) {
+ if (exists $row->{amount}) {
+ if ( $row->{amount} eq '' ) {
+ # treat as zero
+ } elsif ( $row->{amount} =~ /^\d+(?:\.\d+)?$/ ) {
+ $price += $row->{amount};
+ } else {
+ die "sql_external query returned non-numeric amount: $row->{amount}";
+ }
+ }
+ if (exists $row->{quantity}) {
+ $quantity ||= 0;
+ if ( $row->{quantity} eq '' ) {
+ # treat as zero
+ } elsif ( $row->{quantity} =~ /^\d+$/ ) {
+ $quantity += $row->{quantity};
+ } else {
+ die "sql_external query returned non-integer quantity: $row->{quantity}";
+ }
+ }
+
+ my $detail = FS::cust_bill_pkg_detail->new;
+ foreach my $field (@detail_cols) {
+ if (exists $row->{$field}) {
+ $detail->set($field, $row->{$field});
+ }
+ }
+ if (!$detail->get('detail')) {
+ die "sql_external query did not return detail description";
+ # or make something up?
+ # or just don't insert the detail?
+ }
+
+ push @$details, $detail;
+ } # while $row
+
+ } else {
+
+ # simple style: returns only a single value, which is the price
+ $price += $sth->fetchrow_arrayref->[0];
+
+ }
+ }
+ $price = sprintf('%.2f', $price);
+
+ # XXX probably shouldn't allow package quantity > 1 on these packages.
+ if ($cust_pkg->quantity > 1) {
+ warn "sql_external package #".$cust_pkg->pkgnum." has quantity > 1\n";
}
+ $param->{'override_quantity'} = $quantity;
$param->{'override_charges'} = $price;
($cust_pkg->quantity || 1) * $self->calc_recur_Common($cust_pkg,$sdate,$details,$param);
}
--- /dev/null
+#!/usr/bin/perl
+
+use FS::Misc::Getopt;
+use FS::Record qw(qsearch qsearchs dbh);
+
+getopts('cpifXr:');
+my $dbh = dbh;
+$FS::UID::AutoCommit = 0;
+
+sub usage() {
+ "Usage: bulk_void -s start -e end
+ -r void_reason
+ { -c | -p | -i }
+ [ -X ]
+ <user>
+-s, -e: date range (required)
+-r: void reason text (required)
+-c, -p, -i, -f: void credits, payments, invoices
+-X: commit changes
+";
+}
+
+if (!$opt{start} or !$opt{end} or !$opt{r}) {
+ die usage;
+}
+
+print "DRY RUN--changes will not be committed.\n" unless $opt{X};
+
+my $date = " WHERE _date >= $opt{start} AND _date <= $opt{end}";
+
+my %tables = (
+ c => 'cust_credit',
+ p => 'cust_pay',
+ i => 'cust_bill',
+);
+
+my $reason = $opt{r};
+
+foreach my $k (keys %tables) {
+ next unless $opt{$k};
+ my $table = $tables{$k};
+ debug("$table:");
+ my $done_count = 0;
+ my $error_count = 0;
+
+ my $cursor = FS::Cursor->new({
+ table => $table,
+ extra_sql => $date,
+ });
+ my $error;
+ while (my $record = $cursor->fetch) {
+ $error = $record->void($reason);
+ if ( $error ) {
+ $error = "$table #" . $record->get($record->primary_key) . ": $error";
+ print "$error\n";
+ $error_count++;
+ if ( $opt{X} ) {
+ $dbh->rollback;
+ exit(1);
+ }
+ } else {
+ $done_count++;
+ }
+ }
+ print " $table voided: $done_count\n errors: $error_count\n";
+}
+
+if ( $opt{X} ) {
+ $dbh->commit;
+ print "Committed changes.\n";
+} else {
+ $dbh->rollback;
+ print "Reverted changes.\n";
+}
+
--- /dev/null
+#!/usr/bin/perl
+
+use strict;
+use Text::CSV;
+use FS::Misc::Getopt;
+use FS::Record qw(qsearchs qsearch dbh);
+use FS::rate;
+use FS::rate_region;
+use FS::rate_prefix;
+use FS::rate_detail;
+
+getopts('');
+
+$FS::UID::AutoCommit = 0;
+my $dbh = dbh;
+
+my $file = shift or usage();
+open my $in, '<', $file or die "$file: $!\n";
+my $csv = Text::CSV->new({ binary => 1, auto_diag => 2 });
+# set header row
+$csv->column_names($csv->getline($in));
+
+my $error;
+
+my $granularity = 1;
+# default is to charge per second; edit this if needed
+
+while (my $row = $csv->getline_hr($in)) {
+ print $csv->string;
+
+ # ProfileKey is just a number
+ my $rate = qsearchs('rate', { 'ratename' => $row->{'ProfileKey'} });
+ if (!$rate) {
+ $rate = FS::rate->new({ 'ratename' => $row->{'ProfileKey'} });
+ $error = $rate->insert;
+ die $error if $error;
+ }
+
+ # DestinationId looks like "Country - City" or "Country - Mobile -
+ # Carrier" (or sometimes just "Country - Mobile").
+ my $region = qsearchs('rate_region', {
+ 'regionname' => $row->{'DestinationId'}
+ });
+ if (!$region) {
+ $region = FS::rate_region->new({
+ 'regionname' => $row->{'DestinationId'}
+ });
+ $error = $region->insert;
+ die $error if $error;
+ }
+
+ # Prefix strings found in there look like
+ # "e164:123-45-6nnnnnnn-"
+ # The first group of digits is the country code, any others are the
+ # prefix. Sometimes the nnnn's are NNNN's. The dashes are not guaranteed
+ # to be anywhere specific.
+ # Catchall prefixes start with "-A", which has a meaning like "match
+ # anything, but at a lower priority than a digit match".
+ # NANPA numbers use "1-", and for a catchall area code use "1-AAA-".
+ my $cc_long = $row->{CountryCodeLong};
+ $cc_long =~ /^e164:(\d+)-([\d-]*)A*-?n+-$/i;
+ my $countrycode = $1;
+ if (!$countrycode) { # totally legit reasons for this, e.g. 1-AAA-411
+ warn "can't parse number prefix:\n$cc_long\n";
+ next;
+ }
+ my $prefix = $2;
+ $prefix =~ s/-//g;
+
+ my %prefix = (
+ 'regionnum' => $region->regionnum,
+ 'countrycode' => $countrycode,
+ 'npa' => $prefix,
+ );
+ my $rate_prefix = qsearchs('rate_prefix', \%prefix);
+ if (!$rate_prefix) {
+ $rate_prefix = FS::rate_prefix->new(\%prefix);
+ $error = $rate_prefix->insert;
+ die $error if $error;
+ }
+
+ # enough to identify the detail
+ my %detail = (
+ 'ratenum' => $rate->ratenum,
+ 'dest_regionnum' => $region->regionnum,
+ 'cdrtypenum' => '',
+ 'ratetimenum' => '',
+ );
+ my $dest_detail = qsearchs('rate_detail', \%detail);
+ # ProfileRate is 5 decimal places, same as rate_detail.min_charge
+ if (!$dest_detail) {
+ $dest_detail = FS::rate_detail->new({
+ %detail,
+ 'min_included' => 0,
+ 'min_charge' => $row->{ProfileRate},
+ 'sec_granularity' => $granularity,
+ });
+ $error = $dest_detail->insert;
+ } else {
+ local $FS::Record::nowarn_identical = 1;
+ $dest_detail->set('min_charge' => $row->{ProfileRate});
+ $error = $dest_detail->replace;
+ }
+ die $error if $error;
+}
+dbh->commit;
+print "Finished.\n";
+
+
+sub usage {
+ die "Usage: rate-intl.import <user> <file>.csv\n\n";
+}
+
# but explicitly avoid setting ship_ fields
my $new = new FS::cust_main ( {
- map { ( $_, scalar($cgi->param($_)) ) } (fields('cust_main')),
- map { ( "ship_$_", '' ) } (FS::cust_main->location_fields)
+ (map { ( $_, scalar($cgi->param($_)) ) } (fields('cust_main'))),
+ (map { ( "ship_$_", '' ) } (FS::cust_main->location_fields))
} );
$new->invoice_noemail( ($cgi->param('invoice_email') eq 'Y') ? '' : 'Y' );
-% my $error = shift;
+% my $error = $_[0];
% $m->notes('error', $error);
<& /elements/header.html, mt("Error") &>
% while (@_) {
-<P><FONT SIZE="+1" COLOR="#ff0000"><% $error |h %></FONT>
+<P><FONT SIZE="+1" COLOR="#ff0000"><% shift |h %></FONT>
%}
% $m->flush_buffer();
<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $cgi->param('custnum') |h %>">
-% my @sched_item = qsearch('sched_item', { 'disabled' => '', });
-% my @username = map $_->access_user->username, @sched_item;
-% foreach my $username (@username) {
- <INPUT TYPE="hidden" NAME="username" VALUE="<% $username |h %>">
-% }
-
-Length:
+<TABLE>
+<TR>
+<TD STYLE="text-align: right">Length:</TD>
+<TD>
<SELECT NAME="LengthMin">
% for ( my $hours = .5; $hours < 10.5; $hours += .5 ) {
% my $min = $hours * 60;
><% $hours %> hour<% $hours > 1 ? 's' : '' %>
% }
</SELECT>
-<BR>
-<BR>
+</TD>
+</TR>
+
+% my @sched_item = qsearch('sched_item', { 'disabled' => '', });
+% my @username = map $_->access_user->username, @sched_item;
+
+<TR>
+<TD STYLE="text-align: right">Installer:</TD>
+<TD>
+<SELECT NAME="username" ID="username_select" MULTIPLE>
+% foreach my $username (@username) {
+ <OPTION SELECTED><% $username |h %></OPTION>
+% }
+</SELECT>
+</TD>
+</TR>
+</TABLE>
+
+<BR>
<INPUT TYPE="submit" VALUE="Schedule appointment">
</FORM>
#false laziness w/FS:;cust_main::realtime_bop - check both to make sure
# working correctly
if ( $payby eq 'CARD' &&
- grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save') ) {
+ ( (grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save'))
+ || $conf->exists('business-onlinepayment-verification')
+ )
+ ) {
$new->set( 'paycvv' => $paycvv );
} else {
$new->set( 'paycvv' => '');
or errorpage(emt("Illegal address2"));
my $address2 = $1;
- push @cust_main, qsearch( 'cust_main',
- { 'address2' => { 'op' => 'ILIKE',
- 'value' => $address2 } } );
- push @cust_main, qsearch( 'cust_main',
- { 'ship_address2' => { 'op' => 'ILIKE',
- 'value' => $address2 } } );
+ # matching at the start or end of an address, but not in the middle
+ my @where;
+ foreach my $toggle (0,1) {
+ push @where, 'LOWER(cust_location.address2) LIKE LOWER('
+ . dbh->quote($toggle ? $address2 . '%' : '%' . $address2)
+ . ')';
+ }
+
+ push @cust_main, qsearch({
+ 'debug' => 1,
+ 'table' => 'cust_main',
+ 'addl_from' => 'JOIN cust_location ON (cust_location.locationnum IN (cust_main.bill_locationnum, cust_main.ship_locationnum))',
+ 'extra_sql' => 'WHERE ' . join(' OR ',@where),
+ });
\@cust_main;
}
label => mt('Customer Class'),
field => 'cust_classnum',
multiple => 1,
- 'pre_options' => [ '' => emt('(none)') ],
+ 'pre_options' => [ '0' => emt('(none)') ],
'all_selected' => 1,
&>