RT#38973: Bill for time worked on ticket resolution [validate queueids]
[freeside.git] / FS / FS / part_pkg / rt_field.pm
1 package FS::part_pkg::rt_field;
2
3 use strict;
4 use FS::Conf;
5 use FS::TicketSystem;
6 use FS::Record qw(qsearchs qsearch);
7 use FS::part_pkg::recur_Common;
8 use FS::part_pkg::global_Mixin;
9 use FS::rt_field_charge;
10
11 our @ISA = qw(FS::part_pkg::recur_Common);
12
13 our $DEBUG = 0;
14
15 use vars qw( $conf $money_char );
16
17 FS::UID->install_callback( sub {
18   $conf = new FS::Conf;
19   $money_char = $conf->config('money_char') || '$';
20 });
21
22 my %custom_field = (
23   'type'        => 'select-rt-customfield',
24   'lookuptype'  => 'RT::Queue-RT::Ticket',
25 );
26
27 my %multiple = (
28   'multiple' => 1,
29   'parse' => sub { @_ }, # because /edit/process/part_pkg.pm doesn't grok select multiple
30 );
31
32 our %info = (
33   'name'      =>  'Bill from custom fields in resolved RT tickets',
34   'shortname' =>  'RT custom rate',
35   'weight'    => 65,
36   'inherit_fields' => [ 'global_Mixin' ],
37   'fields'    =>  {
38     'queueids'       => { 'name' => 'Queues',
39                           'type' => 'select-rt-queue',
40                           %multiple,
41                           'validate' => sub { return ${$_[1]} ? '' : 'Queue must be specified' },
42                         },
43     'unit_field'     => { 'name' => 'Units field',
44                           %custom_field,
45                           'validate' => sub { return ${$_[1]} ? '' : 'Units field must be specified' },
46                         },
47     'rate_field'     => { 'name' => 'Charge per unit (from RT field)',
48                           %custom_field,
49                           'empty_label' => '',
50                         },
51         'rate_flat'      => { 'name' => 'Charge per unit (flat)',
52                           'validate' => \&FS::part_pkg::global_Mixin::validate_moneyn },
53     'display_fields' => { 'name' => 'Display fields',
54                           %custom_field,
55                           %multiple,
56                         },
57     # from global_Mixin, but don't get used by this at all
58     'unused_credit_cancel'  => {'disabled' => 1},
59     'unused_credit_suspend' => {'disabled' => 1},
60     'unused_credit_change'  => {'disabled' => 1},
61   },
62   'validate' => sub {
63     my $options = shift;
64     return 'Rate must be specified'
65       unless $options->{'rate_field'} or $options->{'rate_flat'};
66     return 'Cannot specify both flat rate and rate field'
67       if $options->{'rate_field'} and $options->{'rate_flat'};
68     return '';
69   },
70   'fieldorder' => [ 'queueids', 'unit_field', 'rate_field', 'rate_flat', 'display_fields' ]
71 );
72
73 sub price_info {
74     my $self = shift;
75     my $str = $self->SUPER::price_info;
76     $str .= ' plus ' if $str;
77     $str .= 'charge from RT';
78 # takes way too long just to get a package label
79 #    FS::TicketSystem->init();
80 #    my %custom_fields = FS::TicketSystem->custom_fields();
81 #    my $rate = $self->option('rate_flat',1);
82 #    my $rate_field = $self->option('rate_field',1);
83 #    my $unit_field = $self->option('unit_field');
84 #    $str .= $rate
85 #            ? $money_char . sprintf("%.2",$rate)
86 #            : $custom_fields{$rate_field};
87 #    $str .= ' x ' . $custom_fields{$unit_field};
88     return $str;
89 }
90
91 sub calc_setup {
92   my($self, $cust_pkg ) = @_;
93   $self->option('setup_fee');
94 }
95
96 sub calc_recur {
97   my $self = shift;
98   my($cust_pkg, $sdate, $details, $param ) = @_;
99
100   my $charges = 0;
101
102   $charges += $self->calc_usage(@_);
103   $charges += ($cust_pkg->quantity || 1) * $self->calc_recur_Common(@_);
104
105   $charges;
106
107 }
108
109 sub can_discount { 0; }
110
111 sub calc_usage {
112   my $self = shift;
113   my($cust_pkg, $sdate, $details, $param ) = @_;
114
115   FS::TicketSystem->init();
116
117   my %queues = FS::TicketSystem->queues(undef,'SeeCustomField');
118
119   my @tickets;
120   foreach my $queueid (
121     split(', ',$self->option('queueids',1) || '')
122   ) {
123
124     die "Insufficient permission to invoice package"
125       unless exists $queues{$queueid};
126
127     # load all resolved tickets since pkg was ordered
128     # will subtract previous charges below
129     # only way to be sure we've caught everything
130     my $tickets = FS::TicketSystem->customer_tickets({
131       number   => $cust_pkg->custnum, 
132       limit    => 10000, # arbitrarily large
133       status   => 'resolved',
134       queueid  => $queueid,
135       resolved => $cust_pkg->order_date, # or setup? but this is mainly for installations,
136                                          # and workflow might resolve tickets before first bill...
137                                          # for now, expect pkg to be ordered before tickets get resolved,
138                                          # easy enough to make a pkg option to use setup/sdate instead
139     });
140     push @tickets, @$tickets;
141   };
142
143   my $rate = $self->option('rate_flat',1);
144   my $rate_field = $self->option('rate_field',1);
145   my $unit_field = $self->option('unit_field');
146   my @display_fields = split(', ',$self->option('display_fields',1) || '');
147
148   my %custom_fields = FS::TicketSystem->custom_fields();
149   my $rate_label = $rate
150                    ? ''
151                    : ' ' . $custom_fields{$rate_field};
152   my $unit_label = $custom_fields{$unit_field};
153
154   $rate_field = 'CF.{' . $rate_field . '}' if $rate_field;
155   $unit_field = 'CF.{' . $unit_field . '}';
156
157   my $charges = 0;
158   foreach my $ticket ( @tickets ) {
159     next unless $ticket->{$unit_field};
160     next unless $rate || $ticket->{$rate_field};
161     my $trate = $rate || $ticket->{$rate_field};
162     my $tunit = $ticket->{$unit_field};
163     my $subcharge = sprintf('%.2f', $trate * $tunit);
164     my $precharge = _previous_charges( $cust_pkg->pkgnum, $ticket->{'id'} );
165     $subcharge -= $precharge;
166
167     # if field values for previous charges increased,
168     # we can make additional charges here and now,
169     # but if field values were decreased, we just ignore--
170     # credits will have to be applied manually later, if that's what's intended
171     next if $subcharge <= 0;
172
173     my $rt_field_charge = new FS::rt_field_charge {
174       'pkgnum' => $cust_pkg->pkgnum,
175       'ticketid' => $ticket->{'id'},
176       'rate' => $trate,
177       'units' => $tunit,
178       'charge' => $subcharge,
179       '_date' => $$sdate,
180     };
181     my $error = $rt_field_charge->insert;
182     die "Error inserting rt_field_charge: $error" if $error;
183     push @$details, $money_char . sprintf('%.2f',$trate) . $rate_label . ' x ' . $tunit . ' ' . $unit_label;
184     push @$details, ' - ' . $money_char . sprintf('%.2f',$precharge) . ' previously charged' if $precharge;
185     foreach my $field (
186       sort { $ticket->{'_cf_sort_order'}{$a} <=> $ticket->{'_cf_sort_order'}{$b} } @display_fields
187     ) {
188       my $label = $custom_fields{$field};
189       my $value = $ticket->{'CF.{' . $field . '}'};
190       push @$details, $label . ': ' . $value if $value;
191     }
192     $charges += $subcharge;
193   }
194   return $charges;
195 }
196
197 sub _previous_charges {
198   my ($pkgnum, $ticketid) = @_;
199   my $prev = 0;
200   foreach my $rt_field_charge (
201     qsearch('rt_field_charge', { pkgnum => $pkgnum, ticketid => $ticketid })
202   ) {
203     $prev += $rt_field_charge->charge;
204   }
205   return $prev;
206 }
207
208 1;