--- /dev/null
+package FS::Misc::Getopt;
+
+=head1 NAME
+
+FS::Misc::Getopt - Getopt::Std for Freeside command line/cron scripts
+
+=head1 SYNOPSIS
+
+#!/usr/bin/perl
+
+use FS::Getopt;
+use FS::other_stuff;
+our %opt;
+
+getopts('AB');
+
+print "Option A: $opt{A}
+Option B: $opt{B}
+Start date: $opt{start}
+End date: $opt{end}
+Freeside user: $opt{user}
+Verbose mode: $DEBUG
+";
+
+=head1 DESCRIPTION
+
+This module provides a wrapper around Getopt::Std::getopts() that
+automatically processes certain common command line options, and sets
+up a convenient environment for writing a script.
+
+Options will go into %main::opt, as if you had called getopts(..., \%opt).
+All options recognized by the wrapper use (and will always use) lowercase
+letters as flags, so it's safe for a script to define its options as
+capital letters.
+
+Options recognized by the wrapper do not need to be included in the string
+argument to getopts().
+
+The following command line options are recognized:
+
+=over 4
+
+=item -v: Verbose mode. Sets $main::DEBUG.
+
+=item -s: Start date. If provided, FS::Getopt will parse it as a date
+and set $opt{start} to the resulting Unix timestamp value. If parsing fails,
+displays an error and exits.
+
+=item -e: End date. As for -s; sets $opt{end}.
+
+=back
+
+Calling getopts() also performs some additional setup:
+
+=over 4
+
+=item Exports a function named &main::debug, which performs a warn() if
+$DEBUG has a true value, and if not, does nothing. This should be used to
+output informational messages. (warn() is for warnings.)
+
+=item Captures the first command line argument after any switches and
+sets $opt{user} to that value. If a value isn't provided, prints an error
+and exits.
+
+=item Loads L<FS::UID> and calls adminsuidsetup() to connect to the database.
+
+=back
+
+=cut
+
+use strict;
+use base 'Exporter';
+use Getopt::Std ();
+use FS::UID qw(adminsuidsetup);
+use FS::Misc::DateTime qw(parse_datetime day_end);
+
+our @EXPORT = qw( getopts debug );
+
+sub getopts {
+ my $optstring = shift;
+ my %opt;
+ $optstring .= 's:e:v';
+
+ Getopt::Std::getopts($optstring, \%opt);
+
+ $opt{user} = shift(@ARGV)
+ or die "Freeside username required.\n";
+ adminsuidsetup($opt{user})
+ or die "Failed to connect as user '$opt{user}'.\n";
+
+ # now we have config access
+ if ( $opt{s} ) {
+ $opt{start} = parse_datetime($opt{s})
+ or die "Unable to parse start date '$opt{s}'.\n";
+ }
+ if ( $opt{e} ) {
+ $opt{end} = parse_datetime($opt{e})
+ or die "Unable to parse start date '$opt{e}'.\n";
+ $opt{end} = day_end($opt{end});
+ }
+ if ( $opt{v} ) {
+ $main::DEBUG ||= $opt{v};
+ }
+
+ %main::opt = %opt;
+}
+
+sub debug {
+ warn(@_, "\n") if $main::DEBUG;
+}
+
+1;
$self->scalar_sql($sql, @_);
}
+=item row_exists [ WHERE [, PLACEHOLDER ...] ]
+
+Convenience method for the common case of "SELECT 1 FROM table ... LIMIT 1"
+with optional (but almost always needed) WHERE.
+
+=cut
+
+sub row_exists {
+ my($self, $where) = (shift, shift);
+ my $table = $self->table or die 'row_exists called on object of class '.ref($self);
+ my $sql = "SELECT 1 FROM $table";
+ $sql .= " WHERE $where" if $where;
+ $sql .= " LIMIT 1";
+ $self->scalar_sql($sql, @_);
+}
+
=back
=head1 SUBROUTINES
sub _target_column { 'classnum'; }
-use vars qw( $_category_table );
+use vars qw( %_category_table );
sub _category_table {
- return $_category_table if $_category_table;
my $self = shift;
- $_category_table = $self->table;
- $_category_table =~ s/class/category/ # s/_class$/_category/
- or die "can't determine an automatic category table for $_category_table";
- $_category_table;
+ return $_category_table{ ref $self } ||= do {
+ my $category_table = $self->table;
+ $category_table =~ s/class/category/ # s/_class$/_category/
+ or die "can't determine an automatic category table for $category_table";
+ $category_table;
+ }
}
=head1 BUGS
use strict;
use vars qw( $conf $DEBUG $me );
use vars qw( $realtime_bop_decline_quiet ); #ugh
+use Carp;
use Data::Dumper;
use Business::CreditCard 0.28;
use FS::UID qw( dbh );
sub realtime_bop {
my $self = shift;
+ confess "Can't call realtime_bop within another transaction ".
+ '($FS::UID::AutoCommit is false)'
+ unless $FS::UID::AutoCommit;
+
local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
my %options = ();
if ( $DEBUG ) {
warn "$me realtime_bop (new): $options{method} $options{amount}\n";
warn " cc_surcharge = $cc_surcharge\n";
+ }
+ if ( $DEBUG > 2 ) {
warn " $_ => $options{$_}\n" foreach keys %options;
}
? $options{'balance'}
: $self->balance;
+ warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
$self->select_for_update; #mutex ... just until we get our pending record in
+ warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
#the checks here are intended to catch concurrent payments
#double-form-submission prevention is taken care of in cust_pay_pending::check
};
$cust_pay_pending->payunique( $options{payunique} )
if defined($options{payunique}) && length($options{payunique});
+
+ warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
+ if $DEBUG > 1;
my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
return $cpp_new_err if $cpp_new_err;
+ warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
+ if $DEBUG > 1;
+ warn Dumper($cust_pay_pending) if $DEBUG > 2;
+
my( $action1, $action2 ) =
split( /\s*\,\s*/, $payment_gateway->gateway_action );
zipcode => $location->zip,
simultaccess => $part_pkg->option('a2billing_simultaccess'),
typepaid => $part_pkg->option('a2billing_type'),
+ email_notification => $cust_main->invoicing_list_emailonly_scalar,
+ notify_email => ($cust_main->invoicing_list_emailonly_scalar ? 1 : 0),
+ credit_notification => $cust_main->credit_limit || $self->option('credit') || 0,
sip_buddy => 1,
company_name => $cust_main->company,
activated => 't',
my $cc_did_id = $self->a2b_find('cc_did', 'svcnum', $svc->svcnum);
- my $destination = 'SIP/' . $svc->phonenum . '@' . $svc_acct->username;
+ my $destination = 'SIP/user-'. $svc_acct->username. '@'. $svc->sip_server. "!". $svc->phonenum;
my %cc_did_destination = (
destination => $destination,
priority => 1,
id_cc_card => $cc_card_id,
id_cc_did => $cc_did_id,
+ validated => 1,
+ voip_call => 1,
);
# and if there's already a destination, change it to point to
use JSON;
use HTTP::Request::Common;
use Cache::FileCache;
+use FS::Record qw(dbh);
+use FS::Misc::DateTime qw(parse_datetime);
+use DateTime;
our $me = '[voip.ms]';
-our $DEBUG = 2;
+our $DEBUG = 0;
+# our $DEBUG = 1; # log requests
+# our $DEBUG = 2; # log requests and content of replies
our $base_url = 'https://voip.ms/api/v1/rest.php';
# cache cities and provinces
'';
}
+################
+# PROVISIONING #
+################
sub insert_subacct {
my ($self, $svc_acct) = @_;
}
}
+################
+# CALL DETAILS #
+################
+
+=item import_cdrs START, END
+
+Retrieves CDRs for calls in the date range from START to END and inserts them
+as a new CDR batch. On success, returns a new cdr_batch object. On failure,
+returns an error message. If there are no new CDRs, returns nothing.
+
+=cut
+
+sub import_cdrs {
+ my ($self, $start, $end) = @_;
+ $start ||= 0; # all CDRs ever
+ $end ||= time;
+ $DEBUG ||= $self->option('debug');
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+
+ ($start, $end) = ($end, $start) if $end < $start;
+ $start = DateTime->from_epoch(epoch => $start, time_zone => 'local');
+ $end = DateTime->from_epoch(epoch => $end, time_zone => 'local');
+ my $accountnum = $self->option('account');
+ my $cdr_batch;
+ # can't retrieve more than 92 days at a time
+ # actually, it's even less than that; on large batches their server
+ # sometimes cuts off in mid-sentence. so set the chunk size smaller.
+ while ( $start < $end ) {
+
+ my $this_end = $start->clone;
+ $this_end->add(days => 14);
+ if ($this_end > $end) {
+ $this_end = $end;
+ }
+
+ my $date_from = $start->strftime('%F');
+ my $date_to = $this_end->strftime('%F');
+ warn "retrieving CDRs from $date_from to $date_to\n" if $DEBUG;
+ my $timezone = $start->strftime('%z') / 100; # integer number of hours
+ my $result = $self->api_request('getCDR', {
+ date_from => $date_from,
+ date_to => $date_to,
+ answered => 1,
+ noanswer => 1,
+ busy => 1,
+ failed => 1,
+ timezone => $timezone,
+ });
+ if ( $result->{status} eq 'success' ) {
+ if (!$cdr_batch) {
+ # then create one
+ my $cdrbatchname = 'voip_ms-' . $self->exportnum . '-' . $end->epoch;
+ $cdr_batch = FS::cdr_batch->new({ cdrbatch => $cdrbatchname });
+ my $error = $cdr_batch->insert;
+ if ( $error ) {
+ dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ foreach ( @{ $result->{cdr} } ) {
+ my $uniqueid = $_->{uniqueid};
+ # download ranges may overlap; avoid double-importing CDRs
+ if ( FS::cdr->row_exists("uniqueid = ?", $uniqueid) ) {
+ warn "skipped call with uniqueid = '$uniqueid' (already imported)\n"
+ if $DEBUG;
+ next;
+ }
+ # in this case, and probably in other cases in the near future,
+ # easier to do this than to create a FS::cdr::* format module
+ my $hash = {
+ disposition => $_->{disposition},
+ calldate => $_->{date},
+ dst => $_->{destination},
+ uniqueid => $_->{uniqueid},
+ upstream_price => $_->{total},
+ upstream_dst_regionname => $_->{description},
+ clid => $_->{callerid},
+ duration => $_->{seconds},
+ billsec => $_->{seconds},
+ cdrbatchnum => $cdr_batch->cdrbatchnum,
+ };
+ if ( $_->{date} ) {
+ $hash->{startdate} = parse_datetime($_->{date});
+ }
+ if ( $_->{account} eq $accountnum ) {
+ # calls made from the master account, not a subaccount
+ # charged_party will be set to the source number
+ $hash->{charged_party} = '';
+ } elsif ( $_->{account} =~ /^${accountnum}_(\w+)$/ ) {
+ $hash->{charged_party} = $1;
+ } else {
+ warn "skipped call with account = '$_->{account}'\n";
+ next;
+ }
+ if ( $_->{callerid} =~ /<(\w+)>$/ ) {
+ $hash->{src} = $1;
+ } elsif ( $_->{callerid} =~ /^(\w+)$/ ) {
+ $hash->{src} = $1;
+ } else {
+ # else what? they don't have a source number anywhere else
+ warn "skipped call with unparseable callerid '$_->{callerid}'\n";
+ next;
+ }
+
+ my $cdr = FS::cdr->new($hash);
+ my $error = $cdr->insert;
+ if ( $error ) {
+ dbh->rollback if $oldAutoCommit;
+ return "$error (uniqueid $_->{uniqueid})";
+ }
+ } # foreach @{ $result->{cdr} }
+
+ } elsif ( $result->{status} eq 'no_cdr' ) {
+ # normal result if there are no CDRs, duh
+ next; # there may still be more CDRs later
+ } else {
+ dbh->rollback if $oldAutoCommit;
+ return "$me error retrieving CDRs: $result->{status}";
+ }
+
+ # we've retrieved and inserted this sub-batch of CDRs
+ $start->add(days => 15);
+ } # while ( $start < $end )
+
+ if ( $cdr_batch ) {
+ dbh->commit if $oldAutoCommit;
+ return $cdr_batch;
+ } else {
+ # no CDRs were ever found
+ return;
+ }
+}
+
##############
# API ACCESS #
##############
'Accept' => 'text/json',
);
- warn "$me $method\n" . $request->as_string ."\n" if $DEBUG;
+ warn "$me $method\n" if $DEBUG;
+ warn $request->as_string ."\n" if $DEBUG > 1;
my $ua = LWP::UserAgent->new;
my $response = $ua->request($request);
- warn "$me received\n" . $response->as_string ."\n" if $DEBUG;
+ warn "$me received\n" . $response->as_string ."\n" if $DEBUG > 1;
if ( !$response->is_success ) {
return { status => $response->content };
}
- return decode_json($response->content);
+ local $@;
+ my $decoded_response = eval { decode_json($response->content) };
+ if ( $@ ) {
+ die "Error parsing response:\n" . $response->content . "\n\n";
+ }
+ return $decoded_response;
}
=item api_insist METHOD, CONTENT
'svc_pbx.svcnum' => 'Freeside service # (svc_pbx.svcnum)',
'svc_pbx.ip.src' => 'PBX name to source IP address',
'svc_pbx.ip.dst' => 'PBX name to destination IP address',
+ 'svc_acct.username' => 'Username (svc_acct.username)',
;
tie my %rating_method, 'Tie::IxHash',
#my @invoice_details_sort;
# for tagging invoice details
+ # (unfortunate; should be a svc_x class method or table_info item or
+ # something)
my $phonenum;
if ( $svc_table eq 'svc_phone' ) {
$phonenum = $svc_x->phonenum;
} elsif ( $svc_table eq 'svc_pbx' ) {
$phonenum = $svc_x->title;
+ } elsif ( $svc_table eq 'svc_acct' ) {
+ $phonenum = $svc_x->username;
}
$formatter->phonenum($phonenum);
#first rate any outstanding CDRs not yet rated
- # XXX eventually use an FS::Cursor for this
+ # use FS::Cursor for this starting in 4.x
my $cdr_search = $svc_x->psearch_cdrs(%options);
$cdr_search->limit(1000);
$cdr_search->increment(0); # because we're changing their status as we go
# tells whether cust_bill_pkg_detail should return a single line for
# each phonenum
+# i think this is currently unused?
sub sum_usage {
my $self = shift;
$self->option('output_format') =~ /^sum_/;
use FS::Msgcat qw(gettext);
use FS::UI::bytecount;
use FS::UI::Web;
+use FS::PagedSearch qw( psearch ); # XXX in v4, replace with FS::Cursor
use FS::part_pkg;
use FS::part_svc;
use FS::svc_acct_pop;
label => 'Quota', #Mail storage limit
type => 'text',
disable_inventory => 1,
- disable_select => 1,
},
'file_quota'=> {
label => 'File storage limit',
type => 'text',
disable_inventory => 1,
- disable_select => 1,
},
'file_maxnum'=> {
label => 'Number of files limit',
type => 'text',
disable_inventory => 1,
- disable_select => 1,
},
'file_maxsize'=> {
label => 'File size limit',
type => 'text',
disable_inventory => 1,
- disable_select => 1,
},
'_password' => 'Password',
'gid' => {
$self->last_login ? ctime($self->last_login) : 'unknown';
}
-=item get_cdrs TIMESTAMP_START TIMESTAMP_END [ 'OPTION' => 'VALUE ... ]
+=item psearch_cdrs OPTIONS
+
+Returns a paged search (L<FS::PagedSearch>) for Call Detail Records
+associated with this service. For svc_acct, "associated with" means that
+either the "src" or the "charged_party" field of the CDR matches the
+"username" field of the service.
=cut
-sub get_cdrs {
- my($self, $start, $end, %opt ) = @_;
-
- my $did = $self->username; #yup
-
- my $prefix = $opt{'default_prefix'}; #convergent.au '+61'
-
- my $for_update = $opt{'for_update'} ? 'FOR UPDATE' : '';
-
- #SELECT $for_update * FROM cdr
- # WHERE calldate >= $start #need a conversion
- # AND calldate < $end #ditto
- # AND ( charged_party = "$did"
- # OR charged_party = "$prefix$did" #if length($prefix);
- # OR ( ( charged_party IS NULL OR charged_party = '' )
- # AND
- # ( src = "$did" OR src = "$prefix$did" ) # if length($prefix)
- # )
- # )
- # AND ( freesidestatus IS NULL OR freesidestatus = '' )
-
- my $charged_or_src;
- if ( length($prefix) ) {
- $charged_or_src =
- " AND ( charged_party = '$did'
- OR charged_party = '$prefix$did'
- OR ( ( charged_party IS NULL OR charged_party = '' )
- AND
- ( src = '$did' OR src = '$prefix$did' )
- )
- )
- ";
- } else {
- $charged_or_src =
- " AND ( charged_party = '$did'
- OR ( ( charged_party IS NULL OR charged_party = '' )
- AND
- src = '$did'
- )
- )
- ";
+sub psearch_cdrs {
+ my($self, %options) = @_;
+ my @fields;
+ my %hash;
+ my @where;
+ my $did = dbh->quote($self->username);
+
+ my $prefix = $options{'default_prefix'} || ''; #convergent.au '+61'
+ my $prefixdid = dbh->quote($prefix . $self->username);
+
+ my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
+
+ if ( $options{inbound} ) {
+ # these will be selected under their DIDs
+ push @where, "FALSE";
}
- qsearch(
- 'select' => "$for_update *",
+ my @orwhere;
+ if (!$options{'disable_charged_party'}) {
+ push @orwhere,
+ "charged_party = $did",
+ "charged_party = $prefixdid";
+ }
+ if (!$options{'disable_src'}) {
+ push @orwhere,
+ "src = $did AND charged_party IS NULL",
+ "src = $prefixdid AND charged_party IS NULL";
+ }
+ push @where, '(' . join(' OR ', @orwhere) . ')';
+
+ # $options{'status'} = '' is meaningful; for the rest of them it's not
+ if ( exists $options{'status'} ) {
+ $hash{'freesidestatus'} = $options{'status'};
+ }
+ if ( $options{'cdrtypenum'} ) {
+ $hash{'cdrtypenum'} = $options{'cdrtypenum'};
+ }
+ if ( $options{'calltypenum'} ) {
+ $hash{'calltypenum'} = $options{'calltypenum'};
+ }
+ if ( $options{'begin'} ) {
+ push @where, 'startdate >= '. $options{'begin'};
+ }
+ if ( $options{'end'} ) {
+ push @where, 'startdate < '. $options{'end'};
+ }
+ if ( $options{'nonzero'} ) {
+ push @where, 'duration > 0';
+ }
+
+ my $extra_sql = join(' AND ', @where);
+ if ($extra_sql) {
+ if (keys %hash) {
+ $extra_sql = " AND ".$extra_sql;
+ } else {
+ $extra_sql = " WHERE ".$extra_sql;
+ }
+ }
+ return psearch({
+ 'select' => '*',
'table' => 'cdr',
- 'hashref' => {
- #( freesidestatus IS NULL OR freesidestatus = '' )
- 'freesidestatus' => '',
- },
- 'extra_sql' => $charged_or_src,
+ 'hashref' => \%hash,
+ 'extra_sql' => $extra_sql,
+ 'order_by' => "ORDER BY startdate $for_update",
+ });
+}
- );
+=item get_cdrs (DEPRECATED)
+
+Like psearch_cdrs, but returns all the L<FS::cdr> objects at once, in a
+single list. Arguments are the same as for psearch_cdrs.
+
+=cut
+sub get_cdrs {
+ my $self = shift;
+ my $psearch = $self->psearch_cdrs(@_);
+ qsearch ( $psearch->{query} )
}
# sub radius_groups has moved to svc_Radius_Mixin
FS/part_event/Action/pkg_employee_credit.pm
FS/part_event/Action/pkg_employee_credit_pkg.pm
FS/Misc/DateTime.pm
+FS/Misc/Getopt.pm
FS/cgp_rule.pm
t/cgp_rule.t
FS/cgp_rule_condition.pm
--- /dev/null
+#!/usr/bin/perl
+
+use strict;
+use FS::Misc::Getopt;
+use FS::cdr_batch;
+use FS::part_export;
+use FS::Record qw(qsearch qsearchs dbh);
+use Date::Format 'time2str';
+
+###
+# parse command line
+###
+
+our %opt;
+getopts('');
+
+$FS::UID::AutoCommit = 0;
+
+my @exports = qsearch('part_export', { exporttype => 'voip_ms' });
+if (!@exports) {
+ die "There are no voip.ms exports configured.\n";
+}
+
+foreach my $part_export (@exports) {
+ debug "Account #".$part_export->option('account');
+
+ if (!$opt{start}) {
+ # find the most recently downloaded batch
+ my $exportnum = $part_export->exportnum;
+ my $most_recent = qsearchs({
+ 'table' => 'cdr_batch',
+ 'hashref' => { 'cdrbatch' => {op=>'like',
+ value=>'voip_ms-' . $exportnum . '-%'}
+ },
+ 'order_by' => 'ORDER BY _date DESC LIMIT 1',
+ });
+ if ( $most_recent ) {
+ $most_recent->cdrbatch =~ /-(\d+)$/; # extract the end timestamp
+ $opt{start} = $1;
+ debug "Downloading records since most recent batch: ".
+ time2str('%Y-%m-%d', $opt{start});
+ } else {
+ $opt{start} = 1262332800;
+ debug "Downloading records since January 2010.";
+ }
+ }
+
+ $opt{end} ||= time;
+
+ my $error_or_batch = $part_export->import_cdrs( $opt{start}, $opt{end} );
+ if ( ref $error_or_batch ) {
+ debug "Created batch #".$error_or_batch->cdrbatchnum;
+ dbh->commit;
+ } elsif ( $error_or_batch ) {
+ warn $error_or_batch;
+ dbh->rollback;
+ } else {
+ debug "No CDRs found."
+ }
+}
+
+sub usage {
+ "Usage: \n cdr-voip_ms.import [ options ] user
+ Options:
+ -v: be verbose
+ -s date: start date (defaults to the most recent batch date)
+ -e date: end date
+";
+}
+
--- /dev/null
+% if (!$init) {
+% if ($clipboard_hack) {
+<& init_overlib.html &>
+<script>
+<&| /elements/onload.js &>
+ var transform_text = function(str) {
+ var regexp = new RegExp('.*(..):(..):(..):(..):(..):(..).*');
+ return str.replace(regexp, '$1$2$3$4$5$6');
+ }
+ var span_onclick = function() {
+ var input = document.createElement('INPUT');
+ // IE8 doesn't support textContent
+ var str = this.textContent || this.innerText || '';
+ input.value = transform_text(str);
+ input.style.position = 'absolute';
+ input.style.top = '0px';
+ input.style.left = '0px';
+ input.onblur = function() { input.parentNode.removeChild(input) }
+ this.appendChild(input);
+ input.select();
+ }
+ // set this on any ".mac_addr" object in the doc
+
+ // IE8 doesn't support getElementsByClassName
+ var els = document.getElementsByTagName('span');
+ for (var i = 0; i < els.length; i++) {
+ if (els[i].className = 'mac_addr') {
+ els[i].id = 'span_mac_addr' + i;
+ els[i].onclick = span_onclick;
+ }
+ }
+</&>
+</SCRIPT>
+<style type="text/css">
+.mac_addr {
+ border-bottom: 1px dotted blue;
+ color: blue;
+ position: relative;
+}
+</style>
+% } # if $clipboard_hack
+% $init++;
+% }
+%# the only part to be included in every instance
+<SPAN CLASS="mac_addr"><% $value |h %></SPAN>
+<%shared>
+my $init = 0;
+</%shared>
+<%init>
+my $clipboard_hack =
+ $FS::CurrentUser::CurrentUser->option('enable_mask_clipboard_hack');
+my $value = shift; # no other params
+</%init>
if ( $cgi->param('magic') eq '_date' ) {
if ( $cgi->param('agentnum') && $cgi->param('agentnum') =~ /^(\d+)$/ ) {
- push @search, "agentnum = $1"; # $search{'agentnum'} = $1;
+ push @search, "cust_main.agentnum = $1"; # $search{'agentnum'} = $1;
my $agent = qsearchs('agent', { 'agentnum' => $1 } );
die "unknown agentnum $1" unless $agent;
$title = $agent->agent. " $title";
}
if ( $cgi->param('refnum') && $cgi->param('refnum') =~ /^(\d+)$/ ) {
- push @search, "refnum = $1";
+ push @search, "cust_main.refnum = $1";
my $part_referral = qsearchs('part_referral', { 'refnum' => $1 } );
die "unknown refnum $1" unless $part_referral;
$title = $part_referral->referral. " $title";
}
if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
- push @search, "custnum = $1";
+ push @search, "$table.custnum = $1";
}
if ( $cgi->param('payby') ) {
#for cust_pay_pending... statusNOT=done
if ( $cgi->param('statusNOT') =~ /^(\w+)$/ ) {
- push @search, "status != '$1'";
+ push @search, "$table.status != '$1'";
}
my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
- push @search, "_date >= $beginning ",
- "_date <= $ending";
+ push @search, "$table._date >= $beginning ",
+ "$table._date <= $ending";
if ( $table eq 'cust_pay_void' ) {
my($v_beginning, $v_ending) =
FS::UI::Web::parse_beginning_ending($cgi, 'void');
- push @search, "void_date >= $v_beginning ",
- "void_date <= $v_ending";
+ push @search, "$table.void_date >= $v_beginning ",
+ "$table.void_date <= $v_ending";
}
- push @search, FS::UI::Web::parse_lt_gt($cgi, $amount_field );
+ push @search, FS::UI::Web::parse_lt_gt($cgi, "$table.$amount_field" );
$orderby = '_date';
my $search = ' WHERE '. join(' AND ', @search);
- $count_query = "SELECT COUNT(*), SUM($amount_field) ";
+ $count_query = "SELECT COUNT(*), SUM($table.$amount_field) ";
$count_query .= ', SUM(' . "FS::$table"->unapplied_sql . ') '
if $unapplied;
$count_query .= "FROM $table $addl_from".
$cgi->param('payby') =~ /^(\w+)$/ or die "illegal payby";
my $payby = $1;
- $count_query = "SELECT COUNT(*), SUM($amount_field) FROM $table".
+ $count_query = "SELECT COUNT(*), SUM($table.$amount_field) FROM $table".
" WHERE payinfo = '$payinfo' AND payby = '$payby'".
" AND ". $curuser->agentnums_sql;
@count_addl = ( '$%.2f total '.$opt{name_verb} );
% # One-time charge. Nothing you can do with this, unless:
% if ( $curuser->access_right('Modify one-time charge') ) {
( <%onetime_change_link($cust_pkg)%> )
- <BR>
% }
+% # also, you can discount it
+% if ( $curuser->access_right('Discount customer package')
+% && ! scalar($cust_pkg->cust_pkg_discount_active)
+% && ! scalar($cust_pkg->part_pkg->part_pkg_discount)
+% ) {
+ ( <%pkg_discount_link($cust_pkg)%> )
+% }
+ <BR>
%
% } elsif ( !$cust_pkg->get('cancel') and !$opt{no_links} ) {
%
$field = $f;
$type = 'text';
}
+ warn "$field\t$type\t$value\n";
my $columndef = $part_svc->part_svc_column($field);
# skip fields that are fixed and empty
$value = time2str("$date_format %H:%M",$value)
} elsif ( $type eq 'checkbox' ) {
$value = $value eq 'Y' ? emt('Yes') : emt('No');
- } elsif ( $type eq 'mac_addr' and $value =~ /\w/) {
- $value .= ' ('. (Net::MAC::Vendor::lookup($value))->[0]. ')'
+ } elsif ( $type =~ /(input-)?mac_addr/ and $value =~ /\w/) {
+ my $vendor = Net::MAC::Vendor::lookup($value)->[0];
+ $value .= " ($vendor)" if $vendor;
+ $value = $m->scomp('/elements/mac_addr.html', $value);
}
# 'link' option