Merge remote-tracking branch 'upstream/master'
[freeside.git] / FS / FS / contact.pm
1 package FS::contact;
2 use base qw( FS::Record );
3
4 use strict;
5 use FS::Record qw( qsearch qsearchs dbh );
6 use FS::prospect_main;
7 use FS::cust_main;
8 use FS::contact_class;
9 use FS::cust_location;
10 use FS::contact_phone;
11 use FS::contact_email;
12
13 =head1 NAME
14
15 FS::contact - Object methods for contact records
16
17 =head1 SYNOPSIS
18
19   use FS::contact;
20
21   $record = new FS::contact \%hash;
22   $record = new FS::contact { '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 =head1 DESCRIPTION
33
34 An FS::contact object represents an example.  FS::contact inherits from
35 FS::Record.  The following fields are currently supported:
36
37 =over 4
38
39 =item contactnum
40
41 primary key
42
43 =item prospectnum
44
45 prospectnum
46
47 =item custnum
48
49 custnum
50
51 =item locationnum
52
53 locationnum
54
55 =item last
56
57 last
58
59 =item first
60
61 first
62
63 =item title
64
65 title
66
67 =item comment
68
69 comment
70
71 =item disabled
72
73 disabled
74
75
76 =back
77
78 =head1 METHODS
79
80 =over 4
81
82 =item new HASHREF
83
84 Creates a new example.  To add the example to the database, see L<"insert">.
85
86 Note that this stores the hash reference, not a distinct copy of the hash it
87 points to.  You can ask the object for a copy with the I<hash> method.
88
89 =cut
90
91 # the new method can be inherited from FS::Record, if a table method is defined
92
93 sub table { 'contact'; }
94
95 =item insert
96
97 Adds this record to the database.  If there is an error, returns the error,
98 otherwise returns false.
99
100 =cut
101
102 sub insert {
103   my $self = shift;
104
105   local $SIG{INT} = 'IGNORE';
106   local $SIG{QUIT} = 'IGNORE';
107   local $SIG{TERM} = 'IGNORE';
108   local $SIG{TSTP} = 'IGNORE';
109   local $SIG{PIPE} = 'IGNORE';
110
111   my $oldAutoCommit = $FS::UID::AutoCommit;
112   local $FS::UID::AutoCommit = 0;
113   my $dbh = dbh;
114
115   my $error = $self->SUPER::insert;
116   if ( $error ) {
117     $dbh->rollback if $oldAutoCommit;
118     return $error;
119   }
120
121   foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
122                         keys %{ $self->hashref } ) {
123     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
124     my $phonetypenum = $1;
125
126     my $contact_phone = new FS::contact_phone {
127       'contactnum' => $self->contactnum,
128       'phonetypenum' => $phonetypenum,
129       _parse_phonestring( $self->get($pf) ),
130     };
131     $error = $contact_phone->insert;
132     if ( $error ) {
133       $dbh->rollback if $oldAutoCommit;
134       return $error;
135     }
136   }
137
138   if ( $self->get('emailaddress') =~ /\S/ ) {
139
140     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
141  
142       my $contact_email = new FS::contact_email {
143         'contactnum'   => $self->contactnum,
144         'emailaddress' => $email,
145       };
146       $error = $contact_email->insert;
147       if ( $error ) {
148         $dbh->rollback if $oldAutoCommit;
149         return $error;
150       }
151
152     }
153
154   }
155
156   #unless ( $import || $skip_fuzzyfiles ) {
157     #warn "  queueing fuzzyfiles update\n"
158     #  if $DEBUG > 1;
159     $error = $self->queue_fuzzyfiles_update;
160     if ( $error ) {
161       $dbh->rollback if $oldAutoCommit;
162       return "updating fuzzy search cache: $error";
163     }
164   #}
165
166   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
167
168   '';
169
170 }
171
172 =item delete
173
174 Delete this record from the database.
175
176 =cut
177
178 # the delete method can be inherited from FS::Record
179
180 sub delete {
181   my $self = shift;
182
183   local $SIG{HUP} = 'IGNORE';
184   local $SIG{INT} = 'IGNORE';
185   local $SIG{QUIT} = 'IGNORE';
186   local $SIG{TERM} = 'IGNORE';
187   local $SIG{TSTP} = 'IGNORE';
188   local $SIG{PIPE} = 'IGNORE';
189
190   my $oldAutoCommit = $FS::UID::AutoCommit;
191   local $FS::UID::AutoCommit = 0;
192   my $dbh = dbh;
193
194   foreach my $object ( $self->contact_phone, $self->contact_email ) {
195     my $error = $object->delete;
196     if ( $error ) {
197       $dbh->rollback if $oldAutoCommit;
198       return $error;
199     }
200   }
201
202   my $error = $self->SUPER::delete;
203   if ( $error ) {
204     $dbh->rollback if $oldAutoCommit;
205     return $error;
206   }
207
208   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
209   '';
210
211 }
212
213 =item replace OLD_RECORD
214
215 Replaces the OLD_RECORD with this one in the database.  If there is an error,
216 returns the error, otherwise returns false.
217
218 =cut
219
220 sub replace {
221   my $self = shift;
222
223   local $SIG{INT} = 'IGNORE';
224   local $SIG{QUIT} = 'IGNORE';
225   local $SIG{TERM} = 'IGNORE';
226   local $SIG{TSTP} = 'IGNORE';
227   local $SIG{PIPE} = 'IGNORE';
228
229   my $oldAutoCommit = $FS::UID::AutoCommit;
230   local $FS::UID::AutoCommit = 0;
231   my $dbh = dbh;
232
233   my $error = $self->SUPER::replace(@_);
234   if ( $error ) {
235     $dbh->rollback if $oldAutoCommit;
236     return $error;
237   }
238
239   foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) }
240                         keys %{ $self->hashref } ) {
241     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
242     my $phonetypenum = $1;
243
244     my %cp = ( 'contactnum'   => $self->contactnum,
245                'phonetypenum' => $phonetypenum,
246              );
247     my $contact_phone = qsearchs('contact_phone', \%cp)
248                         || new FS::contact_phone   \%cp;
249
250     my %cpd = _parse_phonestring( $self->get($pf) );
251     $contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
252
253     my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
254
255     $error = $contact_phone->$method;
256     if ( $error ) {
257       $dbh->rollback if $oldAutoCommit;
258       return $error;
259     }
260   }
261
262   if ( defined($self->get('emailaddress')) ) {
263
264     #ineffecient but whatever, how many email addresses can there be?
265
266     foreach my $contact_email ( $self->contact_email ) {
267       my $error = $contact_email->delete;
268       if ( $error ) {
269         $dbh->rollback if $oldAutoCommit;
270         return $error;
271       }
272     }
273
274     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
275  
276       my $contact_email = new FS::contact_email {
277         'contactnum'   => $self->contactnum,
278         'emailaddress' => $email,
279       };
280       $error = $contact_email->insert;
281       if ( $error ) {
282         $dbh->rollback if $oldAutoCommit;
283         return $error;
284       }
285
286     }
287
288   }
289
290   #unless ( $import || $skip_fuzzyfiles ) {
291     #warn "  queueing fuzzyfiles update\n"
292     #  if $DEBUG > 1;
293     $error = $self->queue_fuzzyfiles_update;
294     if ( $error ) {
295       $dbh->rollback if $oldAutoCommit;
296       return "updating fuzzy search cache: $error";
297     }
298   #}
299
300   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
301
302   '';
303
304 }
305
306 #i probably belong in contact_phone.pm
307 sub _parse_phonestring {
308   my $value = shift;
309
310   my($countrycode, $extension) = ('1', '');
311
312   #countrycode
313   if ( $value =~ s/^\s*\+\s*(\d+)// ) {
314     $countrycode = $1;
315   } else {
316     $value =~ s/^\s*1//;
317   }
318   #extension
319   if ( $value =~ s/\s*(ext|x)\s*(\d+)\s*$//i ) {
320      $extension = $2;
321   }
322
323   ( 'countrycode' => $countrycode,
324     'phonenum'    => $value,
325     'extension'   => $extension,
326   );
327 }
328
329 =item queue_fuzzyfiles_update
330
331 Used by insert & replace to update the fuzzy search cache
332
333 =cut
334
335 use FS::cust_main::Search;
336 sub queue_fuzzyfiles_update {
337   my $self = shift;
338
339   local $SIG{HUP} = 'IGNORE';
340   local $SIG{INT} = 'IGNORE';
341   local $SIG{QUIT} = 'IGNORE';
342   local $SIG{TERM} = 'IGNORE';
343   local $SIG{TSTP} = 'IGNORE';
344   local $SIG{PIPE} = 'IGNORE';
345
346   my $oldAutoCommit = $FS::UID::AutoCommit;
347   local $FS::UID::AutoCommit = 0;
348   my $dbh = dbh;
349
350   foreach my $field ( 'first', 'last' ) {
351     my $queue = new FS::queue { 
352       'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
353     };
354     my @args = "contact.$field", $self->get($field);
355     my $error = $queue->insert( @args );
356     if ( $error ) {
357       $dbh->rollback if $oldAutoCommit;
358       return "queueing job (transaction rolled back): $error";
359     }
360   }
361
362   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
363   '';
364
365 }
366
367 =item check
368
369 Checks all fields to make sure this is a valid example.  If there is
370 an error, returns the error, otherwise returns false.  Called by the insert
371 and replace methods.
372
373 =cut
374
375 # the check method should currently be supplied - FS::Record contains some
376 # data checking routines
377
378 sub check {
379   my $self = shift;
380
381   my $error = 
382     $self->ut_numbern('contactnum')
383     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
384     || $self->ut_foreign_keyn('custnum',     'cust_main',     'custnum')
385     || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
386     || $self->ut_foreign_keyn('classnum',    'contact_class', 'classnum')
387     || $self->ut_namen('last')
388     || $self->ut_namen('first')
389     || $self->ut_textn('title')
390     || $self->ut_textn('comment')
391     || $self->ut_enum('disabled', [ '', 'Y' ])
392   ;
393   return $error if $error;
394
395   return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
396   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
397
398   return "One of first name, last name, or title must have a value"
399     if ! grep $self->$_(), qw( first last title);
400
401   $self->SUPER::check;
402 }
403
404 sub line {
405   my $self = shift;
406   my $data = $self->first. ' '. $self->last;
407   $data .= ', '. $self->title
408     if $self->title;
409   $data .= ' ('. $self->comment. ')'
410     if $self->comment;
411   $data;
412 }
413
414 sub cust_location {
415   my $self = shift;
416   return '' unless $self->locationnum;
417   qsearchs('cust_location', { 'locationnum' => $self->locationnum } );
418 }
419
420 sub contact_class {
421   my $self = shift;
422   return '' unless $self->classnum;
423   qsearchs('contact_class', { 'classnum' => $self->classnum } );
424 }
425
426 sub contact_classname {
427   my $self = shift;
428   my $contact_class = $self->contact_class or return '';
429   $contact_class->classname;
430 }
431
432 sub contact_phone {
433   my $self = shift;
434   qsearch('contact_phone', { 'contactnum' => $self->contactnum } );
435 }
436
437 sub contact_email {
438   my $self = shift;
439   qsearch('contact_email', { 'contactnum' => $self->contactnum } );
440 }
441
442 sub cust_main {
443   my $self = shift;
444   qsearchs('cust_main', { 'custnum' => $self->custnum  } );
445 }
446
447 =back
448
449 =head1 BUGS
450
451 =head1 SEE ALSO
452
453 L<FS::Record>, schema.html from the base documentation.
454
455 =cut
456
457 1;
458