fix TeleAPI import (what kind of crack was Christopher smoking that he couldn't fix...
[freeside.git] / FS / FS / svc_pbx.pm
1 package FS::svc_pbx;
2 use base qw( FS::o2m_Common FS::device_Common FS::svc_External_Common );
3
4 use strict;
5 use Tie::IxHash;
6 use FS::Record qw( qsearch qsearchs dbh );
7 use FS::PagedSearch qw( psearch );
8 use FS::Conf;
9 use FS::cust_svc;
10 use FS::svc_phone;
11 use FS::svc_acct;
12
13 =head1 NAME
14
15 FS::svc_pbx - Object methods for svc_pbx records
16
17 =head1 SYNOPSIS
18
19   use FS::svc_pbx;
20
21   $record = new FS::svc_pbx \%hash;
22   $record = new FS::svc_pbx { 'column' => 'value' };
23
24   $error = $record->insert;
25
26   $error = $new_record->replace($old_record);
27
28   $error = $record->delete;
29
30   $error = $record->check;
31
32   $error = $record->suspend;
33
34   $error = $record->unsuspend;
35
36   $error = $record->cancel;
37
38 =head1 DESCRIPTION
39
40 An FS::svc_pbx object represents a PBX tenant.  FS::svc_pbx inherits from
41 FS::svc_Common.  The following fields are currently supported:
42
43 =over 4
44
45 =item svcnum
46
47 Primary key (assigned automatcially for new accounts)
48
49 =item id
50
51 (Unique?) number of external record
52
53 =item title
54
55 PBX name
56
57 =item max_extensions
58
59 Maximum number of extensions
60
61 =item max_simultaneous
62
63 Maximum number of simultaneous users
64
65 =item ip_addr
66
67 The IP address of this PBX, if that's relevant. This must be a valid IP 
68 address (or blank), but it's not checked for block assignment or uniqueness.
69
70 =back
71
72 =head1 METHODS
73
74 =over 4
75
76 =item new HASHREF
77
78 Creates a new PBX tenant.  To add the PBX tenant to the database, see
79 L<"insert">.
80
81 Note that this stores the hash reference, not a distinct copy of the hash it
82 points to.  You can ask the object for a copy with the I<hash> method.
83
84 =cut
85
86 sub table { 'svc_pbx'; }
87
88 sub table_info {
89
90   tie my %fields, 'Tie::IxHash',
91     'svcnum' => 'PBX',
92     'id'     => 'PBX/Tenant ID',
93     'uuid'   => 'External UUID',
94     'title'  => 'Name',
95     'max_extensions' => 'Maximum number of User Extensions',
96     'max_simultaneous' => 'Maximum number of simultaneous users',
97     'ip_addr' => 'IP address',
98   ;
99
100   {
101     'name' => 'PBX',
102     'name_plural' => 'PBXs',
103     'lcname_plural' => 'PBXs',
104     'longname_plural' => 'PBXs',
105     'sorts' => 'svcnum', # optional sort field (or arrayref of sort fields, main first)
106     'display_weight' => 70,
107     'cancel_weight'  => 90,
108     'fields' => \%fields,
109   };
110 }
111
112 =item search_sql STRING
113
114 Class method which returns an SQL fragment to search for the given string.
115
116 =cut
117
118 #XXX
119 #or something more complicated if necessary
120 #sub search_sql {
121 #  my($class, $string) = @_;
122 #  $class->search_sql_field('title', $string);
123 #}
124
125 =item label
126
127 Returns the title field for this PBX tenant.
128
129 =cut
130
131 sub label {
132   my $self = shift;
133   $self->title;
134 }
135
136 =item insert
137
138 Adds this record to the database.  If there is an error, returns the error,
139 otherwise returns false.
140
141 The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
142 defined.  An FS::cust_svc record will be created and inserted.
143
144 =item delete
145
146 Delete this record from the database.
147
148 =cut
149
150 sub delete {
151   my $self = shift;
152
153   local $SIG{HUP} = 'IGNORE';
154   local $SIG{INT} = 'IGNORE';
155   local $SIG{QUIT} = 'IGNORE';
156   local $SIG{TERM} = 'IGNORE';
157   local $SIG{TSTP} = 'IGNORE';
158   local $SIG{PIPE} = 'IGNORE';
159
160   my $oldAutoCommit = $FS::UID::AutoCommit;
161   local $FS::UID::AutoCommit = 0;
162   my $dbh = dbh;
163
164   foreach my $svc_phone (qsearch('svc_phone', { 'pbxsvc' => $self->svcnum } )) {
165     $svc_phone->pbxsvc('');
166     my $error = $svc_phone->replace;
167     if ( $error ) {
168       $dbh->rollback if $oldAutoCommit;
169       return $error;
170     }
171   }
172
173   foreach my $svc_acct  (qsearch('svc_acct',  { 'pbxsvc' => $self->svcnum } )) {
174     my $error = $svc_acct->delete;
175     if ( $error ) {
176       $dbh->rollback if $oldAutoCommit;
177       return $error;
178     }
179   }
180
181   my $error = $self->SUPER::delete;
182   if ( $error ) {
183     $dbh->rollback if $oldAutoCommit;
184     return $error;
185   }
186
187   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
188   '';
189 }
190
191
192 =item replace OLD_RECORD
193
194 Replaces the OLD_RECORD with this one in the database.  If there is an error,
195 returns the error, otherwise returns false.
196
197 =item suspend
198
199 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
200
201 =item unsuspend
202
203 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
204
205 =item cancel
206
207 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
208
209 =item check
210
211 Checks all fields to make sure this is a valid PBX tenant.  If there is
212 an error, returns the error, otherwise returns false.  Called by the insert
213 and repalce methods.
214
215 =cut
216
217 sub check {
218   my $self = shift;
219
220   my $x = $self->setfixed;
221   return $x unless ref($x);
222   my $part_svc = $x;
223  
224   return
225      $self->ut_ipn('ip_addr')
226   || $self->SUPER::check;
227 }
228
229 sub _check_duplicate {
230   my $self = shift;
231
232   my $conf = new FS::Conf;
233   
234   $self->lock_table;
235
236   foreach my $field ('title', 'id') {
237     my $global_unique = $conf->config("global_unique-pbx_$field");
238     # can be 'disabled', 'enabled', or empty.
239     # if empty, check per exports; if not empty or disabled, check 
240     # globally.
241     next if $global_unique eq 'disabled';
242     my @dup = $self->find_duplicates(
243       ($global_unique ? 'global' : 'export') , $field
244     );
245     next if !@dup;
246     return "duplicate $field '".$self->getfield($field).
247            "': conflicts with svcnum ".$dup[0]->svcnum;
248   }
249   return '';
250 }
251
252 =item psearch_cdrs OPTIONS
253
254 Returns a paged search (L<FS::PagedSearch>) for Call Detail Records 
255 associated with this service.  By default, "associated with" means that 
256 the "charged_party" field of the CDR matches the "title" field of the 
257 service.  To access the CDRs themselves, call "->fetch" on the resulting
258 object.
259
260 =over 2
261
262 Accepts the following options:
263
264 =item for_update => 1: SELECT the CDRs "FOR UPDATE".
265
266 =item status => "" (or "done"): Return only CDRs with that processing status.
267
268 =item inbound => 1: No-op for svc_pbx CDR processing.
269
270 =item default_prefix => "XXX": Also accept the phone number of the service prepended 
271 with the chosen prefix.
272
273 =item disable_src => 1: No-op for svc_pbx CDR processing.
274
275 =item by_svcnum => 1: Select CDRs where the svcnum field matches, instead of 
276 title/charged_party.  Normally this field is set after processing.
277
278 =item by_ip_addr => 'src' or 'dst': Select CDRs where the src_ip_addr or 
279 dst_ip_addr field matches title.  In this case, some special logic is applied
280 to allow title to indicate a range of IP addresses.
281
282 =item begin, end: Start and end of date range, as unix timestamp.
283
284 =item cdrtypenum: Only return CDRs with this type.
285
286 =item calltypenum: Only return CDRs with this call type.
287
288 =back
289
290 =cut
291
292 sub psearch_cdrs {
293   my($self, %options) = @_;
294   my %hash = ();
295   my @where = ();
296
297   my @fields = ( 'charged_party' );
298   $hash{'freesidestatus'} = $options{'status'}
299     if exists($options{'status'});
300
301   if ($options{'cdrtypenum'}) {
302     $hash{'cdrtypenum'} = $options{'cdrtypenum'};
303   }
304   if ($options{'calltypenum'}) {
305     $hash{'calltypenum'} = $options{'calltypenum'};
306   }
307
308   my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
309
310   if ( $options{'by_svcnum'} ) {
311     $hash{'svcnum'} = $self->svcnum;
312   }
313   elsif ( $options{'by_ip_addr'} =~ /^src|dst$/) {
314     my $field = 'cdr.'.$options{'by_ip_addr'}.'_ip_addr';
315     push @where, FS::cdr->ip_addr_sql($field, $self->title);
316   }
317   else {
318     #matching by title
319     my $title = $self->title;
320
321     my $prefix = $options{'default_prefix'};
322
323     my @orwhere =  map " $_ = '$title'        ", @fields;
324     push @orwhere, map " $_ = '$prefix$title' ", @fields
325       if length($prefix);
326     if ( $prefix =~ /^\+(\d+)$/ ) {
327       push @orwhere, map " $_ = '$1$title' ", @fields
328     }
329
330     push @where, ' ( '. join(' OR ', @orwhere ). ' ) ';
331   }
332
333   if ( $options{'begin'} ) {
334     push @where, 'startdate >= '. $options{'begin'};
335   }
336   if ( $options{'end'} ) {
337     push @where, 'startdate < '.  $options{'end'};
338   }
339
340   my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where )
341     if @where;
342
343   psearch( {
344       'table'      => 'cdr',
345       'hashref'    => \%hash,
346       'extra_sql'  => $extra_sql,
347       'order_by'   => "ORDER BY startdate $for_update",
348   } );
349 }
350
351 =item get_cdrs (DEPRECATED)
352
353 Like psearch_cdrs, but returns all the L<FS::cdr> objects at once, in a 
354 single list.  Arguments are the same as for psearch_cdrs.  This can take
355 an unreasonably large amount of memory and is best avoided.
356
357 =cut
358
359 sub get_cdrs {
360   my $self = shift;
361   my $psearch = $self->psearch_cdrs($_);
362   qsearch ( $psearch->{query} )
363 }
364
365 =item sum_cdrs
366
367 Takes the same options as psearch_cdrs, but returns a single row containing
368 "count" (the number of CDRs) and the sums of the following fields: duration,
369 billsec, rated_price, rated_seconds, rated_minutes.
370
371 Note that if any calls are not rated, their rated_* fields will be null.
372 If you want to use those fields, pass the 'status' option to limit to 
373 calls that have been rated.  This is intentional; please don't "fix" it.
374
375 =cut
376
377 sub sum_cdrs {
378   my $self = shift;
379   my $psearch = $self->psearch_cdrs(@_);
380   $psearch->{query}->{'select'} = join(',',
381     'COUNT(*) AS count',
382     map { "SUM($_) AS $_" }
383       qw(duration billsec rated_price rated_seconds rated_minutes)
384   );
385   # hack
386   $psearch->{query}->{'extra_sql'} =~ s/ ORDER BY.*$//;
387   qsearchs ( $psearch->{query} );
388 }
389
390 sub _upgrade_data {
391
392   require FS::Misc::FixIPFormat;
393   FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
394       'svc_pbx', 'svcnum', 'ip_addr',
395   );
396
397   '';
398
399 }
400
401 =back
402
403 =head1 BUGS
404
405 =head1 SEE ALSO
406
407 L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
408 L<FS::cust_pkg>, schema.html from the base documentation.
409
410 =cut
411
412 1;