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 = ();
171 $contact_nums{$self->contactnum} = '1' if $self->contactnum;
173 if ( $self->get('emailaddress') =~ /\S/ ) {
175 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
177 my $contact_email = qsearchs('contact_email', { emailaddress=>$email } );
178 unless ($contact_email) { push @contact_emails, $email; next; }
180 my $contact = $contact_email->contact;
181 if ($contact->contactnum eq $self->contactnum) {
182 push @contact_emails, $email;
185 $contact_nums{$contact->contactnum} = '1';
190 my $emails = join(' , ', @contact_emails);
191 $self->emailaddress($emails);
193 $no_new_contact = '1' unless $self->emailaddress;
198 $error = $self->SUPER::insert unless $no_new_contact;
201 $dbh->rollback if $oldAutoCommit;
205 my $cust_contact = '';
206 # if $self->custnum was set, then the customer-specific properties
207 # (custnum, classnum, invoice_dest, selfservice_access, comment) are in
208 # pseudo-fields, and are now in %link_hash. otherwise, ignore all those
211 foreach my $contactnum (keys %contact_nums) {
212 my %hash = ( 'contactnum' => $contactnum,
213 'custnum' => $custnum,
215 $cust_contact = qsearchs('cust_contact', \%hash )
216 || new FS::cust_contact { %hash, %link_hash };
217 my $error = $cust_contact->custcontactnum ? $cust_contact->replace
218 : $cust_contact->insert;
220 $dbh->rollback if $oldAutoCommit;
226 if ( $prospectnum && !$no_new_contact) {
227 my %hash = ( 'contactnum' => $self->contactnum,
228 'prospectnum' => $prospectnum,
230 my $prospect_contact = qsearchs('prospect_contact', \%hash )
231 || new FS::prospect_contact { %hash, %link_hash };
233 $prospect_contact->prospectcontactnum ? $prospect_contact->replace
234 : $prospect_contact->insert;
236 $dbh->rollback if $oldAutoCommit;
241 unless ($no_new_contact) {
242 foreach my $pf ( grep { /^phonetypenum(\d+)$/ && $self->get($_) =~ /\S/ }
243 keys %{ $self->hashref } ) {
244 $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
245 my $phonetypenum = $1;
247 my %hash = ( 'contactnum' => $self->contactnum,
248 'phonetypenum' => $phonetypenum,
251 qsearchs('contact_phone', \%hash)
252 || new FS::contact_phone { %hash, _parse_phonestring($self->get($pf)) };
253 my $error = $contact_phone->contactphonenum ? $contact_phone->replace
254 : $contact_phone->insert;
256 $dbh->rollback if $oldAutoCommit;
262 if ( $self->get('emailaddress') =~ /\S/ ) {
264 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
266 'contactnum' => $self->contactnum,
267 'emailaddress' => $email,
269 unless ( qsearchs('contact_email', \%hash) ) {
270 my $contact_email = new FS::contact_email \%hash;
271 my $error = $contact_email->insert;
273 $dbh->rollback if $oldAutoCommit;
281 unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
282 #warn " queueing fuzzyfiles update\n"
284 my $error = $self->queue_fuzzyfiles_update;
286 $dbh->rollback if $oldAutoCommit;
287 return "updating fuzzy search cache: $error";
291 if ( $link_hash{'selfservice_access'} eq 'R'
292 or ( $link_hash{'selfservice_access'}
294 && ! length($self->_password)
298 my $error = $self->send_reset_email( queue=>1 );
300 $dbh->rollback if $oldAutoCommit;
305 if ( $self->get('password') ) {
306 my $error = $self->is_password_allowed($self->get('password'))
307 || $self->change_password($self->get('password'));
309 $dbh->rollback if $oldAutoCommit;
314 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
322 Delete this record from the database.
329 local $SIG{HUP} = 'IGNORE';
330 local $SIG{INT} = 'IGNORE';
331 local $SIG{QUIT} = 'IGNORE';
332 local $SIG{TERM} = 'IGNORE';
333 local $SIG{TSTP} = 'IGNORE';
334 local $SIG{PIPE} = 'IGNORE';
336 my $oldAutoCommit = $FS::UID::AutoCommit;
337 local $FS::UID::AutoCommit = 0;
340 #got a prospetnum or custnum? delete the prospect_contact or cust_contact link
342 if ( $self->prospectnum ) {
343 my $prospect_contact = qsearchs('prospect_contact', {
344 'contactnum' => $self->contactnum,
345 'prospectnum' => $self->prospectnum,
347 my $error = $prospect_contact->delete;
349 $dbh->rollback if $oldAutoCommit;
354 # if $self->custnum was set, then we're removing the contact from this
356 if ( $self->custnum ) {
357 my $cust_contact = qsearchs('cust_contact', {
358 'contactnum' => $self->contactnum,
359 'custnum' => $self->custnum,
361 my $error = $cust_contact->delete;
363 $dbh->rollback if $oldAutoCommit;
368 # then, proceed with deletion only if the contact isn't attached to any other
369 # prospects or customers
371 #inefficient, but how many prospects/customers can a single contact be
372 # attached too? (and is removing them from one a common operation?)
373 if ( $self->prospect_contact || $self->cust_contact ) {
374 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
378 #proceed with deletion
380 foreach my $cust_pkg ( $self->cust_pkg ) {
381 $cust_pkg->contactnum('');
382 my $error = $cust_pkg->replace;
384 $dbh->rollback if $oldAutoCommit;
389 foreach my $object ( $self->contact_phone, $self->contact_email ) {
390 my $error = $object->delete;
392 $dbh->rollback if $oldAutoCommit;
397 my $error = $self->delete_password_history
398 || $self->SUPER::delete;
400 $dbh->rollback if $oldAutoCommit;
404 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
409 =item replace OLD_RECORD
411 Replaces the OLD_RECORD with this one in the database. If there is an error,
412 returns the error, otherwise returns false.
419 my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
421 : $self->replace_old;
423 $self->$_( $self->$_ || $old->$_ ) for qw( _password _password_encoding );
425 local $SIG{INT} = 'IGNORE';
426 local $SIG{QUIT} = 'IGNORE';
427 local $SIG{TERM} = 'IGNORE';
428 local $SIG{TSTP} = 'IGNORE';
429 local $SIG{PIPE} = 'IGNORE';
431 my $oldAutoCommit = $FS::UID::AutoCommit;
432 local $FS::UID::AutoCommit = 0;
435 #save off and blank values that move to cust_contact / prospect_contact now
436 my $prospectnum = $self->prospectnum;
437 $self->prospectnum('');
438 my $custnum = $self->custnum;
439 $self->custnum(''); $old->custnum(''); # remove because now stored cust_contact
442 for (qw( classnum comment selfservice_access invoice_dest message_dest )) {
443 $link_hash{$_} = $self->get($_);
444 $old->$_(''); ##remove values from old record, causes problem with history
448 ## check for an existing contact with this email address other than current customer
449 ## if found, just add that contact to cust_contact with link_hash credentials
450 ## as email can not be tied to two contacts.
451 my @contact_emails = ();
452 my %contact_nums = ();
453 $contact_nums{$self->contactnum} = '1' if $self->contactnum;
454 if ( $self->get('emailaddress') =~ /\S/ ) {
456 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
458 my $contact_email = qsearchs('contact_email', { emailaddress=>$email } );
459 unless ($contact_email) { push @contact_emails, $email; next; }
461 my $contact = $contact_email->contact;
462 if ($contact->contactnum eq $self->contactnum) {
463 push @contact_emails, $email;
466 $contact_nums{$contact->contactnum} = '1';
471 ## were all emails duplicates? if so reset original emails
472 if (scalar @contact_emails < 1 && scalar (keys %contact_nums) > 1) {
473 foreach (qsearch('contact_email', {'contactnum' => $self->contactnum})) {
474 push @contact_emails, $_->emailaddress;
478 my $emails = join(' , ', @contact_emails);
479 $self->emailaddress($emails);
483 my $error = $self->SUPER::replace($old);
484 if ( $old->_password ne $self->_password ) {
485 $error ||= $self->insert_password_history;
488 $dbh->rollback if $oldAutoCommit;
492 my $cust_contact = '';
493 # if $self->custnum was set, then the customer-specific properties
494 # (custnum, classnum, invoice_dest, selfservice_access, comment) are in
495 # pseudo-fields, and are now in %link_hash. otherwise, ignore all those
499 foreach my $contactnum (keys %contact_nums) {
501 my %hash = ( 'contactnum' => $contactnum,
502 'custnum' => $custnum,
505 if ( $cust_contact = qsearchs('cust_contact', \%hash ) ) {
506 $cust_contact->$_($link_hash{$_}) for keys %link_hash;
507 $error = $cust_contact->replace;
509 $cust_contact = new FS::cust_contact { %hash, %link_hash };
510 $error = $cust_contact->insert;
513 $dbh->rollback if $oldAutoCommit;
519 if ( $prospectnum ) {
520 my %hash = ( 'contactnum' => $self->contactnum,
521 'prospectnum' => $prospectnum,
524 if ( my $prospect_contact = qsearchs('prospect_contact', \%hash ) ) {
525 $prospect_contact->$_($link_hash{$_}) for keys %link_hash;
526 $error = $prospect_contact->replace;
528 my $prospect_contact = new FS::prospect_contact { %hash, %link_hash };
529 $error = $prospect_contact->insert;
532 $dbh->rollback if $oldAutoCommit;
537 foreach my $pf ( grep { /^phonetypenum(\d+)$/ }
538 keys %{ $self->hashref } ) {
539 $pf =~ /^phonetypenum(\d+)$/ or die "wtf (daily, the)";
540 my $phonetypenum = $1;
542 my %cp = ( 'contactnum' => $self->contactnum,
543 'phonetypenum' => $phonetypenum,
545 my $contact_phone = qsearchs('contact_phone', \%cp);
547 my $pv = $self->get($pf);
550 #if new value is empty, delete old entry
552 if ($contact_phone) {
553 $error = $contact_phone->delete;
555 $dbh->rollback if $oldAutoCommit;
562 $contact_phone ||= new FS::contact_phone \%cp;
564 my %cpd = _parse_phonestring( $pv );
565 $contact_phone->set( $_ => $cpd{$_} ) foreach keys %cpd;
567 my $method = $contact_phone->contactphonenum ? 'replace' : 'insert';
569 $error = $contact_phone->$method;
571 $dbh->rollback if $oldAutoCommit;
576 if ( defined($self->hashref->{'emailaddress'}) ) {
578 my %contact_emails = ();
579 foreach my $contact_email ( $self->contact_email ) {
580 $contact_emails{$contact_email->emailaddress} = '1';
583 foreach my $email ( split(/\s*,\s*/, $self->get('emailaddress') ) ) {
585 unless ($contact_emails{$email}) {
586 my $contact_email = new FS::contact_email {
587 'contactnum' => $self->contactnum,
588 'emailaddress' => $email,
590 $error = $contact_email->insert;
592 $dbh->rollback if $oldAutoCommit;
596 else { delete($contact_emails{$email}); }
600 foreach my $contact_email ( $self->contact_email ) {
601 if ($contact_emails{$contact_email->emailaddress}) {
602 my $error = $contact_email->delete;
604 $dbh->rollback if $oldAutoCommit;
612 unless ( $skip_fuzzyfiles ) { #unless ( $import || $skip_fuzzyfiles ) {
613 #warn " queueing fuzzyfiles update\n"
615 $error = $self->queue_fuzzyfiles_update;
617 $dbh->rollback if $oldAutoCommit;
618 return "updating fuzzy search cache: $error";
622 if ( $cust_contact and (
623 ( $cust_contact->selfservice_access eq ''
624 && $link_hash{selfservice_access}
625 && ! length($self->_password)
627 || $cust_contact->_resend()
631 my $error = $self->send_reset_email( queue=>1 );
633 $dbh->rollback if $oldAutoCommit;
638 if ( $self->get('password') ) {
639 my $error = $self->is_password_allowed($self->get('password'))
640 || $self->change_password($self->get('password'));
642 $dbh->rollback if $oldAutoCommit;
647 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
653 =item _parse_phonestring PHONENUMBER_STRING
655 Subroutine, takes a string and returns a list (suitable for assigning to a hash)
656 with keys 'countrycode', 'phonenum' and 'extension'
658 (Should probably be moved to contact_phone.pm, hence the initial underscore.)
662 sub _parse_phonestring {
665 my($countrycode, $extension) = ('1', '');
668 if ( $value =~ s/^\s*\+\s*(\d+)// ) {
674 if ( $value =~ s/\s*(ext|x)\s*(\d+)\s*$//i ) {
678 ( 'countrycode' => $countrycode,
679 'phonenum' => $value,
680 'extension' => $extension,
684 =item queue_fuzzyfiles_update
686 Used by insert & replace to update the fuzzy search cache
690 use FS::cust_main::Search;
691 sub queue_fuzzyfiles_update {
694 local $SIG{HUP} = 'IGNORE';
695 local $SIG{INT} = 'IGNORE';
696 local $SIG{QUIT} = 'IGNORE';
697 local $SIG{TERM} = 'IGNORE';
698 local $SIG{TSTP} = 'IGNORE';
699 local $SIG{PIPE} = 'IGNORE';
701 my $oldAutoCommit = $FS::UID::AutoCommit;
702 local $FS::UID::AutoCommit = 0;
705 foreach my $field ( 'first', 'last' ) {
706 my $queue = new FS::queue {
707 'job' => 'FS::cust_main::Search::append_fuzzyfiles_fuzzyfield'
709 my @args = "contact.$field", $self->get($field);
710 my $error = $queue->insert( @args );
712 $dbh->rollback if $oldAutoCommit;
713 return "queueing job (transaction rolled back): $error";
717 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
724 Checks all fields to make sure this is a valid contact. If there is
725 an error, returns the error, otherwise returns false. Called by the insert
733 if ( $self->selfservice_access eq 'R' || $self->selfservice_access eq 'P' ) {
734 $self->selfservice_access('Y');
739 $self->ut_numbern('contactnum')
740 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum')
741 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
742 || $self->ut_foreign_keyn('locationnum', 'cust_location', 'locationnum')
743 || $self->ut_foreign_keyn('classnum', 'contact_class', 'classnum')
744 || $self->ut_namen('last')
745 || $self->ut_namen('first')
746 || $self->ut_textn('title')
747 || $self->ut_textn('comment')
748 || $self->ut_enum('selfservice_access', [ '', 'Y' ])
749 || $self->ut_textn('_password')
750 || $self->ut_enum('_password_encoding', [ '', 'bcrypt'])
751 || $self->ut_enum('disabled', [ '', 'Y' ])
753 return $error if $error;
755 return "Prospect and customer!" if $self->prospectnum && $self->custnum;
757 return "One of first name, last name, or title must have a value"
758 if ! grep $self->$_(), qw( first last title);
765 Returns a formatted string representing this contact, including name, title and
772 my $data = $self->first. ' '. $self->last;
773 $data .= ', '. $self->title
775 $data .= ' ('. $self->comment. ')'
782 Returns a formatted string representing this contact, with just the name.
788 $self->first . ' ' . $self->last;
791 #=item contact_classname PROSPECT_OBJ | CUST_MAIN_OBJ
793 #Returns the name of this contact's class for the specified prospect or
794 #customer (see L<FS::prospect_contact>, L<FS::cust_contact> and
795 #L<FS::contact_class>).
799 #sub contact_classname {
800 # my( $self, $prospect_or_cust ) = @_;
803 # if ( ref($prospect_or_cust) eq 'FS::prospect_main' ) {
804 # $link = qsearchs('prospect_contact', {
805 # 'contactnum' => $self->contactnum,
806 # 'prospectnum' => $prospect_or_cust->prospectnum,
808 # } elsif ( ref($prospect_or_cust) eq 'FS::cust_main' ) {
809 # $link = qsearchs('cust_contact', {
810 # 'contactnum' => $self->contactnum,
811 # 'custnum' => $prospect_or_cust->custnum,
814 # croak "$prospect_or_cust is not an FS::prospect_main or FS::cust_main object";
817 # my $contact_class = $link->contact_class or return '';
818 # $contact_class->classname;
821 =item by_selfservice_email EMAILADDRESS
823 Alternate search constructor (class method). Given an email address, returns
824 the contact for that address. If that contact doesn't have selfservice access,
825 or there isn't one, returns the empty string.
829 sub by_selfservice_email {
830 my($class, $email) = @_;
832 my $contact_email = qsearchs({
833 'table' => 'contact_email',
834 'addl_from' => ' LEFT JOIN contact USING ( contactnum ) ',
835 'hashref' => { 'emailaddress' => $email, },
837 AND ( contact.disabled IS NULL )
838 AND EXISTS ( SELECT 1 FROM cust_contact
839 WHERE contact.contactnum = cust_contact.contactnum
840 AND cust_contact.selfservice_access = 'Y'
845 $contact_email->contact;
849 #these three functions are very much false laziness w/FS/FS/Auth/internal.pm
850 # and should maybe be libraried in some way for other password needs
852 use Crypt::Eksblowfish::Bcrypt qw( bcrypt_hash en_base64 de_base64);
854 sub authenticate_password {
855 my($self, $check_password) = @_;
857 if ( $self->_password_encoding eq 'bcrypt' ) {
859 my( $cost, $salt, $hash ) = split(',', $self->_password);
861 my $check_hash = en_base64( bcrypt_hash( { key_nul => 1,
863 salt => de_base64($salt),
869 $hash eq $check_hash;
873 return 0 if $self->_password eq '';
875 $self->_password eq $check_password;
881 =item change_password NEW_PASSWORD
883 Changes the contact's selfservice access password to NEW_PASSWORD. This does
884 not check password policy rules (see C<is_password_allowed>) and will return
885 an error only if editing the record fails for some reason.
887 If NEW_PASSWORD is the same as the existing password, this does nothing.
891 sub change_password {
892 my($self, $new_password) = @_;
894 # do nothing if the password is unchanged
895 return if $self->authenticate_password($new_password);
897 $self->change_password_fields( $new_password );
903 sub change_password_fields {
904 my($self, $new_password) = @_;
906 $self->_password_encoding('bcrypt');
910 my $salt = pack( 'C*', map int(rand(256)), 1..16 );
912 my $hash = bcrypt_hash( { key_nul => 1,
920 join(',', $cost, en_base64($salt), en_base64($hash) )
925 # end of false laziness w/FS/FS/Auth/internal.pm
928 #false laziness w/ClientAPI/MyAccount/reset_passwd
929 use Digest::SHA qw(sha512_hex);
931 use FS::ClientAPI_SessionCache;
932 sub send_reset_email {
933 my( $self, %opt ) = @_;
935 my @contact_email = $self->contact_email or return '';
937 my $reset_session = {
938 'contactnum' => $self->contactnum,
939 'svcnum' => $opt{'svcnum'},
943 my $conf = new FS::Conf;
945 ($conf->config('selfservice-password_reset_hours') || 24 ). ' hours';
947 my $reset_session_id;
949 $reset_session_id = sha512_hex(time(). {}. rand(). $$)
950 } until ( ! defined $self->myaccount_cache->get("reset_passwd_$reset_session_id") );
953 $self->myaccount_cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
958 my @cust_contact = grep $_->selfservice_access, $self->cust_contact;
959 $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
961 my $agentnum = $cust_main ? $cust_main->agentnum : '';
962 my $msgnum = $conf->config('selfservice-password_reset_msgnum', $agentnum);
963 #die "selfservice-password_reset_msgnum unset" unless $msgnum;
964 return "selfservice-password_reset_msgnum unset" unless $msgnum;
965 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
966 return "selfservice-password_reset_msgnum cannot be loaded" unless $msg_template;
968 'to' => join(',', map $_->emailaddress, @contact_email ),
969 'cust_main' => $cust_main,
971 'substitutions' => { 'session_id' => $reset_session_id }
974 if ( $opt{'queue'} ) { #or should queueing just be the default?
976 my $cust_msg = $msg_template->prepare( %msg_template );
977 my $error = $cust_msg->insert;
978 return $error if $error;
979 my $queue = new FS::queue {
980 'job' => 'FS::cust_msg::process_send',
981 'custnum' => $cust_main ? $cust_main->custnum : '',
983 $queue->insert( $cust_msg->custmsgnum );
987 $msg_template->send( %msg_template );
993 use vars qw( $myaccount_cache );
994 sub myaccount_cache {
996 $myaccount_cache ||= new FS::ClientAPI_SessionCache( {
997 'namespace' => 'FS::ClientAPI::MyAccount',
1001 =item cgi_contact_fields
1003 Returns a list reference containing the set of contact fields used in the web
1004 interface for one-line editing (i.e. excluding contactnum, prospectnum, custnum
1005 and locationnum, as well as password fields, but including fields for
1006 contact_email and contact_phone records.)
1010 sub cgi_contact_fields {
1013 my @contact_fields = qw(
1014 classnum first last title comment emailaddress selfservice_access
1015 invoice_dest message_dest password
1018 push @contact_fields, 'phonetypenum'. $_->phonetypenum
1019 foreach qsearch({table=>'phone_type', order_by=>'weight'});
1025 use FS::upgrade_journal;
1026 sub _upgrade_data { #class method
1027 my ($class, %opts) = @_;
1029 # before anything else, migrate contact.custnum to cust_contact records
1030 unless ( FS::upgrade_journal->is_done('contact_invoice_dest') ) {
1032 local($skip_fuzzyfiles) = 1;
1034 foreach my $contact (qsearch('contact', {})) {
1035 my $error = $contact->replace;
1036 die $error if $error;
1039 FS::upgrade_journal->set_done('contact_invoice_dest');
1043 # always migrate cust_main_invoice records over
1044 local $FS::cust_main::import = 1; # override require_phone and such
1045 my $search = FS::Cursor->new('cust_main_invoice', {});
1047 while (my $cust_main_invoice = $search->fetch) {
1048 my $custnum = $cust_main_invoice->custnum;
1049 my $dest = $cust_main_invoice->dest;
1050 my $cust_main = $cust_main_invoice->cust_main;
1052 if ( $dest =~ /^\d+$/ ) {
1053 my $svc_acct = FS::svc_acct->by_key($dest);
1054 die "custnum $custnum, invoice destination svcnum $svc_acct does not exist\n"
1056 $dest = $svc_acct->email;
1058 push @{ $custnum_dest{$custnum} ||= [] }, $dest;
1060 my $error = $cust_main_invoice->delete;
1062 die "custnum $custnum, cleaning up cust_main_invoice: $error\n";
1066 foreach my $custnum (keys %custnum_dest) {
1067 my $dests = $custnum_dest{$custnum};
1068 my $cust_main = FS::cust_main->by_key($custnum);
1069 my $error = $cust_main->replace( invoicing_list => $dests );
1071 die "custnum $custnum, creating contact: $error\n";
1083 L<FS::Record>, schema.html from the base documentation.