enhance contacts: contact classes, 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::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   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
157
158   '';
159
160 }
161
162 =item delete
163
164 Delete this record from the database.
165
166 =cut
167
168 # the delete method can be inherited from FS::Record
169
170 sub delete {
171   my $self = shift;
172
173   local $SIG{HUP} = 'IGNORE';
174   local $SIG{INT} = 'IGNORE';
175   local $SIG{QUIT} = 'IGNORE';
176   local $SIG{TERM} = 'IGNORE';
177   local $SIG{TSTP} = 'IGNORE';
178   local $SIG{PIPE} = 'IGNORE';
179
180   my $oldAutoCommit = $FS::UID::AutoCommit;
181   local $FS::UID::AutoCommit = 0;
182   my $dbh = dbh;
183
184   foreach my $object ( $self->contact_phone, $self->contact_email ) {
185     my $error = $object->delete;
186     if ( $error ) {
187       $dbh->rollback if $oldAutoCommit;
188       return $error;
189     }
190   }
191
192   my $error = $self->SUPER::delete;
193   if ( $error ) {
194     $dbh->rollback if $oldAutoCommit;
195     return $error;
196   }
197
198   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
199   '';
200
201 }
202
203 =item replace OLD_RECORD
204
205 Replaces the OLD_RECORD with this one in the database.  If there is an error,
206 returns the error, otherwise returns false.
207
208 =cut
209
210 sub replace {
211   my $self = shift;
212
213   local $SIG{INT} = 'IGNORE';
214   local $SIG{QUIT} = 'IGNORE';
215   local $SIG{TERM} = 'IGNORE';
216   local $SIG{TSTP} = 'IGNORE';
217   local $SIG{PIPE} = 'IGNORE';
218
219   my $oldAutoCommit = $FS::UID::AutoCommit;
220   local $FS::UID::AutoCommit = 0;
221   my $dbh = dbh;
222
223   my $error = $self->SUPER::replace(@_);
224   if ( $error ) {
225     $dbh->rollback if $oldAutoCommit;
226     return $error;
227   }
228
229   foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) }
230                         keys %{ $self->hashref } ) {
231     $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
232     my $phonetypenum = $1;
233
234     my %cp = ( 'contactnum'   => $self->contactnum,
235                'phonetypenum' => $phonetypenum,
236              );
237     my $contact_phone = qsearchs('contact_phone', \%cp)
238                         || new FS::contact_phone   \%cp;
239
240     my %cpd = _parse_phonestring( $self->get($pf) );
241     $contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
242
243     my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
244
245     $error = $contact_phone->$method;
246     if ( $error ) {
247       $dbh->rollback if $oldAutoCommit;
248       return $error;
249     }
250   }
251
252   if ( defined($self->get('emailaddress')) ) {
253
254     #ineffecient but whatever, how many email addresses can there be?
255
256     foreach my $contact_email ( $self->contact_email ) {
257       my $error = $contact_email->delete;
258       if ( $error ) {
259         $dbh->rollback if $oldAutoCommit;
260         return $error;
261       }
262     }
263
264     foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
265  
266       my $contact_email = new FS::contact_email {
267         'contactnum'   => $self->contactnum,
268         'emailaddress' => $email,
269       };
270       $error = $contact_email->insert;
271       if ( $error ) {
272         $dbh->rollback if $oldAutoCommit;
273         return $error;
274       }
275
276     }
277
278   }
279
280   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
281
282   '';
283
284 }
285
286 #i probably belong in contact_phone.pm
287 sub _parse_phonestring {
288   my $value = shift;
289
290   my($countrycode, $extension) = ('1', '');
291
292   #countrycode
293   if ( $value =~ s/^\s*\+\s*(\d+)// ) {
294     $countrycode = $1;
295   } else {
296     $value =~ s/^\s*1//;
297   }
298   #extension
299   if ( $value =~ s/\s*(ext|x)\s*(\d+)\s*$//i ) {
300      $extension = $2;
301   }
302
303   ( 'countrycode' => $countrycode,
304     'phonenum'    => $value,
305     'extension'   => $extension,
306   );
307 }
308
309 =item check
310
311 Checks all fields to make sure this is a valid example.  If there is
312 an error, returns the error, otherwise returns false.  Called by the insert
313 and replace methods.
314
315 =cut
316
317 # the check method should currently be supplied - FS::Record contains some
318 # data checking routines
319
320 sub check {
321   my $self = shift;
322
323   my $error = 
324     $self->ut_numbern('contactnum')
325     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
326     || $self->ut_foreign_keyn('custnum',     'cust_main',     'custnum')
327     || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
328     || $self->ut_foreign_keyn('classnum',    'contact_class', 'classnum')
329     || $self->ut_textn('last')
330     || $self->ut_textn('first')
331     || $self->ut_textn('title')
332     || $self->ut_textn('comment')
333     || $self->ut_enum('disabled', [ '', 'Y' ])
334   ;
335   return $error if $error;
336
337   return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
338   return "Prospect and customer!"       if $self->prospectnum && $self->custnum;
339
340   return "One of first name, last name, or title must have a value"
341     if ! grep $self->$_(), qw( first last title);
342
343   $self->SUPER::check;
344 }
345
346 sub line {
347   my $self = shift;
348   my $data = $self->first. ' '. $self->last;
349   $data .= ', '. $self->title
350     if $self->title;
351   $data .= ' ('. $self->comment. ')'
352     if $self->comment;
353   $data;
354 }
355
356 sub cust_location {
357   my $self = shift;
358   return '' unless $self->locationnum;
359   qsearchs('cust_location', { 'locationnum' => $self->locationnum } );
360 }
361
362 sub contact_class {
363   my $self = shift;
364   return '' unless $self->classnum;
365   qsearchs('contact_class', { 'classnum' => $self->classnum } );
366 }
367
368 sub contact_classname {
369   my $self = shift;
370   my $contact_class = $self->contact_class or return '';
371   $contact_class->classname;
372 }
373
374 sub contact_phone {
375   my $self = shift;
376   qsearch('contact_phone', { 'contactnum' => $self->contactnum } );
377 }
378
379 sub contact_email {
380   my $self = shift;
381   qsearch('contact_email', { 'contactnum' => $self->contactnum } );
382 }
383
384 =back
385
386 =head1 BUGS
387
388 =head1 SEE ALSO
389
390 L<FS::Record>, schema.html from the base documentation.
391
392 =cut
393
394 1;
395