summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/Schema.pm20
-rw-r--r--FS/FS/TicketSystem/RT_Internal.pm47
-rw-r--r--FS/FS/part_pkg.pm12
-rw-r--r--FS/FS/part_pkg/rt_field.pm177
-rw-r--r--FS/FS/rt_field_charge.pm132
-rwxr-xr-xhttemplate/edit/part_pkg.cgi10
-rw-r--r--httemplate/elements/select-rt-customfield.html34
7 files changed, 411 insertions, 21 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 7d92d65e5..b09266b29 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -7438,6 +7438,26 @@ sub tables_hashref {
],
},
+ 'rt_field_charge' => {
+ 'columns' => [
+ 'rtfieldchargenum', 'serial', '', '', '', '',
+ 'pkgnum', 'int', '', '', '', '',
+ 'ticketid', 'int', '', '', '', '',
+ 'rate', @money_type, '', '',
+ 'units', 'decimal', '', '10,4', '', '',
+ 'charge', @money_type, '', '',
+ '_date', @date_type, '', '',
+ ],
+ 'primary_key' => 'rtfieldchargenum',
+ 'unique' => [],
+ 'index' => [ ['pkgnum', 'ticketid'] ],
+ 'foreign_keys' => [
+ { columns => [ 'pkgnum' ],
+ table => 'cust_pkg',
+ },
+ ],
+ },
+
# name type nullability length default local
#'new_table' => {
diff --git a/FS/FS/TicketSystem/RT_Internal.pm b/FS/FS/TicketSystem/RT_Internal.pm
index ffee484e9..01806c1b9 100644
--- a/FS/FS/TicketSystem/RT_Internal.pm
+++ b/FS/FS/TicketSystem/RT_Internal.pm
@@ -255,7 +255,10 @@ sub _ticket_info {
}
$ticket_info{'owner'} = $t->OwnerObj->Name;
$ticket_info{'queue'} = $t->QueueObj->Name;
+ $ticket_info{'_cf_sort_order'} = {};
+ my $cf_sort = 0;
foreach my $CF ( @{ $t->CustomFields->ItemsArrayRef } ) {
+ $ticket_info{'_cf_sort_order'}{$CF->Name} = $cf_sort++;
my $name = 'CF.{'.$CF->Name.'}';
$ticket_info{$name} = $t->CustomFieldValuesAsString($CF->Id);
}
@@ -649,5 +652,49 @@ sub selfservice_priority {
}
}
+=item custom_fields
+
+Returns a hash of custom field names and descriptions.
+
+Accepts the following options:
+
+lookuptype - limit results to this lookuptype
+
+valuetype - limit results to this valuetype
+
+Fields must be visible to CurrentUser.
+
+=cut
+
+sub custom_fields {
+ my $self = shift;
+ my %opt = @_;
+ my $lookuptype = $opt{lookuptype};
+ my $valuetype = $opt{valuetype};
+
+ my $CurrentUser = RT::CurrentUser->new();
+ $CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
+ die "RT not configured" unless $CurrentUser->id;
+ my $CFs = RT::CustomFields->new($CurrentUser);
+
+ $CFs->UnLimit;
+
+ $CFs->Limit(FIELD => 'LookupType',
+ OPERATOR => 'ENDSWITH',
+ VALUE => $lookuptype)
+ if $lookuptype;
+
+ $CFs->Limit(FIELD => 'Type',
+ VALUE => $valuetype)
+ if $valuetype;
+
+ my @fields;
+ while (my $CF = $CFs->Next) {
+ push @fields, $CF->Name, ($CF->Description || $CF->Name);
+ }
+
+ return @fields;
+}
+
1;
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 709e137f3..92943f25c 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -773,8 +773,12 @@ sub check {
=item check_options
For a passed I<$options> hashref, validates any options that
-have 'validate' subroutines defined (I<$options> values might
-be altered.) Returns error message, or empty string if valid.
+have 'validate' subroutines defined in the info hash,
+then validates the entire hashref if the price plan has
+its own 'validate' subroutine defined in the info hash
+(I<$options> values might be altered.)
+
+Returns error message, or empty string if valid.
Invoked by L</insert> and L</replace> via the equivalent
methods in L<FS::option_Common>.
@@ -793,6 +797,10 @@ sub check_options {
}
} # else "option does not exist" error?
}
+ if (exists($plans{$self->plan}->{'validate'})) {
+ my $error = &{$plans{$self->plan}->{'validate'}}($options);
+ return $error if $error;
+ }
return '';
}
diff --git a/FS/FS/part_pkg/rt_field.pm b/FS/FS/part_pkg/rt_field.pm
new file mode 100644
index 000000000..1b18720ac
--- /dev/null
+++ b/FS/FS/part_pkg/rt_field.pm
@@ -0,0 +1,177 @@
+package FS::part_pkg::rt_field;
+
+use strict;
+use FS::Conf;
+use FS::TicketSystem;
+use FS::Record qw(qsearchs qsearch);
+use FS::part_pkg::recur_Common;
+use FS::part_pkg::global_Mixin;
+use FS::rt_field_charge;
+
+our @ISA = qw(FS::part_pkg::recur_Common);
+
+our $DEBUG = 0;
+
+use vars qw( $conf $money_char );
+
+FS::UID->install_callback( sub {
+ $conf = new FS::Conf;
+ $money_char = $conf->config('money_char') || '$';
+});
+
+my %custom_field = (
+ 'type' => 'select-rt-customfield',
+ 'lookuptype' => 'RT::Queue-RT::Ticket',
+);
+
+our %info = (
+ 'name' => 'Bill from custom fields in resolved RT tickets',
+ 'shortname' => 'RT custom rate',
+ 'weight' => 65,
+ 'inherit_fields' => [ 'global_Mixin' ],
+ 'fields' => {
+ 'unit_field' => { 'name' => 'Units field',
+ %custom_field,
+ 'validate' => sub { return ${$_[1]} ? '' : 'Units field must be specified' },
+ },
+ 'rate_field' => { 'name' => 'Charge per unit (from RT field)',
+ %custom_field,
+ 'empty_label' => '',
+ },
+ 'rate_flat' => { 'name' => 'Charge per unit (flat)',
+ 'validate' => \&FS::part_pkg::global_Mixin::validate_moneyn },
+ 'display_fields' => { 'name' => 'Display fields',
+ %custom_field,
+ 'multiple' => 1,
+ 'parse' => sub { @_ }, # because /edit/process/part_pkg.pm doesn't grok select multiple
+ },
+ # from global_Mixin, but don't get used by this at all
+ 'unused_credit_cancel' => {'disabled' => 1},
+ 'unused_credit_suspend' => {'disabled' => 1},
+ 'unused_credit_change' => {'disabled' => 1},
+ },
+ 'validate' => sub {
+ my $options = shift;
+ return 'Rate must be specified'
+ unless $options->{'rate_field'} or $options->{'rate_flat'};
+ return 'Cannot specify both flat rate and rate field'
+ if $options->{'rate_field'} and $options->{'rate_flat'};
+ return '';
+ },
+ 'fieldorder' => [ 'unit_field', 'rate_field', 'rate_flat', 'display_fields' ]
+);
+
+sub price_info {
+ my $self = shift;
+ my $str = $self->SUPER::price_info;
+ $str .= ' plus ' if $str;
+ FS::TicketSystem->init();
+ my %custom_fields = FS::TicketSystem->custom_fields();
+ my $rate = $self->option('rate_flat',1);
+ my $rate_field = $self->option('rate_field',1);
+ my $unit_field = $self->option('unit_field');
+ $str .= $rate
+ ? $money_char . sprintf("%.2",$rate)
+ : $custom_fields{$rate_field};
+ $str .= ' x ' . $custom_fields{$unit_field};
+ return $str;
+}
+
+sub calc_setup {
+ my($self, $cust_pkg ) = @_;
+ $self->option('setup_fee');
+}
+
+sub calc_recur {
+ my $self = shift;
+ my($cust_pkg, $sdate, $details, $param ) = @_;
+
+ my $charges = 0;
+
+ $charges += $self->calc_usage(@_);
+ $charges += ($cust_pkg->quantity || 1) * $self->calc_recur_Common(@_);
+
+ $charges;
+
+}
+
+sub can_discount { 0; }
+
+sub calc_usage {
+ my $self = shift;
+ my($cust_pkg, $sdate, $details, $param ) = @_;
+
+ FS::TicketSystem->init();
+
+ # not date delimited--load all resolved tickets
+ # will subtract previous charges below
+ # only way to be sure we've caught everything
+ # limit set to be arbitrarily large (10000)
+ my $tickets = FS::TicketSystem->customer_tickets( $cust_pkg->custnum, 10000, undef, 'resolved');
+
+ my $rate = $self->option('rate_flat',1);
+ my $rate_field = $self->option('rate_field',1);
+ my $unit_field = $self->option('unit_field');
+ my @display_fields = split(', ',$self->option('display_fields',1) || '');
+
+ my %custom_fields = FS::TicketSystem->custom_fields();
+ my $rate_label = $rate
+ ? ''
+ : ' ' . $custom_fields{$rate_field};
+ my $unit_label = $custom_fields{$unit_field};
+
+ $rate_field = 'CF.{' . $rate_field . '}' if $rate_field;
+ $unit_field = 'CF.{' . $unit_field . '}';
+
+ my $charges = 0;
+ foreach my $ticket ( @$tickets ) {
+ next unless $ticket->{$unit_field};
+ next unless $rate || $ticket->{$rate_field};
+ my $trate = $rate || $ticket->{$rate_field};
+ my $tunit = $ticket->{$unit_field};
+ my $subcharge = sprintf('%.2f', $trate * $tunit);
+ my $precharge = _previous_charges( $cust_pkg->pkgnum, $ticket->{'id'} );
+ $subcharge -= $precharge;
+
+ # if field values for previous charges increased,
+ # we can make additional charges here and now,
+ # but if field values were decreased, we just ignore--
+ # credits will have to be applied manually later, if that's what's intended
+ next if $subcharge <= 0;
+
+ my $rt_field_charge = new FS::rt_field_charge {
+ 'pkgnum' => $cust_pkg->pkgnum,
+ 'ticketid' => $ticket->{'id'},
+ 'rate' => $trate,
+ 'units' => $tunit,
+ 'charge' => $subcharge,
+ '_date' => $$sdate,
+ };
+ my $error = $rt_field_charge->insert;
+ die "Error inserting rt_field_charge: $error" if $error;
+ push @$details, $money_char . sprintf('%.2f',$trate) . $rate_label . ' x ' . $tunit . ' ' . $unit_label;
+ push @$details, ' - ' . $money_char . sprintf('%.2f',$precharge) . ' previously charged' if $precharge;
+ foreach my $field (
+ sort { $ticket->{'_cf_sort_order'}{$a} <=> $ticket->{'_cf_sort_order'}{$b} } @display_fields
+ ) {
+ my $label = $custom_fields{$field};
+ my $value = $ticket->{'CF.{' . $field . '}'};
+ push @$details, $label . ': ' . $value if $value;
+ }
+ $charges += $subcharge;
+ }
+ return $charges;
+}
+
+sub _previous_charges {
+ my ($pkgnum, $ticketid) = @_;
+ my $prev = 0;
+ foreach my $rt_field_charge (
+ qsearch('rt_field_charge', { pkgnum => $pkgnum, ticketid => $ticketid })
+ ) {
+ $prev += $rt_field_charge->charge;
+ }
+ return $prev;
+}
+
+1;
diff --git a/FS/FS/rt_field_charge.pm b/FS/FS/rt_field_charge.pm
new file mode 100644
index 000000000..fb01f810e
--- /dev/null
+++ b/FS/FS/rt_field_charge.pm
@@ -0,0 +1,132 @@
+package FS::rt_field_charge;
+use base qw( FS::Record );
+
+use strict;
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::rt_field_charge - Object methods for rt_field_charge records
+
+=head1 SYNOPSIS
+
+ use FS::rt_field_charge;
+
+ $record = new FS::rt_field_charge \%hash;
+ $record = new FS::rt_field_charge { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::rt_field_charge object represents an individual charge
+that has been added to an invoice by a package with the rt_field price plan.
+FS::rt_field_charge inherits from FS::Record.
+The following fields are currently supported:
+
+=over 4
+
+=item rtfieldchargenum - primary key
+
+=item pkgnum - cust_pkg that generated the charge
+
+=item ticketid - RT ticket that generated the charge
+
+=item rate - the rate per unit for the charge
+
+=item units - quantity of units being charged
+
+=item charge - the total amount charged
+
+=item _date - billing date for the charge
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new object. To add the object to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'rt_field_charge'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=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.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid object. 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('rtfieldchargenum')
+ || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum' )
+ || $self->ut_number('ticketid')
+ || $self->ut_money('rate')
+ || $self->ut_float('units')
+ || $self->ut_money('charge')
+ || $self->ut_number('_date')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi
index f2c4aacef..2c8477d8e 100755
--- a/httemplate/edit/part_pkg.cgi
+++ b/httemplate/edit/part_pkg.cgi
@@ -988,6 +988,16 @@ my $html_bottom = sub {
: ''
). '>';
+ } elsif ( $href->{$field}{'type'} eq 'select-rt-customfield' ) {
+
+ $html .= include('/elements/select-rt-customfield.html',
+ 'name' => $layer.'__'.$field,
+ 'curr_value' => $options{$field},
+ map { $_ => $href->{$field}{$_} }
+ grep { $_ !~ /^(name|type|parse)$/ }
+ keys %{ $href->{$field} }
+ );
+
} elsif ( $href->{$field}{'type'} eq 'select-rate' ) {
$html .= include('/elements/select-rate.html',
diff --git a/httemplate/elements/select-rt-customfield.html b/httemplate/elements/select-rt-customfield.html
index 85758d585..488accac3 100644
--- a/httemplate/elements/select-rt-customfield.html
+++ b/httemplate/elements/select-rt-customfield.html
@@ -1,31 +1,27 @@
-<SELECT NAME="<% $opt{name} %>">
+<SELECT NAME="<% $opt{'name'} %>"<% $opt{'multiple'} ? ' MULTIPLE' : '' %>>
% while ( @fields ) {
-<OPTION VALUE="<% shift @fields %>"><% shift @fields %></OPTION>
+% my $value = shift @fields;
+% my $label = shift @fields;
+<OPTION VALUE="<% $value %>"<% $curr_value{$value} ? ' SELECTED' : '' %>><% $label %></OPTION>
% }
</SELECT>
<%init>
my %opt = @_;
-my $lookuptype = $opt{lookuptype};
-my $valuetype = $opt{valuetype};
-# get a list of TimeValue-type custom fields
-my $CurrentUser = RT::CurrentUser->new();
-$CurrentUser->LoadByName($FS::CurrentUser::CurrentUser->username);
-die "RT not configured" unless $CurrentUser->id;
-my $CFs = RT::CustomFields->new($CurrentUser);
-$CFs->Limit(FIELD => 'LookupType',
- OPERATOR => 'ENDSWITH',
- VALUE => $lookuptype)
- if $lookuptype;
-
-$CFs->Limit(FIELD => 'Type',
- VALUE => $valuetype)
- if $valuetype;
+my %curr_value = map { $_ => 1 } split(', ',$opt{'curr_value'});
my @fields;
push @fields, '', $opt{empty_label} if exists($opt{empty_label});
-while (my $CF = $CFs->Next) {
- push @fields, $CF->Name, ($CF->Description || $CF->Name);
+my $conf = new FS::Conf;
+
+if ($conf->config('ticket_system') eq 'RT_Internal') {
+
+ push @fields, FS::TicketSystem->custom_fields(
+ lookuptype => $opt{lookuptype},
+ valuetype => $opt{valuetype},
+ );
+
}
+
</%init>