[freeside-commits] branch master updated. fe25108857542f5d7c460ab831bc782f608179fa

Jonathan Prykop jonathan at 420.am
Sat Jul 16 13:44:25 PDT 2016


The branch, master has been updated
       via  fe25108857542f5d7c460ab831bc782f608179fa (commit)
      from  c91974c5178828304d723d15f0b6405d173fa707 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit fe25108857542f5d7c460ab831bc782f608179fa
Author: Jonathan Prykop <jonathan at freeside.biz>
Date:   Sat Jul 16 15:43:42 2016 -0500

    RT#38973: Bill for time worked on ticket resolution [checkpoint, not ready for backport]

diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 9074ae4..ac58510 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -7418,6 +7418,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 ffee484..01806c1 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 709e137..92943f2 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 0000000..1b18720
--- /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 0000000..fb01f81
--- /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 f2c4aac..2c8477d 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 85758d5..488acca 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>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Schema.pm                                    |   20 +++
 FS/FS/TicketSystem/RT_Internal.pm                  |   47 ++++++
 FS/FS/part_pkg.pm                                  |   12 +-
 FS/FS/part_pkg/rt_field.pm                         |  177 ++++++++++++++++++++
 ...s_srvderive_component.pm => rt_field_charge.pm} |   52 +++---
 httemplate/edit/part_pkg.cgi                       |   10 ++
 httemplate/elements/select-rt-customfield.html     |   34 ++--
 7 files changed, 308 insertions(+), 44 deletions(-)
 create mode 100644 FS/FS/part_pkg/rt_field.pm
 copy FS/FS/{torrus_srvderive_component.pm => rt_field_charge.pm} (53%)




More information about the freeside-commits mailing list