summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2016-08-03 17:52:34 -0700
committerIvan Kohler <ivan@freeside.biz>2016-08-03 17:52:34 -0700
commitcf54023e010df76e0c39ac70902877d7c4c94c6e (patch)
tree8109fa975ae8ecb4435172f9a990435a734c67b6 /FS
parentd7759b49c2ff3b220ab328767645bfed85d18f31 (diff)
parentecf1c9cc0a20be6e489657e005ea771977b9f69c (diff)
Merge branch 'master' of git.freeside.biz:/home/git/freeside
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/ClientAPI/MyAccount.pm28
-rw-r--r--FS/FS/ClientAPI_XMLRPC.pm1
-rw-r--r--FS/FS/Conf.pm7
-rw-r--r--FS/FS/Log.pm33
-rw-r--r--FS/FS/Mason.pm2
-rw-r--r--FS/FS/Record.pm21
-rw-r--r--FS/FS/Schema.pm40
-rw-r--r--FS/FS/TaxEngine/internal.pm6
-rw-r--r--FS/FS/Upgrade.pm9
-rw-r--r--FS/FS/access_right.pm4
-rw-r--r--FS/FS/commission_rate.pm116
-rw-r--r--FS/FS/commission_schedule.pm235
-rw-r--r--FS/FS/cust_bill_pkg_tax_location.pm2
-rw-r--r--FS/FS/cust_credit.pm1
-rw-r--r--FS/FS/cust_location.pm41
-rw-r--r--FS/FS/cust_main/Billing_Realtime.pm59
-rw-r--r--FS/FS/cust_main_county.pm46
-rw-r--r--FS/FS/cust_pay_pending.pm20
-rw-r--r--FS/FS/cust_pkg_reason.pm2
-rw-r--r--FS/FS/log.pm47
-rw-r--r--FS/FS/log_context.pm3
-rw-r--r--FS/FS/msg_template.pm53
-rw-r--r--FS/FS/part_event/Action/bill_agent_credit_schedule.pm76
-rw-r--r--FS/FS/payinfo_Mixin.pm17
-rw-r--r--FS/FS/svc_Common.pm8
-rw-r--r--FS/MANIFEST4
-rw-r--r--FS/bin/freeside-cdrrewrited39
-rw-r--r--FS/t/commission_rate.t5
-rw-r--r--FS/t/commission_schedule.t5
29 files changed, 883 insertions, 47 deletions
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index d767e910d..685821bad 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -1627,6 +1627,34 @@ sub insert_payby {
}
+sub update_payby {
+ my $p = shift;
+
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ my $cust_payby = qsearchs('cust_payby', {
+ 'custnum' => $custnum,
+ 'custpaybynum' => $p->{'custpaybynum'},
+ })
+ or return { 'error' => 'unknown custpaybynum '. $p->{'custpaybynum'} };
+
+ foreach my $field (
+ qw( weight payby payinfo paycvv paydate payname paystate paytype payip )
+ ) {
+ next unless exists($p->{$field});
+ $cust_payby->set($field,$p->{$field});
+ }
+
+ my $error = $cust_payby->replace;
+ if ( $error ) {
+ return { 'error' => $error };
+ } else {
+ return { 'custpaybynum' => $cust_payby->custpaybynum };
+ }
+
+}
+
sub verify_payby {
my $p = shift;
diff --git a/FS/FS/ClientAPI_XMLRPC.pm b/FS/FS/ClientAPI_XMLRPC.pm
index 622f3df05..08c6c2d59 100644
--- a/FS/FS/ClientAPI_XMLRPC.pm
+++ b/FS/FS/ClientAPI_XMLRPC.pm
@@ -129,6 +129,7 @@ sub ss2clientapi {
'list_invoices' => 'MyAccount/list_invoices', #?
'list_payby' => 'MyAccount/list_payby',
'insert_payby' => 'MyAccount/insert_payby',
+ 'update_payby' => 'MyAccount/update_payby',
'delete_payby' => 'MyAccount/delete_payby',
'cancel' => 'MyAccount/cancel', #add to ss cgi!
'payment_info' => 'MyAccount/payment_info',
diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm
index 94b8839a1..1b50006a5 100644
--- a/FS/FS/Conf.pm
+++ b/FS/FS/Conf.pm
@@ -4950,6 +4950,13 @@ and customer address. Include units.',
# },
{
+ 'key' => 'cdr-skip_duplicate_rewrite',
+ 'section' => 'telephony',
+ 'description' => 'Use the freeside-cdrrewrited daemon to prevent billing CDRs with a src, dst and calldate identical to an existing CDR',
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'cdr-charged_party_rewrite',
'section' => 'telephony',
'description' => 'Do charged party rewriting in the freeside-cdrrewrited daemon; useful if CDRs are being dropped off directly in the database and require special charged_party processing such as cdr-charged_party-accountcode or cdr-charged_party-truncate*.',
diff --git a/FS/FS/Log.pm b/FS/FS/Log.pm
index 2fd002093..aed1f3969 100644
--- a/FS/FS/Log.pm
+++ b/FS/FS/Log.pm
@@ -5,13 +5,20 @@ use FS::Record qw(qsearch qsearchs);
use FS::Conf;
use FS::Log::Output;
use FS::log;
-use vars qw(@STACK @LEVELS);
+use vars qw(@STACK %LEVELS);
# override the stringification of @_ with something more sensible.
BEGIN {
- @LEVELS = qw(debug info notice warning error critical alert emergency);
+ # subset of Log::Dispatch levels
+ %LEVELS = (
+ 0 => 'debug',
+ 1 => 'info',
+ 3 => 'warning',
+ 4 => 'error',
+ 5 => 'critical'
+ );
- foreach my $l (@LEVELS) {
+ foreach my $l (values %LEVELS) {
my $sub = sub {
my $self = shift;
$self->log( level => $l, message => @_ );
@@ -100,4 +107,24 @@ sub DESTROY {
splice(@STACK, $self->{'index'}, 1); # delete the stack entry
}
+=item levelnums
+
+Subroutine. Returns ordered list of level nums.
+
+=cut
+
+sub levelnums {
+ sort keys %LEVELS;
+}
+
+=item levelmap
+
+Subroutine. Returns ordered map of level num => level name.
+
+=cut
+
+sub levelmap {
+ map { $_ => $LEVELS{$_} } levelnums;
+}
+
1;
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 1008fd5d8..245bdea88 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -413,6 +413,8 @@ if ( -e $addl_handler_use_file ) {
use FS::olt_site;
use FS::access_user_page_pref;
use FS::part_svc_msgcat;
+ use FS::commission_schedule;
+ use FS::commission_rate;
# Sammath Naur
if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index 7f76d9988..c3d397389 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -2,6 +2,7 @@ package FS::Record;
use base qw( Exporter );
use strict;
+use charnames ':full';
use vars qw( $AUTOLOAD
%virtual_fields_cache %fk_method_cache $fk_table_cache
$money_char $lat_lower $lon_upper
@@ -2913,6 +2914,10 @@ sub ut_coord {
my $coord = $self->getfield($field);
my $neg = $coord =~ s/^(-)//;
+ # ignore degree symbol at the end,
+ # but not otherwise supporting degree/minutes/seconds symbols
+ $coord =~ s/\N{DEGREE SIGN}\s*$//;
+
my ($d, $m, $s) = (0, 0, 0);
if (
@@ -3220,6 +3225,22 @@ sub ut_agentnum_acl {
}
+=item trim_whitespace FIELD[, FIELD ... ]
+
+Strip leading and trailing spaces from the value in the named FIELD(s).
+
+=cut
+
+sub trim_whitespace {
+ my $self = shift;
+ foreach my $field (@_) {
+ my $value = $self->get($field);
+ $value =~ s/^\s+//;
+ $value =~ s/\s+$//;
+ $self->set($field, $value);
+ }
+}
+
=item fields [ TABLE ]
This is a wrapper for real_fields. Code that called
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index ac585108e..8661c4b97 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -1361,6 +1361,7 @@ sub tables_hashref {
'commission_agentnum', 'int', 'NULL', '', '', '', #
'commission_salesnum', 'int', 'NULL', '', '', '', #
'commission_pkgnum', 'int', 'NULL', '', '', '', #
+ 'commission_invnum', 'int', 'NULL', '', '', '',
'credbatch', 'varchar', 'NULL', $char_d, '', '',
],
'primary_key' => 'crednum',
@@ -1396,6 +1397,10 @@ sub tables_hashref {
table => 'cust_pkg',
references => [ 'pkgnum' ],
},
+ { columns => [ 'commission_invnum' ],
+ table => 'cust_bill',
+ references => [ 'invnum' ],
+ },
],
},
@@ -1417,6 +1422,7 @@ sub tables_hashref {
'commission_agentnum', 'int', 'NULL', '', '', '',
'commission_salesnum', 'int', 'NULL', '', '', '',
'commission_pkgnum', 'int', 'NULL', '', '', '',
+ 'commission_invnum', 'int', 'NULL', '', '', '',
#void fields
'void_date', @date_type, '', '',
'void_reason', 'varchar', 'NULL', $char_d, '', '',
@@ -1456,6 +1462,10 @@ sub tables_hashref {
table => 'cust_pkg',
references => [ 'pkgnum' ],
},
+ { columns => [ 'commission_invnum' ],
+ table => 'cust_bill',
+ references => [ 'invnum' ],
+ },
{ columns => [ 'void_reasonnum' ],
table => 'reason',
references => [ 'reasonnum' ],
@@ -7438,6 +7448,36 @@ sub tables_hashref {
],
},
+ 'commission_schedule' => {
+ 'columns' => [
+ 'schedulenum', 'serial', '', '', '', '',
+ 'schedulename', 'varchar', '', $char_d, '', '',
+ 'reasonnum', 'int', 'NULL', '', '', '',
+ 'basis', 'varchar', 'NULL', 32, '', '',
+ ],
+ 'primary_key' => 'schedulenum',
+ 'unique' => [],
+ 'index' => [],
+ },
+
+ 'commission_rate' => {
+ 'columns' => [
+ 'commissionratenum', 'serial', '', '', '', '',
+ 'schedulenum', 'int', '', '', '', '',
+ 'cycle', 'int', '', '', '', '',
+ 'amount', @money_type, '', '',
+ 'percent', 'decimal','', '7,4', '', '',
+ ],
+ 'primary_key' => 'commissionratenum',
+ 'unique' => [ [ 'schedulenum', 'cycle', ] ],
+ 'index' => [],
+ 'foreign_keys' => [
+ { columns => [ 'schedulenum' ],
+ table => 'commission_schedule',
+ },
+ ],
+ },
+
# name type nullability length default local
#'new_table' => {
diff --git a/FS/FS/TaxEngine/internal.pm b/FS/FS/TaxEngine/internal.pm
index db7010c18..3e3e7e520 100644
--- a/FS/FS/TaxEngine/internal.pm
+++ b/FS/FS/TaxEngine/internal.pm
@@ -28,8 +28,10 @@ sub add_sale {
push @{ $self->{items} }, $cust_bill_pkg;
- my @loc_keys = qw( district city county state country );
- my %taxhash = map { $_ => $location->get($_) } @loc_keys;
+ my %taxhash = map { $_ => $location->get($_) }
+ qw( district county state country );
+ # city names in cust_main_county are uppercase
+ $taxhash{'city'} = uc($location->get('city'));
$taxhash{'taxclass'} = $part_item->taxclass;
diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm
index 6f14cd202..3faf47e24 100644
--- a/FS/FS/Upgrade.pm
+++ b/FS/FS/Upgrade.pm
@@ -352,6 +352,9 @@ sub upgrade_data {
tie my %hash, 'Tie::IxHash',
+ #remap log levels
+ 'log' => [],
+
#cust_main (remove paycvv from history, locations, cust_payby, etc)
'cust_main' => [],
@@ -478,8 +481,12 @@ sub upgrade_data {
#populate tax statuses
'tax_status' => [],
- #mark certain taxes as system-maintained
+ #mark certain taxes as system-maintained,
+ # and fix whitespace
'cust_main_county' => [],
+
+ #fix whitespace
+ 'cust_location' => [],
;
\%hash;
diff --git a/FS/FS/access_right.pm b/FS/FS/access_right.pm
index 0ee0aa04a..13a826f29 100644
--- a/FS/FS/access_right.pm
+++ b/FS/FS/access_right.pm
@@ -253,7 +253,9 @@ sub _upgrade_data { # class method
'Generate quotation' => 'Disable quotation',
'Add on-the-fly void credit reason' => 'Add on-the-fly void reason',
'_ALL' => 'Employee preference telephony integration',
- 'Edit customer package dates' => 'Change package start date', #4.x
+ 'Edit customer package dates' => [ 'Change package start date', #4.x
+ 'Change package contract end date',
+ ],
'Resend invoices' => 'Print and mail invoices',
);
diff --git a/FS/FS/commission_rate.pm b/FS/FS/commission_rate.pm
new file mode 100644
index 000000000..dcb596d60
--- /dev/null
+++ b/FS/FS/commission_rate.pm
@@ -0,0 +1,116 @@
+package FS::commission_rate;
+use base qw( FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::commission_rate - Object methods for commission_rate records
+
+=head1 SYNOPSIS
+
+ use FS::commission_rate;
+
+ $record = new FS::commission_rate \%hash;
+ $record = new FS::commission_rate { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::commission_rate object represents a commission rate (a percentage or a
+flat amount) that will be paid on a customer's N-th invoice. The sequence of
+commissions that will be paid on consecutive invoices is the parent object,
+L<FS::commission_schedule>.
+
+FS::commission_rate inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item commissionratenum - primary key
+
+=item schedulenum - L<FS::commission_schedule> foreign key
+
+=item cycle - the ordinal of the billing cycle this commission will apply
+to. cycle = 1 applies to the customer's first invoice, cycle = 2 to the
+second, etc.
+
+=item amount - the flat amount to pay per invoice in commission
+
+=item percent - the percentage of the invoice amount to pay in
+commission
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new commission rate. To add it to the database, see L<"insert">.
+
+=cut
+
+sub table { 'commission_rate'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid commission rate. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ $self->set('amount', '0.00')
+ if $self->get('amount') eq '';
+ $self->set('percent', '0')
+ if $self->get('percent') eq '';
+
+ my $error =
+ $self->ut_numbern('commissionratenum')
+ || $self->ut_number('schedulenum')
+ || $self->ut_number('cycle')
+ || $self->ut_money('amount')
+ || $self->ut_decimal('percent')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/commission_schedule.pm b/FS/FS/commission_schedule.pm
new file mode 100644
index 000000000..375386c33
--- /dev/null
+++ b/FS/FS/commission_schedule.pm
@@ -0,0 +1,235 @@
+package FS::commission_schedule;
+use base qw( FS::o2m_Common FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+use FS::commission_rate;
+use Tie::IxHash;
+
+tie our %basis_options, 'Tie::IxHash', (
+ setuprecur => 'Total sales',
+ setup => 'One-time and setup charges',
+ recur => 'Recurring charges',
+ setup_cost => 'Setup costs',
+ recur_cost => 'Recurring costs',
+ setup_margin => 'Setup charges minus costs',
+ recur_margin_permonth => 'Monthly recurring charges minus costs',
+);
+
+=head1 NAME
+
+FS::commission_schedule - Object methods for commission_schedule records
+
+=head1 SYNOPSIS
+
+ use FS::commission_schedule;
+
+ $record = new FS::commission_schedule \%hash;
+ $record = new FS::commission_schedule { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::commission_schedule object represents a bundle of one or more
+commission rates for invoices. FS::commission_schedule inherits from
+FS::Record. The following fields are currently supported:
+
+=over 4
+
+=item schedulenum - primary key
+
+=item schedulename - descriptive name
+
+=item reasonnum - the credit reason (L<FS::reason>) that will be assigned
+to these commission credits
+
+=item basis - for percentage credits, which component of the invoice charges
+the percentage will be calculated on:
+- setuprecur (total charges)
+- setup
+- recur
+- setup_cost
+- recur_cost
+- setup_margin (setup - setup_cost)
+- recur_margin_permonth ((recur - recur_cost) / freq)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new commission schedule. To add the object to the database, see
+L<"insert">.
+
+=cut
+
+sub table { 'commission_schedule'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+sub delete {
+ my $self = shift;
+ # don't allow the schedule to be removed if it's still linked to events
+ if ($self->part_event) {
+ return 'This schedule is still in use.'; # UI should be smarter
+ }
+ $self->process_o2m(
+ 'table' => 'commission_rate',
+ 'params' => [],
+ ) || $self->delete;
+}
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('schedulenum')
+ || $self->ut_text('schedulename')
+ || $self->ut_number('reasonnum')
+ || $self->ut_enum('basis', [ keys %basis_options ])
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item part_event
+
+Returns a list of billing events (L<FS::part_event> objects) that pay
+commission on this schedule.
+
+=cut
+
+sub part_event {
+ my $self = shift;
+ map { $_->part_event }
+ qsearch('part_event_option', {
+ optionname => 'schedulenum',
+ optionvalue => $self->schedulenum,
+ }
+ );
+}
+
+=item calc_credit INVOICE
+
+Takes an L<FS::cust_bill> object and calculates credit on this schedule.
+Returns the amount to credit. If there's no rate defined for this invoice,
+returns nothing.
+
+=cut
+
+# Some false laziness w/ FS::part_event::Action::Mixin::credit_bill.
+# this is a little different in that we calculate the credit on the whole
+# invoice.
+
+sub calc_credit {
+ my $self = shift;
+ my $cust_bill = shift;
+ die "cust_bill record required" if !$cust_bill or !$cust_bill->custnum;
+ # count invoices before or including this one
+ my $cycle = FS::cust_bill->count('custnum = ? AND _date <= ?',
+ $cust_bill->custnum,
+ $cust_bill->_date
+ );
+ my $rate = qsearchs('commission_rate', {
+ schedulenum => $self->schedulenum,
+ cycle => $cycle,
+ });
+ # we might do something with a rate that applies "after the end of the
+ # schedule" (cycle = 0 or something) so that this can do commissions with
+ # no end date. add that here if there's a need.
+ return unless $rate;
+
+ my $amount;
+ if ( $rate->percent ) {
+ my $what = $self->basis;
+ my $cost = ($what =~ /_cost/ ? 1 : 0);
+ my $margin = ($what =~ /_margin/ ? 1 : 0);
+ my %part_pkg_cache;
+ foreach my $cust_bill_pkg ( $cust_bill->cust_bill_pkg ) {
+
+ my $charge = 0;
+ next if !$cust_bill_pkg->pkgnum; # exclude taxes and fees
+
+ my $cust_pkg = $cust_bill_pkg->cust_pkg;
+ if ( $margin or $cost ) {
+ # look up package costs only if we need them
+ my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
+ my $part_pkg = $part_pkg_cache{$pkgpart}
+ ||= FS::part_pkg->by_key($pkgpart);
+
+ if ( $cost ) {
+ $charge = $part_pkg->get($what);
+ } else { # $margin
+ $charge = $part_pkg->$what($cust_pkg);
+ }
+
+ $charge = ($charge || 0) * ($cust_pkg->quantity || 1);
+
+ } else {
+
+ if ( $what eq 'setup' ) {
+ $charge = $cust_bill_pkg->get('setup');
+ } elsif ( $what eq 'recur' ) {
+ $charge = $cust_bill_pkg->get('recur');
+ } elsif ( $what eq 'setuprecur' ) {
+ $charge = $cust_bill_pkg->get('setup') +
+ $cust_bill_pkg->get('recur');
+ }
+ }
+
+ $amount += ($charge * $rate->percent / 100);
+
+ }
+ } # if $rate->percent
+
+ if ( $rate->amount ) {
+ $amount += $rate->amount;
+ }
+
+ $amount = sprintf('%.2f', $amount + 0.005);
+ return $amount;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::part_event>, L<FS::commission_rate>
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_bill_pkg_tax_location.pm b/FS/FS/cust_bill_pkg_tax_location.pm
index 9a1f22a02..7c67c2df8 100644
--- a/FS/FS/cust_bill_pkg_tax_location.pm
+++ b/FS/FS/cust_bill_pkg_tax_location.pm
@@ -338,7 +338,7 @@ sub upgrade_taxable_billpkgnum {
} #for $i
} else {
# the more complicated case
- $log->warn("mismatched charges and tax links in pkg#$pkgnum",
+ $log->warning("mismatched charges and tax links in pkg#$pkgnum",
object => $cust_bill);
my $tax_amount = sum(map {$_->amount} @tax_links);
# remove all tax link records and recreate them to be 1:1 with
diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm
index 85463724c..e4b1fc07d 100644
--- a/FS/FS/cust_credit.pm
+++ b/FS/FS/cust_credit.pm
@@ -315,6 +315,7 @@ sub check {
|| $self->ut_foreign_keyn('commission_agentnum', 'agent', 'agentnum')
|| $self->ut_foreign_keyn('commission_salesnum', 'sales', 'salesnum')
|| $self->ut_foreign_keyn('commission_pkgnum', 'cust_pkg', 'pkgnum')
+ || $self->ut_foreign_keyn('commission_invnum', 'cust_bill', 'invnum')
;
return $error if $error;
diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm
index 90400984c..fdc2cf8da 100644
--- a/FS/FS/cust_location.pm
+++ b/FS/FS/cust_location.pm
@@ -2,7 +2,7 @@ package FS::cust_location;
use base qw( FS::geocode_Mixin FS::Record );
use strict;
-use vars qw( $import $DEBUG $conf $label_prefix );
+use vars qw( $import $DEBUG $conf $label_prefix $allow_location_edit );
use Data::Dumper;
use Date::Format qw( time2str );
use FS::UID qw( dbh driver_name );
@@ -171,6 +171,10 @@ sub find_or_insert {
delete $nonempty{'locationnum'};
my %hash = map { $_ => $self->get($_) } @essential;
+ foreach (values %hash) {
+ s/^\s+//;
+ s/\s+$//;
+ }
my @matches = qsearch('cust_location', \%hash);
# we no longer reject matches for having different values in nonessential
@@ -292,7 +296,7 @@ sub replace {
# it's a prospect location, then there are no active packages, no billing
# history, no taxes, and in general no reason to keep the old location
# around.
- if ( $self->custnum ) {
+ if ( !$allow_location_edit and $self->custnum ) {
foreach (qw(address1 address2 city state zip country)) {
if ( $self->$_ ne $old->$_ ) {
return "can't change cust_location field $_";
@@ -347,6 +351,10 @@ sub check {
return '' if $self->disabled; # so that disabling locations never fails
+ # maybe should just do all fields in the table?
+ # or in every table?
+ $self->trim_whitespace(qw(district city county state country));
+
my $error =
$self->ut_numbern('locationnum')
|| $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
@@ -887,6 +895,35 @@ sub process_standardize {
close $log;
}
+sub _upgrade_data {
+ my $class = shift;
+
+ # are we going to need to update tax districts?
+ my $use_districts = $conf->config('tax_district_method') ? 1 : 0;
+
+ # trim whitespace on records that need it
+ local $allow_location_edit = 1;
+ foreach my $field (qw(city county state country district)) {
+ foreach my $location (qsearch({
+ table => 'cust_location',
+ extra_sql => " WHERE $field LIKE ' %' OR $field LIKE '% '"
+ })) {
+ my $error = $location->replace;
+ die "$error (fixing whitespace in $field, locationnum ".$location->locationnum.')'
+ if $error;
+
+ if ( $use_districts ) {
+ my $queue = new FS::queue {
+ 'job' => 'FS::geocode_Mixin::process_district_update'
+ };
+ $error = $queue->insert( 'FS::cust_location' => $location->locationnum );
+ die $error if $error;
+ }
+ } # foreach $location
+ } # foreach $field
+ '';
+}
+
=head1 BUGS
=head1 SEE ALSO
diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm
index 0fc2cb7e0..3e4a438d6 100644
--- a/FS/FS/cust_main/Billing_Realtime.pm
+++ b/FS/FS/cust_main/Billing_Realtime.pm
@@ -355,6 +355,35 @@ sub _bop_content {
\%content;
}
+sub _tokenize_card {
+ my ($self,$transaction,$payinfo,$log) = @_;
+
+ if ( $transaction->can('card_token')
+ and $transaction->card_token
+ and $payinfo !~ /^99\d{14}$/ #not already tokenized
+ ) {
+
+ my @cust_payby = $self->cust_payby('CARD','DCRD');
+ @cust_payby = grep { $payinfo == $_->payinfo } @cust_payby;
+ if (@cust_payby > 1) {
+ $log->error('Multiple matching card numbers for cust '.$self->custnum.', could not tokenize card');
+ } elsif (@cust_payby) {
+ my $cust_payby = $cust_payby[0];
+ $cust_payby->payinfo($transaction->card_token);
+ my $error = $cust_payby->replace;
+ if ( $error ) {
+ $log->error('Error storing token for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum.': '.$error);
+ } else {
+ $log->debug('Tokenized card for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum);
+ }
+ } else {
+ $log->debug('No matching card numbers for cust '.$self->custnum.', could not tokenize card');
+ }
+
+ }
+
+}
+
my %bop_method2payby = (
'CC' => 'CARD',
'ECHECK' => 'CHEK',
@@ -369,6 +398,8 @@ sub realtime_bop {
unless $FS::UID::AutoCommit;
local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
+ my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_bop');
my %options = ();
if (ref($_[0]) eq 'HASH') {
@@ -774,18 +805,7 @@ sub realtime_bop {
# Tokenize
###
-
- if ( $transaction->can('card_token') && $transaction->card_token ) {
-
- if ( $options{'payinfo'} eq $self->payinfo ) {
- $self->payinfo($transaction->card_token);
- my $error = $self->replace;
- if ( $error ) {
- warn "WARNING: error storing token: $error, but proceeding anyway\n";
- }
- }
-
- }
+ $self->_tokenize_card($transaction,$options{'payinfo'},$log);
###
# result handling
@@ -1950,6 +1970,7 @@ sub realtime_verify_bop {
if ( $reverse->is_success ) {
$cust_pay_pending->status('done');
+ $cust_pay_pending->statustext('reversed');
my $cpp_authorized_err = $cust_pay_pending->replace;
return $cpp_authorized_err if $cpp_authorized_err;
@@ -2083,19 +2104,7 @@ sub realtime_verify_bop {
# Tokenize
###
- if ( $transaction->can('card_token') && $transaction->card_token ) {
-
- if ( $options{'payinfo'} eq $self->payinfo ) {
- $self->payinfo($transaction->card_token);
- my $error = $self->replace;
- if ( $error ) {
- my $warning = "WARNING: error storing token: $error, but proceeding anyway\n";
- $log->warning($warning);
- warn $warning;
- }
- }
-
- }
+ $self->_tokenize_card($transaction,$options{'payinfo'},$log);
###
# result handling
diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm
index 3c355e823..a1233d083 100644
--- a/FS/FS/cust_main_county.pm
+++ b/FS/FS/cust_main_county.pm
@@ -122,6 +122,9 @@ methods.
sub check {
my $self = shift;
+ $self->trim_whitespace(qw(district city county state country));
+ $self->set('city', uc($self->get('city'))); # also county?
+
$self->exempt_amount(0) unless $self->exempt_amount;
$self->ut_numbern('taxnum')
@@ -701,6 +704,49 @@ sub _upgrade_data {
}
FS::upgrade_journal->set_done($journal);
}
+ # trim whitespace and convert to uppercase in the 'city' field.
+ foreach my $record (qsearch({
+ table => 'cust_main_county',
+ extra_sql => " WHERE city LIKE ' %' OR city LIKE '% ' OR city != UPPER(city)",
+ })) {
+ # any with-trailing-space records probably duplicate other records
+ # from the same city, and if we just fix the record in place, we'll
+ # create an exact duplicate.
+ # so find the record this one would duplicate, and merge them.
+ $record->check; # trims whitespace
+ my %match = map { $_ => $record->get($_) }
+ qw(city county state country district taxname taxclass);
+ my $other = qsearchs('cust_main_county', \%match);
+ if ($other) {
+ my $new_taxnum = $other->taxnum;
+ my $old_taxnum = $record->taxnum;
+ if ($other->tax != $record->tax or
+ $other->exempt_amount != $record->exempt_amount) {
+ # don't assume these are the same.
+ warn "Found duplicate taxes (#$new_taxnum and #$old_taxnum) but they have different rates and can't be merged.\n";
+ } else {
+ warn "Merging tax #$old_taxnum into #$new_taxnum\n";
+ foreach my $table (qw(
+ cust_bill_pkg_tax_location
+ cust_bill_pkg_tax_location_void
+ cust_tax_exempt_pkg
+ cust_tax_exempt_pkg_void
+ )) {
+ foreach my $row (qsearch($table, { 'taxnum' => $old_taxnum })) {
+ $row->set('taxnum' => $new_taxnum);
+ my $error = $row->replace;
+ die $error if $error;
+ }
+ }
+ my $error = $record->delete;
+ die $error if $error;
+ }
+ } else {
+ # else there is no record this one duplicates, so just fix it
+ my $error = $record->replace;
+ die $error if $error;
+ }
+ } # foreach $record
'';
}
diff --git a/FS/FS/cust_pay_pending.pm b/FS/FS/cust_pay_pending.pm
index dfb07b84d..3a8322e06 100644
--- a/FS/FS/cust_pay_pending.pm
+++ b/FS/FS/cust_pay_pending.pm
@@ -455,6 +455,26 @@ sub decline {
$self->replace;
}
+=item reverse [ STATUSTEXT ]
+
+Sets the status of this pending payment to "done" (with statustext
+"reversed (manual)" unless otherwise specified).
+
+Currently only used when resolving pending payments manually.
+
+=cut
+
+# almost complete false laziness with decline,
+# but want to avoid confusion, in case any additional steps/defaults are ever added to either
+sub reverse {
+ my $self = shift;
+ my $statustext = shift || "reversed (manual)";
+
+ $self->status('done');
+ $self->statustext($statustext);
+ $self->replace;
+}
+
# _upgrade_data
#
# Used by FS::Upgrade to migrate to a new database.
diff --git a/FS/FS/cust_pkg_reason.pm b/FS/FS/cust_pkg_reason.pm
index 29b4b0a91..a632ab415 100644
--- a/FS/FS/cust_pkg_reason.pm
+++ b/FS/FS/cust_pkg_reason.pm
@@ -3,7 +3,7 @@ use base qw( FS::otaker_Mixin FS::Record );
use strict;
use vars qw( $ignore_empty_action );
-use FS::Record qw( qsearch ); #qsearchs );
+use FS::Record qw( qsearch qsearchs );
use FS::upgrade_journal;
$ignore_empty_action = 0;
diff --git a/FS/FS/log.pm b/FS/FS/log.pm
index 1d4df730a..d432ee3c6 100644
--- a/FS/FS/log.pm
+++ b/FS/FS/log.pm
@@ -6,6 +6,8 @@ use FS::Record qw( qsearch qsearchs dbdef );
use FS::UID qw( dbh driver_name );
use FS::log_context;
use FS::log_email;
+use FS::upgrade_journal;
+use Tie::IxHash;
=head1 NAME
@@ -115,7 +117,7 @@ sub insert {
'msgtype' => 'admin',
'to' => $log_email->to_addr,
'substitutions' => {
- 'loglevel' => $FS::Log::LEVELS[$self->level], # which has hopefully been loaded...
+ 'loglevel' => $FS::Log::LEVELS{$self->level}, # which has hopefully been loaded...
'logcontext' => $log_email->context, # use the one that triggered the email
'logmessage' => $self->message,
},
@@ -383,6 +385,49 @@ sub search {
};
}
+sub _upgrade_data {
+ my ($class, %opts) = @_;
+
+ return if FS::upgrade_journal->is_done('log__remap_levels');
+
+ tie my %levelmap, 'Tie::IxHash',
+ 2 => 1, #notice -> info
+ 6 => 5, #alert -> critical
+ 7 => 5, #emergency -> critical
+ ;
+
+ # this method should never autocommit
+ # should have been set in upgrade, but just in case...
+ local $FS::UID::AutoCommit = 0;
+
+ # in practice, only debug/info/warning/error appear to have been used,
+ # so this probably won't do anything, but just in case
+ foreach my $old (keys %levelmap) {
+ # FS::log has no replace method
+ my $sql = 'UPDATE log SET level=' . dbh->quote($levelmap{$old}) . ' WHERE level=' . dbh->quote($old);
+ warn $sql unless $opts{'quiet'};
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute() or die $sth->errstr;
+ $sth->finish();
+ }
+
+ foreach my $log_email (
+ qsearch('log_email',{ 'min_level' => 2 }),
+ qsearch('log_email',{ 'min_level' => 6 }),
+ qsearch('log_email',{ 'min_level' => 7 }),
+ ) {
+ $log_email->min_level($levelmap{$log_email->min_level});
+ my $error = $log_email->replace;
+ if ($error) {
+ dbh->rollback;
+ die $error;
+ }
+ }
+
+ FS::upgrade_journal->set_done('log__remap_levels');
+
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/log_context.pm b/FS/FS/log_context.pm
index 83414a680..37befb515 100644
--- a/FS/FS/log_context.pm
+++ b/FS/FS/log_context.pm
@@ -5,10 +5,10 @@ use base qw( FS::Record );
use FS::Record qw( qsearch qsearchs );
my @contexts = ( qw(
- test
bill_and_collect
FS::cust_main::Billing::bill_and_collect
FS::cust_main::Billing::bill
+ FS::cust_main::Billing_Realtime::realtime_bop
FS::cust_main::Billing_Realtime::realtime_verify_bop
FS::pay_batch::import_from_gateway
FS::part_pkg
@@ -23,6 +23,7 @@ my @contexts = ( qw(
upgrade_taxable_billpkgnum
freeside-paymentech-upload
freeside-paymentech-download
+ test
) );
=head1 NAME
diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm
index b89071710..0a16724a8 100644
--- a/FS/FS/msg_template.pm
+++ b/FS/FS/msg_template.pm
@@ -804,6 +804,59 @@ sub _upgrade_data {
###
$self->_populate_initial_data;
+ ###
+ # Move welcome_msgnum to an export
+ ###
+
+ #upgrade_journal loaded by _populate_initial_data
+ unless (FS::upgrade_journal->is_done('msg_template__welcome_export')) {
+ if (my $msgnum = $conf->config('welcome_msgnum')) {
+ eval "use FS::part_export;";
+ die $@ if $@;
+ eval "use FS::part_svc;";
+ die $@ if $@;
+ eval "use FS::export_svc;";
+ die $@ if $@;
+ #create the export
+ my $part_export = new FS::part_export {
+ 'exportname' => 'Welcome Email',
+ 'exporttype' => 'send_email'
+ };
+ my $error = $part_export->insert({
+ 'to_customer' => 1,
+ 'insert_template' => $msgnum,
+ # replicate blank options that would be generated by UI,
+ # to avoid unexpected results from not having them exist
+ 'to_address' => '',
+ 'replace_template' => 0,
+ 'suspend_template' => 0,
+ 'unsuspend_template' => 0,
+ 'delete_template' => 0,
+ });
+ die $error if $error;
+ #attach it to part_svcs
+ my @welcome_exclude_svcparts = $conf->config('svc_acct_welcome_exclude');
+ foreach my $part_svc (
+ qsearch('part_svc',{ 'svcdb' => 'svc_acct', 'disabled' => '' })
+ ) {
+ next if grep { $_ eq $part_svc->svcpart } @welcome_exclude_svcparts;
+ my $export_svc = new FS::export_svc {
+ 'exportnum' => $part_export->exportnum,
+ 'svcpart' => $part_svc->svcpart,
+ };
+ $error = $export_svc->insert;
+ die $error if $error;
+ }
+ #remove the old confs
+ $error = $conf->delete('welcome_msgnum');
+ die $error if $error;
+ $error = $conf->delete('svc_acct_welcome_exclude');
+ die $error if $error;
+ }
+ FS::upgrade_journal->set_done('msg_template__welcome_export');
+ }
+
+
### Fix dump-email_to (needs to happen after _populate_initial_data)
if ($conf->config('dump-email_to')) {
# anyone who still uses dump-email_to should have just had this created
diff --git a/FS/FS/part_event/Action/bill_agent_credit_schedule.pm b/FS/FS/part_event/Action/bill_agent_credit_schedule.pm
new file mode 100644
index 000000000..31189a237
--- /dev/null
+++ b/FS/FS/part_event/Action/bill_agent_credit_schedule.pm
@@ -0,0 +1,76 @@
+package FS::part_event::Action::bill_agent_credit_schedule;
+
+use base qw( FS::part_event::Action );
+use FS::Conf;
+use FS::cust_credit;
+use FS::commission_schedule;
+use Date::Format qw(time2str);
+
+use strict;
+
+sub description { 'Credit the agent based on a commission schedule' }
+
+sub option_fields {
+ 'schedulenum' => { 'label' => 'Schedule',
+ 'type' => 'select-table',
+ 'table' => 'commission_schedule',
+ 'name_col' => 'schedulename',
+ 'disable_empty'=> 1,
+ },
+}
+
+sub eventtable_hashref {
+ { 'cust_bill' => 1 };
+}
+
+our $date_format;
+
+sub do_action {
+ my( $self, $cust_bill, $cust_event ) = @_;
+
+ $date_format ||= FS::Conf->new->config('date_format') || '%x';
+
+ my $cust_main = $self->cust_main($cust_bill);
+ my $agent = $cust_main->agent;
+ return "No customer record for agent ". $agent->agent
+ unless $agent->agent_custnum;
+
+ my $agent_cust_main = $agent->agent_cust_main;
+
+ my $schedulenum = $self->option('schedulenum')
+ or return "no commission schedule selected";
+ my $schedule = FS::commission_schedule->by_key($schedulenum)
+ or return "commission schedule #$schedulenum not found";
+ # commission_schedule::delete tries to prevent this, but just in case
+
+ my $amount = $schedule->calc_credit($cust_bill)
+ or return;
+
+ my $reasonnum = $schedule->reasonnum;
+
+ #XXX shouldn't do this here, it's a localization problem.
+ # credits with commission_invnum should know how to display it as part
+ # of invoice rendering.
+ my $desc = 'from invoice #'. $cust_bill->display_invnum .
+ ' ('. time2str($date_format, $cust_bill->_date) . ')';
+ # could also show custnum and pkgnums here?
+ my $cust_credit = FS::cust_credit->new({
+ 'custnum' => $agent_cust_main->custnum,
+ 'reasonnum' => $reasonnum,
+ 'amount' => $amount,
+ 'eventnum' => $cust_event->eventnum,
+ 'addlinfo' => $desc,
+ 'commission_agentnum' => $cust_main->agentnum,
+ 'commission_invnum' => $cust_bill->invnum,
+ });
+ my $error = $cust_credit->insert;
+ die "Error crediting customer ". $agent_cust_main->custnum.
+ " for agent commission: $error"
+ if $error;
+
+ #return $warning; # currently don't get warnings here
+ return;
+
+}
+
+1;
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index 4f26e8c6f..5f7ce3550 100644
--- a/FS/FS/payinfo_Mixin.pm
+++ b/FS/FS/payinfo_Mixin.pm
@@ -420,15 +420,30 @@ sub paydate_epoch_sql {
Find all records with a credit card payment type and no paycardtype, and
replace them in order to set their paycardtype.
+This method actually just starts a queue job.
+
=cut
sub upgrade_set_cardtype {
my $class = shift;
+ my $table = $class->table or die "upgrade_set_cardtype needs a table";
+
+ if ( ! FS::upgrade_journal->is_done("${table}__set_cardtype") ) {
+ my $job = FS::queue->new({ job => 'FS::payinfo_Mixin::process_set_cardtype' });
+ my $error = $job->insert($table);
+ die $error if $error;
+ FS::upgrade_journal->set_done("${table}__set_cardtype");
+ }
+}
+
+sub process_set_cardtype {
+ my $table = shift;
+
# assign cardtypes to CARD/DCRDs that need them; check_payinfo_cardtype
# will do this. ignore any problems with the cards.
local $ignore_masked_payinfo = 1;
my $search = FS::Cursor->new({
- table => $class->table,
+ table => $table,
extra_sql => q[ WHERE payby IN('CARD','DCRD') AND paycardtype IS NULL ],
});
while (my $record = $search->fetch) {
diff --git a/FS/FS/svc_Common.pm b/FS/FS/svc_Common.pm
index 1dd9ffb63..f2456a56f 100644
--- a/FS/FS/svc_Common.pm
+++ b/FS/FS/svc_Common.pm
@@ -1481,8 +1481,12 @@ sub search {
}
#svcnum
- if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
- push @where, "svcnum = $1";
+ if ( $params->{'svcnum'} ) {
+ my @svcnum = ref( $params->{'svcnum'} )
+ ? @{ $params->{'svcnum'} }
+ : $params->{'svcnum'};
+ @svcnum = grep /^\d+$/, @svcnum;
+ push @where, 'svcnum IN ('. join(',', @svcnum) . ')' if @svcnum;
}
# svcpart
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 83359f118..4184b9ce6 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -870,3 +870,7 @@ FS/webservice_log.pm
t/webservice_log.t
FS/access_user_page_pref.pm
t/access_user_page_pref.t
+FS/commission_schedule.pm
+t/commission_schedule.t
+FS/commission_rate.pm
+t/commission_rate.t
diff --git a/FS/bin/freeside-cdrrewrited b/FS/bin/freeside-cdrrewrited
index 008759008..1745d67a0 100644
--- a/FS/bin/freeside-cdrrewrited
+++ b/FS/bin/freeside-cdrrewrited
@@ -4,7 +4,7 @@ use strict;
use vars qw( $conf );
use FS::Daemon ':all'; #daemonize1 drop_root daemonize2 myexit logfile sig*
use FS::UID qw( adminsuidsetup );
-use FS::Record qw( qsearch qsearchs );
+use FS::Record qw( qsearch qsearchs dbh );
#use FS::cdr;
#use FS::cust_pkg;
#use FS::queue;
@@ -24,12 +24,12 @@ daemonize2();
$conf = new FS::Conf;
-die "not running; cdr-asterisk_forward_rewrite, cdr-charged_party_rewrite ".
- " and cdr-taqua-accountcode_rewrite conf options are all off\n"
+die "not running; relevant conf options are all off\n"
unless _shouldrun();
#--
+#used for taqua
my %sessionnum_unmatch = ();
my $sessionnum_retry = 4 * 60 * 60; # 4 hours
my $sessionnum_giveup = 4 * 24 * 60 * 60; # 4 days
@@ -45,20 +45,25 @@ while (1) {
# instead of just doing this search like normal CDRs
#hmm :/
+ #used only by taqua, should have no effect otherwise
my @recent = grep { ($sessionnum_unmatch{$_} + $sessionnum_retry) > time }
keys %sessionnum_unmatch;
my $extra_sql = scalar(@recent)
? ' AND acctid NOT IN ('. join(',', @recent). ') '
: '';
+ #order matters for removing dupes--only the first is preserved
+ $extra_sql .= ' ORDER BY acctid '
+ if $conf->exists('cdr-skip_duplicate_rewrite');
+
my $found = 0;
- my %skip = ();
+ my %skip = (); #used only by taqua
my %warning = ();
foreach my $cdr (
qsearch( {
'table' => 'cdr',
- 'extra_sql' => 'FOR UPDATE',
+ 'extra_sql' => 'FOR UPDATE', #XXX overwritten by opt below...would fixing this break anything?
'hashref' => {},
'extra_sql' => 'WHERE freesidestatus IS NULL '.
' AND freesiderewritestatus IS NULL '.
@@ -67,11 +72,27 @@ while (1) {
} )
) {
- next if $skip{$cdr->acctid};
+ next if $skip{$cdr->acctid}; #used only by taqua
$found = 1;
my @status = ();
+ if ($conf->exists('cdr-skip_duplicate_rewrite')) {
+ #qsearch can't handle timestamp type of calldate
+ my $sth = dbh->prepare(
+ 'SELECT 1 FROM cdr WHERE src=? AND dst=? AND calldate=? AND acctid < ? LIMIT 1'
+ ) or die dbh->errstr;
+ $sth->execute($cdr->src,$cdr->dst,$cdr->calldate,$cdr->acctid) or die $sth->errstr;
+ my $isdup = $sth->fetchrow_hashref;
+ $sth->finish;
+ if ($isdup) {
+ #we only act on this cdr, not touching previous dupes
+ #if a dupe somehow creeped in previously, too late to fix it
+ $cdr->freesidestatus('done'); #prevent it from being billed
+ push(@status,'duplicate');
+ }
+ }
+
if ( $conf->exists('cdr-asterisk_forward_rewrite')
&& $cdr->dstchannel =~ /^Local\/(\d+)/i && $1 ne $cdr->dst
)
@@ -240,6 +261,7 @@ sub _shouldrun {
|| $conf->exists('cdr-taqua-accountcode_rewrite')
|| $conf->exists('cdr-taqua-callerid_rewrite')
|| $conf->exists('cdr-intl_to_domestic_rewrite')
+ || $conf->exists('cdr-skip_duplicate_rewrite')
|| 0
;
}
@@ -263,6 +285,11 @@ of the following config options are enabled:
=over 4
+=item cdr-skip_duplicate_rewrite
+
+Marks as 'done' (prevents billing for) any CDRs with
+a src, dst and calldate identical to an existing CDR
+
=item cdr-asterisk_australia_rewrite
Classifies Australian numbers as domestic, mobile, tollfree, international, or
diff --git a/FS/t/commission_rate.t b/FS/t/commission_rate.t
new file mode 100644
index 000000000..fb5f43cc5
--- /dev/null
+++ b/FS/t/commission_rate.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::commission_rate;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/commission_schedule.t b/FS/t/commission_schedule.t
new file mode 100644
index 000000000..bbe6b42dc
--- /dev/null
+++ b/FS/t/commission_schedule.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::commission_schedule;
+$loaded=1;
+print "ok 1\n";