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