Merge branch 'master' of git.freeside.biz:/home/git/freeside
[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 =cut
145
146 sub insert {
147   my $self = shift;
148   my $error;
149
150   $error = $self->SUPER::insert;
151   return $error if $error;
152
153   '';
154 }
155
156 =item delete
157
158 Delete this record from the database.
159
160 =cut
161
162 sub delete {
163   my $self = shift;
164
165   local $SIG{HUP} = 'IGNORE';
166   local $SIG{INT} = 'IGNORE';
167   local $SIG{QUIT} = 'IGNORE';
168   local $SIG{TERM} = 'IGNORE';
169   local $SIG{TSTP} = 'IGNORE';
170   local $SIG{PIPE} = 'IGNORE';
171
172   my $oldAutoCommit = $FS::UID::AutoCommit;
173   local $FS::UID::AutoCommit = 0;
174   my $dbh = dbh;
175
176   foreach my $svc_phone (qsearch('svc_phone', { 'pbxsvc' => $self->svcnum } )) {
177     $svc_phone->pbxsvc('');
178     my $error = $svc_phone->replace;
179     if ( $error ) {
180       $dbh->rollback if $oldAutoCommit;
181       return $error;
182     }
183   }
184
185   foreach my $svc_acct  (qsearch('svc_acct',  { 'pbxsvc' => $self->svcnum } )) {
186     my $error = $svc_acct->delete;
187     if ( $error ) {
188       $dbh->rollback if $oldAutoCommit;
189       return $error;
190     }
191   }
192
193   my $error = $self->SUPER::delete;
194   if ( $error ) {
195     $dbh->rollback if $oldAutoCommit;
196     return $error;
197   }
198
199   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
200   '';
201 }
202
203
204 =item replace OLD_RECORD
205
206 Replaces the OLD_RECORD with this one in the database.  If there is an error,
207 returns the error, otherwise returns false.
208
209 =cut
210
211 #sub replace {
212 #  my ( $new, $old ) = ( shift, shift );
213 #  my $error;
214 #
215 #  $error = $new->SUPER::replace($old);
216 #  return $error if $error;
217 #
218 #  '';
219 #}
220
221 =item suspend
222
223 Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
224
225 =item unsuspend
226
227 Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
228
229 =item cancel
230
231 Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
232
233 =item check
234
235 Checks all fields to make sure this is a valid PBX tenant.  If there is
236 an error, returns the error, otherwise returns false.  Called by the insert
237 and repalce methods.
238
239 =cut
240
241 sub check {
242   my $self = shift;
243
244   my $x = $self->setfixed;
245   return $x unless ref($x);
246   my $part_svc = $x;
247  
248   return
249      $self->ut_ipn('ip_addr')
250   || $self->SUPER::check;
251 }
252
253 sub _check_duplicate {
254   my $self = shift;
255
256   my $conf = new FS::Conf;
257   
258   $self->lock_table;
259
260   foreach my $field ('title', 'id') {
261     my $global_unique = $conf->config("global_unique-pbx_$field");
262     # can be 'disabled', 'enabled', or empty.
263     # if empty, check per exports; if not empty or disabled, check 
264     # globally.
265     next if $global_unique eq 'disabled';
266     my @dup = $self->find_duplicates(
267       ($global_unique ? 'global' : 'export') , $field
268     );
269     next if !@dup;
270     return "duplicate $field '".$self->getfield($field).
271            "': conflicts with svcnum ".$dup[0]->svcnum;
272   }
273   return '';
274 }
275
276 =item psearch_cdrs OPTIONS
277
278 Returns a paged search (L<FS::PagedSearch>) for Call Detail Records 
279 associated with this service.  By default, "associated with" means that 
280 the "charged_party" field of the CDR matches the "title" field of the 
281 service.  To access the CDRs themselves, call "->fetch" on the resulting
282 object.
283
284 =over 2
285
286 Accepts the following options:
287
288 =item for_update => 1: SELECT the CDRs "FOR UPDATE".
289
290 =item status => "" (or "done"): Return only CDRs with that processing status.
291
292 =item inbound => 1: No-op for svc_pbx CDR processing.
293
294 =item default_prefix => "XXX": Also accept the phone number of the service prepended 
295 with the chosen prefix.
296
297 =item disable_src => 1: No-op for svc_pbx CDR processing.
298
299 =item by_svcnum => 1: Select CDRs where the svcnum field matches, instead of 
300 title/charged_party.  Normally this field is set after processing.
301
302 =item by_ip_addr => 'src' or 'dst': Select CDRs where the src_ip_addr or 
303 dst_ip_addr field matches title.  In this case, some special logic is applied
304 to allow title to indicate a range of IP addresses.
305
306 =item begin, end: Start and end of date range, as unix timestamp.
307
308 =item cdrtypenum: Only return CDRs with this type.
309
310 =item calltypenum: Only return CDRs with this call type.
311
312 =back
313
314 =cut
315
316 sub psearch_cdrs {
317   my($self, %options) = @_;
318   my %hash = ();
319   my @where = ();
320
321   my @fields = ( 'charged_party' );
322   $hash{'freesidestatus'} = $options{'status'}
323     if exists($options{'status'});
324
325   if ($options{'cdrtypenum'}) {
326     $hash{'cdrtypenum'} = $options{'cdrtypenum'};
327   }
328   if ($options{'calltypenum'}) {
329     $hash{'calltypenum'} = $options{'calltypenum'};
330   }
331
332   my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
333
334   if ( $options{'by_svcnum'} ) {
335     $hash{'svcnum'} = $self->svcnum;
336   }
337   elsif ( $options{'by_ip_addr'} =~ /^src|dst$/) {
338     my $field = 'cdr.'.$options{'by_ip_addr'}.'_ip_addr';
339     push @where, FS::cdr->ip_addr_sql($field, $self->title);
340   }
341   else {
342     #matching by title
343     my $title = $self->title;
344
345     my $prefix = $options{'default_prefix'};
346
347     my @orwhere =  map " $_ = '$title'        ", @fields;
348     push @orwhere, map " $_ = '$prefix$title' ", @fields
349       if length($prefix);
350     if ( $prefix =~ /^\+(\d+)$/ ) {
351       push @orwhere, map " $_ = '$1$title' ", @fields
352     }
353
354     push @where, ' ( '. join(' OR ', @orwhere ). ' ) ';
355   }
356
357   if ( $options{'begin'} ) {
358     push @where, 'startdate >= '. $options{'begin'};
359   }
360   if ( $options{'end'} ) {
361     push @where, 'startdate < '.  $options{'end'};
362   }
363
364   my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where )
365     if @where;
366
367   psearch( {
368       'table'      => 'cdr',
369       'hashref'    => \%hash,
370       'extra_sql'  => $extra_sql,
371       'order_by'   => "ORDER BY startdate $for_update",
372   } );
373 }
374
375 =item get_cdrs (DEPRECATED)
376
377 Like psearch_cdrs, but returns all the L<FS::cdr> objects at once, in a 
378 single list.  Arguments are the same as for psearch_cdrs.  This can take
379 an unreasonably large amount of memory and is best avoided.
380
381 =cut
382
383 sub get_cdrs {
384   my $self = shift;
385   my $psearch = $self->psearch_cdrs($_);
386   qsearch ( $psearch->{query} )
387 }
388
389 =item sum_cdrs
390
391 Takes the same options as psearch_cdrs, but returns a single row containing
392 "count" (the number of CDRs) and the sums of the following fields: duration,
393 billsec, rated_price, rated_seconds, rated_minutes.
394
395 Note that if any calls are not rated, their rated_* fields will be null.
396 If you want to use those fields, pass the 'status' option to limit to 
397 calls that have been rated.  This is intentional; please don't "fix" it.
398
399 =cut
400
401 sub sum_cdrs {
402   my $self = shift;
403   my $psearch = $self->psearch_cdrs(@_);
404   $psearch->{query}->{'select'} = join(',',
405     'COUNT(*) AS count',
406     map { "SUM($_) AS $_" }
407       qw(duration billsec rated_price rated_seconds rated_minutes)
408   );
409   # hack
410   $psearch->{query}->{'extra_sql'} =~ s/ ORDER BY.*$//;
411   qsearchs ( $psearch->{query} );
412 }
413
414 =back
415
416 =head1 BUGS
417
418 =head1 SEE ALSO
419
420 L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
421 L<FS::cust_pkg>, schema.html from the base documentation.
422
423 =cut
424
425 1;
426