From: Ivan Kohler Date: Sat, 17 Jan 2015 02:53:25 +0000 (-0800) Subject: Merge branch 'issue/SCT-1140' of https://github.com/Jayceh/Freeside X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=4e68cf76788b220cc15f2a080df5e7a1ea4962b8;hp=82fca8a493a6490e385b40ebea690b29bf35bdbd Merge branch 'issue/SCT-1140' of https://github.com/Jayceh/Freeside --- diff --git a/FS/FS/Misc/Getopt.pm b/FS/FS/Misc/Getopt.pm new file mode 100644 index 000000000..a9d0ecff5 --- /dev/null +++ b/FS/FS/Misc/Getopt.pm @@ -0,0 +1,112 @@ +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 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; diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm index 781230fad..25e61d079 100644 --- a/FS/FS/Record.pm +++ b/FS/FS/Record.pm @@ -3336,6 +3336,22 @@ sub count { $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 diff --git a/FS/FS/class_Common.pm b/FS/FS/class_Common.pm index 455cb9f1a..01048ec65 100644 --- a/FS/FS/class_Common.pm +++ b/FS/FS/class_Common.pm @@ -122,14 +122,15 @@ sub _target_table { 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 diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm index 8285cbfdf..330a4547b 100644 --- a/FS/FS/cust_main/Billing_Realtime.pm +++ b/FS/FS/cust_main/Billing_Realtime.pm @@ -3,6 +3,7 @@ package FS::cust_main::Billing_Realtime; 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 ); @@ -319,6 +320,10 @@ my %bop_method2payby = ( 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 = (); @@ -360,6 +365,8 @@ sub realtime_bop { 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; } @@ -542,7 +549,9 @@ sub realtime_bop { ? $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 @@ -593,9 +602,16 @@ sub realtime_bop { }; $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 ); diff --git a/FS/FS/part_export/a2billing.pm b/FS/FS/part_export/a2billing.pm index 7aab01a26..b080d07b9 100644 --- a/FS/FS/part_export/a2billing.pm +++ b/FS/FS/part_export/a2billing.pm @@ -144,6 +144,9 @@ sub export_insert { 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', @@ -233,12 +236,14 @@ sub export_insert { 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 diff --git a/FS/FS/part_export/voip_ms.pm b/FS/FS/part_export/voip_ms.pm index 53a4926e1..7766eac0d 100644 --- a/FS/FS/part_export/voip_ms.pm +++ b/FS/FS/part_export/voip_ms.pm @@ -10,9 +10,14 @@ use URI::Escape; 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 @@ -222,6 +227,9 @@ sub export_unsuspend { ''; } +################ +# PROVISIONING # +################ sub insert_subacct { my ($self, $svc_acct) = @_; @@ -587,6 +595,142 @@ sub reload_cache { } } +################ +# 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 # ############## @@ -614,15 +758,21 @@ sub api_request { '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 diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index 8278afe85..205335b7e 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -19,6 +19,7 @@ tie my %cdr_svc_method, 'Tie::IxHash', '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', @@ -463,16 +464,20 @@ sub calc_usage { #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 @@ -678,6 +683,7 @@ sub reset_usage { # 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_/; diff --git a/FS/FS/svc_acct.pm b/FS/FS/svc_acct.pm index 62cb63362..452f250d8 100644 --- a/FS/FS/svc_acct.pm +++ b/FS/FS/svc_acct.pm @@ -40,6 +40,7 @@ use FS::Record qw( qsearch qsearchs fields dbh dbdef ); 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; @@ -293,25 +294,21 @@ sub table_info { 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' => { @@ -2371,65 +2368,94 @@ sub last_login_text { $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) 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 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 diff --git a/FS/MANIFEST b/FS/MANIFEST index 618ad59ef..581ab0d1f 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -555,6 +555,7 @@ FS/part_event/Action/pkg_agent_credit_pkg.pm 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 diff --git a/bin/cdr-voip_ms.import b/bin/cdr-voip_ms.import new file mode 100755 index 000000000..31fff0bbb --- /dev/null +++ b/bin/cdr-voip_ms.import @@ -0,0 +1,70 @@ +#!/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 +"; +} + diff --git a/debian/freeside.docs b/debian/freeside.docs index 64bf802b8..f4a511b64 100644 --- a/debian/freeside.docs +++ b/debian/freeside.docs @@ -1,3 +1,2 @@ README AGPL -bin/ diff --git a/httemplate/elements/mac_addr.html b/httemplate/elements/mac_addr.html new file mode 100644 index 000000000..1d867f504 --- /dev/null +++ b/httemplate/elements/mac_addr.html @@ -0,0 +1,53 @@ +% if (!$init) { +% if ($clipboard_hack) { +<& init_overlib.html &> + + +% } # if $clipboard_hack +% $init++; +% } +%# the only part to be included in every instance +<% $value |h %> +<%shared> +my $init = 0; + +<%init> +my $clipboard_hack = + $FS::CurrentUser::CurrentUser->option('enable_mask_clipboard_hack'); +my $value = shift; # no other params + diff --git a/httemplate/search/elements/cust_pay_or_refund.html b/httemplate/search/elements/cust_pay_or_refund.html index 8aece0cc2..b5172fb36 100755 --- a/httemplate/search/elements/cust_pay_or_refund.html +++ b/httemplate/search/elements/cust_pay_or_refund.html @@ -239,14 +239,14 @@ if ( $cgi->param('magic') ) { 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"; @@ -262,7 +262,7 @@ if ( $cgi->param('magic') ) { } if ( $cgi->param('custnum') =~ /^(\d+)$/ ) { - push @search, "custnum = $1"; + push @search, "$table.custnum = $1"; } if ( $cgi->param('payby') ) { @@ -421,22 +421,22 @@ if ( $cgi->param('magic') ) { #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'; @@ -517,7 +517,7 @@ if ( $cgi->param('magic') ) { 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". @@ -545,7 +545,7 @@ if ( $cgi->param('magic') ) { $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} ); diff --git a/httemplate/view/cust_main/packages/package.html b/httemplate/view/cust_main/packages/package.html index cf5c98a1c..e47d891f5 100644 --- a/httemplate/view/cust_main/packages/package.html +++ b/httemplate/view/cust_main/packages/package.html @@ -52,8 +52,15 @@ % # One-time charge. Nothing you can do with this, unless: % if ( $curuser->access_right('Modify one-time charge') ) { ( <%onetime_change_link($cust_pkg)%> ) -
% } +% # 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)%> ) +% } +
% % } elsif ( !$cust_pkg->get('cancel') and !$opt{no_links} ) { % diff --git a/httemplate/view/elements/svc_Common.html b/httemplate/view/elements/svc_Common.html index 6c5c90201..b7f7a2c63 100644 --- a/httemplate/view/elements/svc_Common.html +++ b/httemplate/view/elements/svc_Common.html @@ -222,6 +222,7 @@ my $format_field = sub { $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 @@ -273,8 +274,10 @@ my $format_field = sub { $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