2 use base qw( FS::Password_Mixin
6 use vars qw( $skip_fuzzyfiles );
8 use Scalar::Util qw( blessed );
9 use FS::Record qw( qsearch qsearchs dbh );
11 use FS::contact_phone;
12 use FS::contact_email;
13 use FS::contact::Import;
15 use FS::phone_type; #for cgi_contact_fields
17 use FS::prospect_contact;
23 FS::contact - Object methods for contact records
29 $record = new FS::contact \%hash;
30 $record = new FS::contact { 'column' => 'value' };
32 $error = $record->insert;
34 $error = $new_record->replace($old_record);
36 $error = $record->delete;
38 $error = $record->check;
42 An FS::contact object represents an specific contact person for a prospect or
43 customer. FS::contact inherits from FS::Record. The following fields are
80 =item selfservice_access
86 =item _password_encoding
102 Creates a new contact. To add the contact to the database, see L<"insert">.
104 Note that this stores the hash reference, not a distinct copy of the hash it
105 points to. You can ask the object for a copy with the I<hash> method.
109 sub table { 'contact'; }
113 Adds this record to the database. If there is an error, returns the error,
114 otherwise returns false.
116 If the object has an C<emailaddress> field, L<FS::contact_email> records
117 will be created for each (comma-separated) email address in that field. If
118 any of these coincide with an existing email address, this contact will be
119 merged with the contact with that address.
121 Then, if the object has any fields named C<phonetypenumN> an
122 L<FS::contact_phone> record will be created for each of them. Those fields
123 should contain phone numbers of the appropriate types (where N is the key of
124 an L<FS::phone_type> record identifying the type of number: daytime, night,
127 After inserting the record, if the object has a 'custnum' or 'prospectnum'
128 field, an L<FS::cust_contact> or L<FS::prospect_contact> record will be
129 created to link the contact to the customer. The following fields will also
130 be included in that record, if they are set on the object:
141 local $SIG{INT} = 'IGNORE';
142 local $SIG{QUIT} = 'IGNORE';
143 local $SIG{TERM} = 'IGNORE';
144 local $SIG{TSTP} = 'IGNORE';
145 local $SIG{PIPE} = 'IGNORE';
147 my $oldAutoCommit = $FS::UID::AutoCommit;
148 local $FS::UID::AutoCommit = 0;
151 #save off and blank values that move to cust_contact / prospect_contact now
152 my $prospectnum = $self->prospectnum;
153 $self->prospectnum('');
154 my $custnum = $self->custnum;
158 for (qw( classnum comment selfservice_access invoice_dest message_dest)) {
159 $link_hash{$_} = $self->get($_);
164 ## check for an existing contact with this email address other than current customer
165 ## if found, just add that contact to cust_contact with link_hash credentials
166 ## as email can not be tied to two contacts.
168 my $existing_contact = '';
169 my @contact_emails = ();
170 my %contact_nums = ();
172 if ( $self->get('emailaddress') =~ /\S/ ) {
174 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
176 my $contact_email = qsearchs('contact_email', { emailaddress=>$email } );
177 unless ($contact_email) { push @contact_emails, $email; next; }
179 my $contact = $contact_email->contact;
180 if ($contact->contactnum eq $self->contactnum) {
181 push @contact_emails, $email;
184 $contact_nums{$contact->contactnum} = '1';
189 my $emails = join(' , ', @contact_emails);
190 $self->emailaddress($emails);
192 $no_new_contact = '1' unless $self->emailaddress;
197 $error = $self->SUPER::insert unless $no_new_contact;
200 $dbh->rollback if $oldAutoCommit;
204 $contact_nums{$self->contactnum} = '1' if $self->contactnum;
206 my $cust_contact = '';
207 # if $self->custnum was set, then the customer-specific properties
208 # (custnum, classnum, invoice_dest, selfservice_access, comment) are in
209 # pseudo-fields, and are now in %link_hash. otherwise, ignore all those
212 foreach my $contactnum (keys %contact_nums) {
213 my %hash = ( 'contactnum' => $contactnum,
214 'custnum' => $custnum,
216 $cust_contact = qsearchs('cust_contact', \%hash )
217 || new FS::cust_contact { %hash, %link_hash };
218 my $error = $cust_contact->custcontactnum ? $cust_contact->replace
219 : $cust_contact->insert;
221 $dbh->rollback if $oldAutoCommit;
227 if ( $prospectnum && !$no_new_contact) {
228 my %hash = ( 'contactnum' => $self->contactnum,
229 'prospectnum' => $prospectnum,
231 my $prospect_contact = qsearchs('prospect_contact', \%hash )
232 || new FS::prospect_contact { %hash, %link_hash };
234 $prospect_contact->prospectcontactnum ? $prospect_contact->replace
235 : $prospect_contact->insert;
237 $dbh->rollback if $oldAutoCommit;
242 unless ($no_new_contact) {
243 foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
244 keys %{ $self->hashref } ) {
245 $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
246 my $phonetypenum = $1;
248 my %hash = ( 'contactnum' => $self->contactnum,
249 'phonetypenum' => $phonetypenum,
252 qsearchs('contact_phone', \%hash)
253 || new FS::contact_phone { %hash, _parse_phonestring($self->get($pf)) };
254 my $error = $contact_phone->contactphonenum ? $contact_phone->replace
255 : $contact_phone->insert;
257 $dbh->rollback if $oldAutoCommit;
263 if ( $self->get('emailaddress') =~ /\S/ ) {
265 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
267 'contactnum' => $self->contactnum,
268 'emailaddress' => $email,
270 unless ( qsearchs('contact_email', \%hash) ) {
271 my $contact_email = new FS::contact_email \%hash;
272 my $error = $contact_email->insert;
274 $dbh->rollback if $oldAutoCommit;
282 unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
283 #warn " queueing fuzzyfiles update\n"
285 my $error = $self->queue_fuzzyfiles_update;
287 $dbh->rollback if $oldAutoCommit;
288 return "updating fuzzy search cache: $error";
292 if ( $link_hash{'selfservice_access'} eq 'R'
293 or ( $link_hash{'selfservice_access'}
295 && ! length($self->_password)
299 my $error = $self->send_reset_email( queue=>1 );
301 $dbh->rollback if $oldAutoCommit;
306 if ( $self->get('password') ) {
307 my $error = $self->is_password_allowed($self->get('password'))
308 || $self->change_password($self->get('password'));
310 $dbh->rollback if $oldAutoCommit;
315 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
323 Delete this record from the database.
330 local $SIG{HUP} = 'IGNORE';
331 local $SIG{INT} = 'IGNORE';
332 local $SIG{QUIT} = 'IGNORE';
333 local $SIG{TERM} = 'IGNORE';
334 local $SIG{TSTP} = 'IGNORE';
335 local $SIG{PIPE} = 'IGNORE';
337 my $oldAutoCommit = $FS::UID::AutoCommit;
338 local $FS::UID::AutoCommit = 0;
341 #got a prospetnum or custnum? delete the prospect_contact or cust_contact link
343 if ( $self->prospectnum ) {
344 my $prospect_contact = qsearchs('prospect_contact', {
345 'contactnum' => $self->contactnum,
346 'prospectnum' => $self->prospectnum,
348 my $error = $prospect_contact->delete;
350 $dbh->rollback if $oldAutoCommit;
355 # if $self->custnum was set, then we're removing the contact from this
357 if ( $self->custnum ) {
358 my $cust_contact = qsearchs('cust_contact', {
359 'contactnum' => $self->contactnum,
360 'custnum' => $self->custnum,
362 my $error = $cust_contact->delete;
364 $dbh->rollback if $oldAutoCommit;
369 # then, proceed with deletion only if the contact isn't attached to any other
370 # prospects or customers
372 #inefficient, but how many prospects/customers can a single contact be
373 # attached too? (and is removing them from one a common operation?)
374 if ( $self->prospect_contact || $self->cust_contact ) {
375 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
379 #proceed with deletion
381 foreach my $cust_pkg ( $self->cust_pkg ) {
382 $cust_pkg->contactnum('');
383 my $error = $cust_pkg->replace;
385 $dbh->rollback if $oldAutoCommit;
390 foreach my $object ( $self->contact_phone, $self->contact_email ) {
391 my $error = $object->delete;
393 $dbh->rollback if $oldAutoCommit;
398 my $error = $self->delete_password_history
399 || $self->SUPER::delete;
401 $dbh->rollback if $oldAutoCommit;
405 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
410 =item replace OLD_RECORD
412 Replaces the OLD_RECORD with this one in the database. If there is an error,
413 returns the error, otherwise returns false.
420 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
422 : $self->replace_old;
424 $self->$_( $self->$_ || $old->$_ ) for qw( _password _password_encoding );
426 local $SIG{INT} = 'IGNORE';
427 local $SIG{QUIT} = 'IGNORE';
428 local $SIG{TERM} = 'IGNORE';
429 local $SIG{TSTP} = 'IGNORE';
430 local $SIG{PIPE} = 'IGNORE';
432 my $oldAutoCommit = $FS::UID::AutoCommit;
433 local $FS::UID::AutoCommit = 0;
436 #save off and blank values that move to cust_contact / prospect_contact now
437 my $prospectnum = $self->prospectnum;
438 $self->prospectnum('');
439 my $custnum = $self->custnum;
440 $self->custnum(''); $old->custnum(''); # remove because now stored cust_contact
443 for (qw( classnum comment selfservice_access invoice_dest message_dest )) {
444 $link_hash{$_} = $self->get($_);
445 $old->$_(''); ##remove values from old record, causes problem with history
449 ## check for an existing contact with this email address other than current customer
450 ## if found, just add that contact to cust_contact with link_hash credentials
451 ## as email can not be tied to two contacts.
452 my @contact_emails = ();
453 my %contact_nums = ();
454 $contact_nums{$self->contactnum} = '1' if $self->contactnum;
455 if ( $self->get('emailaddress') =~ /\S/ ) {
457 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
459 my $contact_email = qsearchs('contact_email', { emailaddress=>$email } );
460 unless ($contact_email) { push @contact_emails, $email; next; }
462 my $contact = $contact_email->contact;
463 if ($contact->contactnum eq $self->contactnum) {
464 push @contact_emails, $email;
467 $contact_nums{$contact->contactnum} = '1';
472 ## were all emails duplicates? if so reset original emails
473 if (scalar @contact_emails < 1 && scalar (keys %contact_nums) > 1) {
474 foreach (qsearch('contact_email', {'contactnum' => $self->contactnum})) {
475 push @contact_emails, $_->emailaddress;
479 my $emails = join(' , ', @contact_emails);
480 $self->emailaddress($emails);
484 my $error = $self->SUPER::replace($old);
485 if ( $old->_password ne $self->_password ) {
486 $error ||= $self->insert_password_history;
489 $dbh->rollback if $oldAutoCommit;
493 my $cust_contact = '';
494 # if $self->custnum was set, then the customer-specific properties
495 # (custnum, classnum, invoice_dest, selfservice_access, comment) are in
496 # pseudo-fields, and are now in %link_hash. otherwise, ignore all those
500 foreach my $contactnum (keys %contact_nums) {
502 my %hash = ( 'contactnum' => $contactnum,
503 'custnum' => $custnum,
506 if ( $cust_contact = qsearchs('cust_contact', \%hash ) ) {
507 $cust_contact->$_($link_hash{$_}) for keys %link_hash;
508 $error = $cust_contact->replace;
510 $cust_contact = new FS::cust_contact { %hash, %link_hash };
511 $error = $cust_contact->insert;
514 $dbh->rollback if $oldAutoCommit;
520 if ( $prospectnum ) {
521 my %hash = ( 'contactnum' => $self->contactnum,
522 'prospectnum' => $prospectnum,
525 if ( my $prospect_contact = qsearchs('prospect_contact', \%hash ) ) {
526 $prospect_contact->$_($link_hash{$_}) for keys %link_hash;
527 $error = $prospect_contact->replace;
529 my $prospect_contact = new FS::prospect_contact { %hash, %link_hash };
530 $error = $prospect_contact->insert;
533 $dbh->rollback if $oldAutoCommit;
538 foreach my $pf ( grep { /^phonetypenum(\d+)$/ }
539 keys %{ $self->hashref } ) {
540 $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
541 my $phonetypenum = $1;
543 my %cp = ( 'contactnum' => $self->contactnum,
544 'phonetypenum' => $phonetypenum,
546 my $contact_phone = qsearchs('contact_phone', \%cp);
548 my $pv = $self->get($pf);
551 #if new value is empty, delete old entry
553 if ($contact_phone) {
554 $error = $contact_phone->delete;
556 $dbh->rollback if $oldAutoCommit;
563 $contact_phone ||= new FS::contact_phone \%cp;
565 my %cpd = _parse_phonestring( $pv );
566 $contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
568 my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
570 $error = $contact_phone->$method;
572 $dbh->rollback if $oldAutoCommit;
577 if ( defined($self->hashref->{'emailaddress'}) ) {
579 my %contact_emails = ();
580 foreach my $contact_email ( $self->contact_email ) {
581 $contact_emails{$contact_email->emailaddress} = '1';
584 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
586 unless ($contact_emails{$email}) {
587 my $contact_email = new FS::contact_email {
588 'contactnum' => $self->contactnum,
589 'emailaddress' => $email,
591 $error = $contact_email->insert;
593 $dbh->rollback if $oldAutoCommit;
597 else { delete($contact_emails{$email}); }
601 foreach my $contact_email ( $self->contact_email ) {
602 if ($contact_emails{$contact_email->emailaddress}) {
603 my $error = $contact_email->delete;
605 $dbh->rollback if $oldAutoCommit;
613 unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
614 #warn " queueing fuzzyfiles update\n"
616 $error = $self->queue_fuzzyfiles_update;
618 $dbh->rollback if $oldAutoCommit;
619 return "updating fuzzy search cache: $error";
623 if ( $cust_contact and (
624 ( $cust_contact->selfservice_access eq ''
625 && $link_hash{selfservice_access}
626 && ! length($self->_password)
628 || $cust_contact->_resend()
632 my $error = $self->send_reset_email( queue=>1 );
634 $dbh->rollback if $oldAutoCommit;
639 if ( $self->get('password') ) {
640 my $error = $self->is_password_allowed($self->get('password'))
641 || $self->change_password($self->get('password'));
643 $dbh->rollback if $oldAutoCommit;
648 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
654 =item _parse_phonestring PHONENUMBER_STRING
656 Subroutine, takes a string and returns a list (suitable for assigning to a hash)
657 with keys 'countrycode', 'phonenum' and 'extension'
659 (Should probably be moved to contact_phone.pm, hence the initial underscore.)
663 sub _parse_phonestring {
666 my($countrycode, $extension) = ('1', '');
669 if ( $value =~ s/^\s*\+\s*(\d+)// ) {
675 if ( $value =~ s/\s*(ext|x)\s*(\d+)\s*$//i ) {
679 ( 'countrycode' => $countrycode,
680 'phonenum' => $value,
681 'extension' => $extension,
685 =item queue_fuzzyfiles_update
687 Used by insert & replace to update the fuzzy search cache
691 use FS::cust_main::Search;
692 sub queue_fuzzyfiles_update {
695 local $SIG{HUP} = 'IGNORE';
696 local $SIG{INT} = 'IGNORE';
697 local $SIG{QUIT} = 'IGNORE';
698 local $SIG{TERM} = 'IGNORE';
699 local $SIG{TSTP} = 'IGNORE';
700 local $SIG{PIPE} = 'IGNORE';
702 my $oldAutoCommit = $FS::UID::AutoCommit;
703 local $FS::UID::AutoCommit = 0;
706 foreach my $field ( 'first', 'last' ) {
707 my $queue = new FS::queue {
708 'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
710 my @args = "contact.$field", $self->get($field);
711 my $error = $queue->insert( @args );
713 $dbh->rollback if $oldAutoCommit;
714 return "queueing job (transaction rolled back): $error";
718 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
725 Checks all fields to make sure this is a valid contact. If there is
726 an error, returns the error, otherwise returns false. Called by the insert
734 if ( $self->selfservice_access eq 'R' || $self->selfservice_access eq 'P' ) {
735 $self->selfservice_access('Y');
740 $self->ut_numbern('contactnum')
741 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
742 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
743 || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
744 || $self->ut_foreign_keyn('classnum', 'contact_class', 'classnum')
745 || $self->ut_namen('last')
746 || $self->ut_namen('first')
747 || $self->ut_textn('title')
748 || $self->ut_textn('comment')
749 || $self->ut_enum('selfservice_access', [ '', 'Y' ])
750 || $self->ut_textn('_password')
751 || $self->ut_enum('_password_encoding', [ '', 'bcrypt'])
752 || $self->ut_enum('disabled', [ '', 'Y' ])
754 return $error if $error;
756 return "Prospect and customer!" if $self->prospectnum && $self->custnum;
758 return "One of first name, last name, or title must have a value"
759 if ! grep $self->$_(), qw( first last title);
766 Returns a formatted string representing this contact, including name, title and
773 my $data = $self->first. ' '. $self->last;
774 $data .= ', '. $self->title
776 $data .= ' ('. $self->comment. ')'
783 Returns a formatted string representing this contact, with just the name.
789 $self->first . ' ' . $self->last;
792 #=item contact_classname PROSPECT_OBJ | CUST_MAIN_OBJ
794 #Returns the name of this contact's class for the specified prospect or
795 #customer (see L<FS::prospect_contact>, L<FS::cust_contact> and
796 #L<FS::contact_class>).
800 #sub contact_classname {
801 # my( $self, $prospect_or_cust ) = @_;
804 # if ( ref($prospect_or_cust) eq 'FS::prospect_main' ) {
805 # $link = qsearchs('prospect_contact', {
806 # 'contactnum' => $self->contactnum,
807 # 'prospectnum' => $prospect_or_cust->prospectnum,
809 # } elsif ( ref($prospect_or_cust) eq 'FS::cust_main' ) {
810 # $link = qsearchs('cust_contact', {
811 # 'contactnum' => $self->contactnum,
812 # 'custnum' => $prospect_or_cust->custnum,
815 # croak "$prospect_or_cust is not an FS::prospect_main or FS::cust_main object";
818 # my $contact_class = $link->contact_class or return '';
819 # $contact_class->classname;
822 =item by_selfservice_email EMAILADDRESS
824 Alternate search constructor (class method). Given an email address, returns
825 the contact for that address. If that contact doesn't have selfservice access,
826 or there isn't one, returns the empty string.
830 sub by_selfservice_email {
831 my($class, $email) = @_;
833 my $contact_email = qsearchs({
834 'table' => 'contact_email',
835 'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
836 'hashref' => { 'emailaddress' => $email, },
838 AND ( contact.disabled IS NULL )
839 AND EXISTS ( SELECT 1 FROM cust_contact
840 WHERE contact.contactnum = cust_contact.contactnum
841 AND cust_contact.selfservice_access = 'Y'
846 $contact_email->contact;
850 #these three functions are very much false laziness w/FS/FS/Auth/internal.pm
851 # and should maybe be libraried in some way for other password needs
853 use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64);
855 sub authenticate_password {
856 my($self, $check_password) = @_;
858 if ( $self->_password_encoding eq 'bcrypt' ) {
860 my( $cost, $salt, $hash ) = split(',', $self->_password);
862 my $check_hash = en_base64( bcrypt_hash( { key_nul => 1,
864 salt => de_base64($salt),
870 $hash eq $check_hash;
874 return 0 if $self->_password eq '';
876 $self->_password eq $check_password;
882 =item change_password NEW_PASSWORD
884 Changes the contact's selfservice access password to NEW_PASSWORD. This does
885 not check password policy rules (see C<is_password_allowed>) and will return
886 an error only if editing the record fails for some reason.
888 If NEW_PASSWORD is the same as the existing password, this does nothing.
892 sub change_password {
893 my($self, $new_password) = @_;
895 # do nothing if the password is unchanged
896 return if $self->authenticate_password($new_password);
898 $self->change_password_fields( $new_password );
904 sub change_password_fields {
905 my($self, $new_password) = @_;
907 $self->_password_encoding('bcrypt');
911 my $salt = pack( 'C*', map int(rand(256)), 1..16 );
913 my $hash = bcrypt_hash( { key_nul => 1,
921 join(',', $cost, en_base64($salt), en_base64($hash) )
926 # end of false laziness w/FS/FS/Auth/internal.pm
929 #false laziness w/ClientAPI/MyAccount/reset_passwd
930 use Digest::SHA qw(sha512_hex);
932 use FS::ClientAPI_SessionCache;
933 sub send_reset_email {
934 my( $self, %opt ) = @_;
936 my @contact_email = $self->contact_email or return '';
938 my $reset_session = {
939 'contactnum' => $self->contactnum,
940 'svcnum' => $opt{'svcnum'},
944 my $conf = new FS::Conf;
946 ($conf->config('selfservice-password_reset_hours') || 24 ). ' hours';
948 my $reset_session_id;
950 $reset_session_id = sha512_hex(time(). {}. rand(). $$)
951 } until ( ! defined $self->myaccount_cache->get("reset_passwd_$reset_session_id") );
954 $self->myaccount_cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
959 my @cust_contact = grep $_->selfservice_access, $self->cust_contact;
960 $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
962 my $agentnum = $cust_main ? $cust_main->agentnum : '';
963 my $msgnum = $conf->config('selfservice-password_reset_msgnum', $agentnum);
964 #die "selfservice-password_reset_msgnum unset" unless $msgnum;
965 return "selfservice-password_reset_msgnum unset" unless $msgnum;
966 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
967 return "selfservice-password_reset_msgnum cannot be loaded" unless $msg_template;
969 'to' => join(',', map $_->emailaddress, @contact_email ),
970 'cust_main' => $cust_main,
972 'substitutions' => { 'session_id' => $reset_session_id }
975 if ( $opt{'queue'} ) { #or should queueing just be the default?
977 my $cust_msg = $msg_template->prepare( %msg_template );
978 my $error = $cust_msg->insert;
979 return $error if $error;
980 my $queue = new FS::queue {
981 'job' => 'FS::cust_msg::process_send',
982 'custnum' => $cust_main ? $cust_main->custnum : '',
984 $queue->insert( $cust_msg->custmsgnum );
988 $msg_template->send( %msg_template );
994 use vars qw( $myaccount_cache );
995 sub myaccount_cache {
997 $myaccount_cache ||= new FS::ClientAPI_SessionCache( {
998 'namespace' => 'FS::ClientAPI::MyAccount',
1002 =item cgi_contact_fields
1004 Returns a list reference containing the set of contact fields used in the web
1005 interface for one-line editing (i.e. excluding contactnum, prospectnum, custnum
1006 and locationnum, as well as password fields, but including fields for
1007 contact_email and contact_phone records.)
1011 sub cgi_contact_fields {
1014 my @contact_fields = qw(
1015 classnum first last title comment emailaddress selfservice_access
1016 invoice_dest message_dest password
1019 push @contact_fields, 'phonetypenum'. $_->phonetypenum
1020 foreach qsearch({table=>'phone_type', order_by=>'weight'});
1026 use FS::upgrade_journal;
1027 sub _upgrade_data { #class method
1028 my ($class, %opts) = @_;
1030 # before anything else, migrate contact.custnum to cust_contact records
1031 unless ( FS::upgrade_journal->is_done('contact_invoice_dest') ) {
1033 local($skip_fuzzyfiles) = 1;
1035 foreach my $contact (qsearch('contact', {})) {
1036 my $error = $contact->replace;
1037 die $error if $error;
1040 FS::upgrade_journal->set_done('contact_invoice_dest');
1044 # always migrate cust_main_invoice records over
1045 local $FS::cust_main::import = 1; # override require_phone and such
1046 my $search = FS::Cursor->new('cust_main_invoice', {});
1048 while (my $cust_main_invoice = $search->fetch) {
1049 my $custnum = $cust_main_invoice->custnum;
1050 my $dest = $cust_main_invoice->dest;
1051 my $cust_main = $cust_main_invoice->cust_main;
1053 if ( $dest =~ /^\d+$/ ) {
1054 my $svc_acct = FS::svc_acct->by_key($dest);
1055 die "custnum $custnum, invoice destination svcnum $svc_acct does not exist\n"
1057 $dest = $svc_acct->email;
1059 push @{ $custnum_dest{$custnum} ||= [] }, $dest;
1061 my $error = $cust_main_invoice->delete;
1063 die "custnum $custnum, cleaning up cust_main_invoice: $error\n";
1067 foreach my $custnum (keys %custnum_dest) {
1068 my $dests = $custnum_dest{$custnum};
1069 my $cust_main = FS::cust_main->by_key($custnum);
1070 my $error = $cust_main->replace( invoicing_list => $dests );
1072 die "custnum $custnum, creating contact: $error\n";
1084 L<FS::Record>, schema.html from the base documentation.