4 use vars qw(@ISA $disable_agentcheck @SVCDB_CANCEL_SEQ $DEBUG);
5 use FS::UID qw( getotaker dbh );
6 use FS::Record qw( qsearch qsearchs );
7 use FS::Misc qw( send_email );
13 use FS::cust_bill_pkg;
16 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
18 # because they load configuraion by setting FS::UID::callback (see TODO)
24 # for sending cancel emails in sub cancel
27 @ISA = qw( FS::Record );
31 $disable_agentcheck = 0;
33 # The order in which to unprovision services.
34 @SVCDB_CANCEL_SEQ = qw( svc_external
43 my ( $hashref, $cache ) = @_;
44 #if ( $hashref->{'pkgpart'} ) {
45 if ( $hashref->{'pkg'} ) {
46 # #@{ $self->{'_pkgnum'} } = ();
47 # my $subcache = $cache->subcache('pkgpart', 'part_pkg');
48 # $self->{'_pkgpart'} = $subcache;
49 # #push @{ $self->{'_pkgnum'} },
50 # FS::part_pkg->new_or_cached($hashref, $subcache);
51 $self->{'_pkgpart'} = FS::part_pkg->new($hashref);
53 if ( exists $hashref->{'svcnum'} ) {
54 #@{ $self->{'_pkgnum'} } = ();
55 my $subcache = $cache->subcache('svcnum', 'cust_svc', $hashref->{pkgnum});
56 $self->{'_svcnum'} = $subcache;
57 #push @{ $self->{'_pkgnum'} },
58 FS::cust_svc->new_or_cached($hashref, $subcache) if $hashref->{svcnum};
64 FS::cust_pkg - Object methods for cust_pkg objects
70 $record = new FS::cust_pkg \%hash;
71 $record = new FS::cust_pkg { 'column' => 'value' };
73 $error = $record->insert;
75 $error = $new_record->replace($old_record);
77 $error = $record->delete;
79 $error = $record->check;
81 $error = $record->cancel;
83 $error = $record->suspend;
85 $error = $record->unsuspend;
87 $part_pkg = $record->part_pkg;
89 @labels = $record->labels;
91 $seconds = $record->seconds_since($timestamp);
93 $error = FS::cust_pkg::order( $custnum, \@pkgparts );
94 $error = FS::cust_pkg::order( $custnum, \@pkgparts, \@remove_pkgnums ] );
98 An FS::cust_pkg object represents a customer billing item. FS::cust_pkg
99 inherits from FS::Record. The following fields are currently supported:
103 =item pkgnum - primary key (assigned automatically for new billing items)
105 =item custnum - Customer (see L<FS::cust_main>)
107 =item pkgpart - Billing item definition (see L<FS::part_pkg>)
111 =item bill - date (next bill date)
113 =item last_bill - last bill date
121 =item otaker - order taker (assigned automatically if null, see L<FS::UID>)
123 =item manual_flag - If this field is set to 1, disables the automatic
124 unsuspension of this package when using the B<unsuspendauto> config file.
128 Note: setup, bill, susp, expire and cancel are specified as UNIX timestamps;
129 see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for
130 conversion functions.
138 Create a new billing item. To add the item to the database, see L<"insert">.
142 sub table { 'cust_pkg'; }
144 =item insert [ OPTION => VALUE ... ]
146 Adds this billing item to the database ("Orders" the item). If there is an
147 error, returns the error, otherwise returns false.
149 If the additional field I<promo_code> is defined instead of I<pkgpart>, it
150 will be used to look up the package definition and agent restrictions will be
153 The following options are available: I<change>
155 I<change>, if set true, supresses any referral credit to a referring customer.
160 my( $self, %options ) = @_;
162 local $SIG{HUP} = 'IGNORE';
163 local $SIG{INT} = 'IGNORE';
164 local $SIG{QUIT} = 'IGNORE';
165 local $SIG{TERM} = 'IGNORE';
166 local $SIG{TSTP} = 'IGNORE';
167 local $SIG{PIPE} = 'IGNORE';
169 my $oldAutoCommit = $FS::UID::AutoCommit;
170 local $FS::UID::AutoCommit = 0;
173 my $error = $self->SUPER::insert;
175 $dbh->rollback if $oldAutoCommit;
179 my $conf = new FS::Conf;
180 my $cust_main = $self->cust_main;
181 my $part_pkg = $self->part_pkg;
182 if ( $conf->exists('referral_credit')
183 && $cust_main->referral_custnum
184 && ! $options{'change'}
185 && $part_pkg->freq !~ /^0\D?$/
188 my $referring_cust_main = $cust_main->referring_cust_main;
189 if ( $referring_cust_main->status ne 'cancelled' ) {
191 if ( $part_pkg->freq !~ /^\d+$/ ) {
192 warn 'WARNING: Not crediting customer '. $cust_main->referral_custnum.
193 ' for package '. $self->pkgnum.
194 ' ( customer '. $self->custnum. ')'.
195 ' - One-time referral credits not (yet) available for '.
196 ' packages with '. $part_pkg->freq_pretty. ' frequency';
199 my $amount = sprintf( "%.2f", $part_pkg->base_recur / $part_pkg->freq );
201 $referring_cust_main->credit( $amount,
202 'Referral credit for '. $cust_main->name
205 $dbh->rollback if $oldAutoCommit;
206 return "Error crediting customer ". $cust_main->referral_custnum.
207 " for referral: $error";
215 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
222 This method now works but you probably shouldn't use it.
224 You don't want to delete billing items, because there would then be no record
225 the customer ever purchased the item. Instead, see the cancel method.
230 # return "Can't delete cust_pkg records!";
233 =item replace OLD_RECORD
235 Replaces the OLD_RECORD with this one in the database. If there is an error,
236 returns the error, otherwise returns false.
238 Currently, custnum, setup, bill, susp, expire, and cancel may be changed.
240 Changing pkgpart may have disasterous effects. See the order subroutine.
242 setup and bill are normally updated by calling the bill method of a customer
243 object (see L<FS::cust_main>).
245 suspend is normally updated by the suspend and unsuspend methods.
247 cancel is normally updated by the cancel method (and also the order subroutine
253 my( $new, $old ) = ( shift, shift );
255 #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
256 return "Can't change otaker!" if $old->otaker ne $new->otaker;
259 #return "Can't change setup once it exists!"
260 # if $old->getfield('setup') &&
261 # $old->getfield('setup') != $new->getfield('setup');
263 #some logic for bill, susp, cancel?
265 local($disable_agentcheck) = 1 if $old->pkgpart == $new->pkgpart;
267 $new->SUPER::replace($old);
272 Checks all fields to make sure this is a valid billing item. If there is an
273 error, returns the error, otherwise returns false. Called by the insert and
282 $self->ut_numbern('pkgnum')
283 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
284 || $self->ut_numbern('pkgpart')
285 || $self->ut_numbern('setup')
286 || $self->ut_numbern('bill')
287 || $self->ut_numbern('susp')
288 || $self->ut_numbern('cancel')
290 return $error if $error;
292 if ( $self->promo_code ) {
295 qsearchs('part_pkg', {
296 'pkgpart' => $self->pkgpart,
297 'promo_code' => { op=>'ILIKE', value=>$self->promo_code },
299 return 'Unknown promotional code' unless $promo_part_pkg;
300 $self->pkgpart($promo_part_pkg->pkgpart);
304 unless ( $disable_agentcheck ) {
306 qsearchs( 'agent', { 'agentnum' => $self->cust_main->agentnum } );
307 my $pkgpart_href = $agent->pkgpart_hashref;
308 return "agent ". $agent->agentnum.
309 " can't purchase pkgpart ". $self->pkgpart
310 unless $pkgpart_href->{ $self->pkgpart };
313 $error = $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' );
314 return $error if $error;
318 $self->otaker(getotaker) unless $self->otaker;
319 $self->otaker =~ /^([\w\.\-]{0,16})$/ or return "Illegal otaker";
322 if ( $self->dbdef_table->column('manual_flag') ) {
323 $self->manual_flag('') if $self->manual_flag eq ' ';
324 $self->manual_flag =~ /^([01]?)$/
325 or return "Illegal manual_flag ". $self->manual_flag;
326 $self->manual_flag($1);
332 =item cancel [ OPTION => VALUE ... ]
334 Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
335 in this package, then cancels the package itself (sets the cancel field to
338 Available options are: I<quiet>
340 I<quiet> can be set true to supress email cancellation notices.
342 If there is an error, returns the error, otherwise returns false.
347 my( $self, %options ) = @_;
350 local $SIG{HUP} = 'IGNORE';
351 local $SIG{INT} = 'IGNORE';
352 local $SIG{QUIT} = 'IGNORE';
353 local $SIG{TERM} = 'IGNORE';
354 local $SIG{TSTP} = 'IGNORE';
355 local $SIG{PIPE} = 'IGNORE';
357 my $oldAutoCommit = $FS::UID::AutoCommit;
358 local $FS::UID::AutoCommit = 0;
362 foreach my $cust_svc (
363 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
365 push @{ $svc{$cust_svc->part_svc->svcdb} }, $cust_svc;
368 foreach my $svcdb (@SVCDB_CANCEL_SEQ) {
369 foreach my $cust_svc (@{ $svc{$svcdb} }) {
370 my $error = $cust_svc->cancel;
373 $dbh->rollback if $oldAutoCommit;
374 return "Error cancelling cust_svc: $error";
379 unless ( $self->getfield('cancel') ) {
380 my %hash = $self->hash;
381 $hash{'cancel'} = time;
382 my $new = new FS::cust_pkg ( \%hash );
383 $error = $new->replace($self);
385 $dbh->rollback if $oldAutoCommit;
390 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
392 my $conf = new FS::Conf;
393 my @invoicing_list = grep { $_ ne 'POST' } $self->cust_main->invoicing_list;
394 if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) {
395 my $conf = new FS::Conf;
396 my $error = send_email(
397 'from' => $conf->config('invoice_from'),
398 'to' => \@invoicing_list,
399 'subject' => $conf->config('cancelsubject'),
400 'body' => [ map "$_\n", $conf->config('cancelmessage') ],
402 #should this do something on errors?
411 Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
412 package, then suspends the package itself (sets the susp field to now).
414 If there is an error, returns the error, otherwise returns false.
422 local $SIG{HUP} = 'IGNORE';
423 local $SIG{INT} = 'IGNORE';
424 local $SIG{QUIT} = 'IGNORE';
425 local $SIG{TERM} = 'IGNORE';
426 local $SIG{TSTP} = 'IGNORE';
427 local $SIG{PIPE} = 'IGNORE';
429 my $oldAutoCommit = $FS::UID::AutoCommit;
430 local $FS::UID::AutoCommit = 0;
433 foreach my $cust_svc (
434 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
436 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
438 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
439 $dbh->rollback if $oldAutoCommit;
440 return "Illegal svcdb value in part_svc!";
443 require "FS/$svcdb.pm";
445 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
447 $error = $svc->suspend;
449 $dbh->rollback if $oldAutoCommit;
456 unless ( $self->getfield('susp') ) {
457 my %hash = $self->hash;
458 $hash{'susp'} = time;
459 my $new = new FS::cust_pkg ( \%hash );
460 $error = $new->replace($self);
462 $dbh->rollback if $oldAutoCommit;
467 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
474 Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
475 package, then unsuspends the package itself (clears the susp field).
477 If there is an error, returns the error, otherwise returns false.
485 local $SIG{HUP} = 'IGNORE';
486 local $SIG{INT} = 'IGNORE';
487 local $SIG{QUIT} = 'IGNORE';
488 local $SIG{TERM} = 'IGNORE';
489 local $SIG{TSTP} = 'IGNORE';
490 local $SIG{PIPE} = 'IGNORE';
492 my $oldAutoCommit = $FS::UID::AutoCommit;
493 local $FS::UID::AutoCommit = 0;
496 foreach my $cust_svc (
497 qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
499 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
501 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
502 $dbh->rollback if $oldAutoCommit;
503 return "Illegal svcdb value in part_svc!";
506 require "FS/$svcdb.pm";
508 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
510 $error = $svc->unsuspend;
512 $dbh->rollback if $oldAutoCommit;
519 unless ( ! $self->getfield('susp') ) {
520 my %hash = $self->hash;
521 my $inactive = time - $hash{'susp'};
523 $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive
524 if $inactive > 0 && ( $hash{'bill'} || $hash{'setup'} );
525 my $new = new FS::cust_pkg ( \%hash );
526 $error = $new->replace($self);
528 $dbh->rollback if $oldAutoCommit;
533 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
540 Returns the last bill date, or if there is no last bill date, the setup date.
541 Useful for billing metered services.
547 if ( $self->dbdef_table->column('last_bill') ) {
548 return $self->setfield('last_bill', $_[0]) if @_;
549 return $self->getfield('last_bill') if $self->getfield('last_bill');
551 my $cust_bill_pkg = qsearchs('cust_bill_pkg', { 'pkgnum' => $self->pkgnum,
552 'edate' => $self->bill, } );
553 $cust_bill_pkg ? $cust_bill_pkg->sdate : $self->setup || 0;
558 Returns the definition for this billing item, as an FS::part_pkg object (see
565 #exists( $self->{'_pkgpart'} )
567 ? $self->{'_pkgpart'}
568 : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
573 Calls the I<calc_setup> of the FS::part_pkg object associated with this billing
580 $self->part_pkg->calc_setup($self, @_);
585 Calls the I<calc_recur> of the FS::part_pkg object associated with this billing
592 $self->part_pkg->calc_recur($self, @_);
595 =item cust_svc [ SVCPART ]
597 Returns the services for this package, as FS::cust_svc objects (see
598 L<FS::cust_svc>). If a svcpart is specified, return only the matching
607 return qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum,
608 'svcpart' => shift, } );
611 #if ( $self->{'_svcnum'} ) {
612 # values %{ $self->{'_svcnum'}->cache };
614 $self->_sort_cust_svc(
615 [ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ]
621 =item h_cust_svc END_TIMESTAMP [ START_TIMESTAMP ]
623 Returns historical services for this package created before END TIMESTAMP and
624 (optionally) not cancelled before START_TIMESTAMP, as FS::h_cust_svc objects
625 (see L<FS::h_cust_svc>).
632 $self->_sort_cust_svc(
633 [ qsearch( 'h_cust_svc',
634 { 'pkgnum' => $self->pkgnum, },
635 FS::h_cust_svc->sql_h_search(@_),
642 my( $self, $arrayref ) = @_;
645 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
647 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
648 'svcpart' => $_->svcpart } );
650 $pkg_svc ? $pkg_svc->primary_svc : '',
651 $pkg_svc ? $pkg_svc->quantity : 0,
658 =item num_cust_svc [ SVCPART ]
660 Returns the number of provisioned services for this package. If a svcpart is
661 specified, counts only the matching services.
667 my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?';
668 $sql .= ' AND svcpart = ?' if @_;
669 my $sth = dbh->prepare($sql) or die dbh->errstr;
670 $sth->execute($self->pkgnum, @_) or die $sth->errstr;
671 $sth->fetchrow_arrayref->[0];
674 =item available_part_svc
676 Returns a list FS::part_svc objects representing services included in this
677 package but not yet provisioned. Each FS::part_svc object also has an extra
678 field, I<num_avail>, which specifies the number of available services.
682 sub available_part_svc {
684 grep { $_->num_avail > 0 }
686 my $part_svc = $_->part_svc;
687 $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking
688 $_->quantity - $self->num_cust_svc($_->svcpart);
691 $self->part_pkg->pkg_svc;
696 Returns a list of lists, calling the label method for all services
697 (see L<FS::cust_svc>) of this billing item.
703 map { [ $_->label ] } $self->cust_svc;
706 =item h_labels END_TIMESTAMP [ START_TIMESTAMP ]
708 Like the labels method, but returns historical information on services that
709 were active as of END_TIMESTAMP and (optionally) not cancelled before
712 Returns a list of lists, calling the label method for all (historical) services
713 (see L<FS::h_cust_svc>) of this billing item.
719 map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
722 =item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
724 Like h_labels, except returns a simple flat list, and shortens long
725 (currently >5) lists of identical services to one line that lists the service
726 label and the number of individual services rather than individual items.
734 #tie %labels, 'Tie::IxHash';
735 push @{ $labels{$_->[0]} }, $_->[1]
736 foreach $self->h_labels(@_);
738 foreach my $label ( keys %labels ) {
739 my @values = @{ $labels{$label} };
740 my $num = scalar(@values);
742 push @labels, "$label ($num)";
744 push @labels, map { "$label: $_" } @values;
754 Returns the parent customer object (see L<FS::cust_main>).
760 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
763 =item seconds_since TIMESTAMP
765 Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
766 package have been online since TIMESTAMP, according to the session monitor.
768 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
769 L<Time::Local> and L<Date::Parse> for conversion functions.
774 my($self, $since) = @_;
777 foreach my $cust_svc (
778 grep { $_->part_svc->svcdb eq 'svc_acct' } $self->cust_svc
780 $seconds += $cust_svc->seconds_since($since);
787 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
789 Returns the numbers of seconds all accounts (see L<FS::svc_acct>) in this
790 package have been online between TIMESTAMP_START (inclusive) and TIMESTAMP_END
793 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
794 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
800 sub seconds_since_sqlradacct {
801 my($self, $start, $end) = @_;
805 foreach my $cust_svc (
807 my $part_svc = $_->part_svc;
808 $part_svc->svcdb eq 'svc_acct'
809 && scalar($part_svc->part_export('sqlradius'));
812 $seconds += $cust_svc->seconds_since_sqlradacct($start, $end);
819 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
821 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
822 in this package for sessions ending between TIMESTAMP_START (inclusive) and
826 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
827 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
832 sub attribute_since_sqlradacct {
833 my($self, $start, $end, $attrib) = @_;
837 foreach my $cust_svc (
839 my $part_svc = $_->part_svc;
840 $part_svc->svcdb eq 'svc_acct'
841 && scalar($part_svc->part_export('sqlradius'));
844 $sum += $cust_svc->attribute_since_sqlradacct($start, $end, $attrib);
851 =item transfer DEST_PKGNUM | DEST_CUST_PKG, [ OPTION => VALUE ... ]
853 Transfers as many services as possible from this package to another package.
855 The destination package can be specified by pkgnum by passing an FS::cust_pkg
856 object. The destination package must already exist.
858 Services are moved only if the destination allows services with the correct
859 I<svcpart> (not svcdb), unless the B<change_svcpart> option is set true. Use
860 this option with caution! No provision is made for export differences
861 between the old and new service definitions. Probably only should be used
862 when your exports for all service definitions of a given svcdb are identical.
863 (attempt a transfer without it first, to move all possible svcpart-matching
866 Any services that can't be moved remain in the original package.
868 Returns an error, if there is one; otherwise, returns the number of services
869 that couldn't be moved.
874 my ($self, $dest_pkgnum, %opt) = @_;
880 if (ref ($dest_pkgnum) eq 'FS::cust_pkg') {
881 $dest = $dest_pkgnum;
882 $dest_pkgnum = $dest->pkgnum;
884 $dest = qsearchs('cust_pkg', { pkgnum => $dest_pkgnum });
887 return ('Package does not exist: '.$dest_pkgnum) unless $dest;
889 foreach my $pkg_svc ( $dest->part_pkg->pkg_svc ) {
890 $target{$pkg_svc->svcpart} = $pkg_svc->quantity;
893 foreach my $cust_svc ($dest->cust_svc) {
894 $target{$cust_svc->svcpart}--;
897 my %svcpart2svcparts = ();
898 if ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
899 warn "change_svcpart option received, creating alternates list\n" if $DEBUG;
900 foreach my $svcpart ( map { $_->svcpart } $self->cust_svc ) {
901 next if exists $svcpart2svcparts{$svcpart};
902 my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
903 $svcpart2svcparts{$svcpart} = [
905 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
907 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $dest->pkgpart,
910 $pkg_svc ? $pkg_svc->primary_svc : '',
911 $pkg_svc ? $pkg_svc->quantity : 0,
915 grep { $_ != $svcpart }
917 qsearch('part_svc', { 'svcdb' => $part_svc->svcdb } )
919 warn "alternates for svcpart $svcpart: ".
920 join(', ', @{$svcpart2svcparts{$svcpart}}). "\n"
925 foreach my $cust_svc ($self->cust_svc) {
926 if($target{$cust_svc->svcpart} > 0) {
927 $target{$cust_svc->svcpart}--;
928 my $new = new FS::cust_svc {
929 svcnum => $cust_svc->svcnum,
930 svcpart => $cust_svc->svcpart,
931 pkgnum => $dest_pkgnum,
933 my $error = $new->replace($cust_svc);
934 return $error if $error;
935 } elsif ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
937 warn "looking for alternates for svcpart ". $cust_svc->svcpart. "\n";
938 warn "alternates to consider: ".
939 join(', ', @{$svcpart2svcparts{$cust_svc->svcpart}}). "\n";
941 my @alternate = grep {
942 warn "considering alternate svcpart $_: ".
943 "$target{$_} available in new package\n"
946 } @{$svcpart2svcparts{$cust_svc->svcpart}};
948 warn "alternate(s) found\n" if $DEBUG;
949 my $change_svcpart = $alternate[0];
950 $target{$change_svcpart}--;
951 my $new = new FS::cust_svc {
952 svcnum => $cust_svc->svcnum,
953 svcpart => $change_svcpart,
954 pkgnum => $dest_pkgnum,
956 my $error = $new->replace($cust_svc);
957 return $error if $error;
970 This method is deprecated. See the I<depend_jobnum> option to the insert and
971 order_pkgs methods in FS::cust_main for a better way to defer provisioning.
978 local $SIG{HUP} = 'IGNORE';
979 local $SIG{INT} = 'IGNORE';
980 local $SIG{QUIT} = 'IGNORE';
981 local $SIG{TERM} = 'IGNORE';
982 local $SIG{TSTP} = 'IGNORE';
983 local $SIG{PIPE} = 'IGNORE';
985 my $oldAutoCommit = $FS::UID::AutoCommit;
986 local $FS::UID::AutoCommit = 0;
989 foreach my $cust_svc ( $self->cust_svc ) {
990 #false laziness w/svc_Common::insert
991 my $svc_x = $cust_svc->svc_x;
992 foreach my $part_export ( $cust_svc->part_svc->part_export ) {
993 my $error = $part_export->export_insert($svc_x);
995 $dbh->rollback if $oldAutoCommit;
1001 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1012 =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ] ]
1014 CUSTNUM is a customer (see L<FS::cust_main>)
1016 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
1017 L<FS::part_pkg>) to order for this customer. Duplicates are of course
1020 REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
1021 remove for this customer. The services (see L<FS::cust_svc>) are moved to the
1022 new billing items. An error is returned if this is not possible (see
1023 L<FS::pkg_svc>). An empty arrayref is equivalent to not specifying this
1026 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
1027 newly-created cust_pkg objects.
1032 my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
1034 my $conf = new FS::Conf;
1036 # Transactionize this whole mess
1037 local $SIG{HUP} = 'IGNORE';
1038 local $SIG{INT} = 'IGNORE';
1039 local $SIG{QUIT} = 'IGNORE';
1040 local $SIG{TERM} = 'IGNORE';
1041 local $SIG{TSTP} = 'IGNORE';
1042 local $SIG{PIPE} = 'IGNORE';
1044 my $oldAutoCommit = $FS::UID::AutoCommit;
1045 local $FS::UID::AutoCommit = 0;
1049 my $cust_main = qsearchs('cust_main', { custnum => $custnum });
1050 return "Customer not found: $custnum" unless $cust_main;
1052 my $change = scalar(@$remove_pkgnum) != 0;
1054 # Create the new packages.
1055 foreach my $pkgpart (@$pkgparts) {
1056 my $cust_pkg = new FS::cust_pkg { custnum => $custnum,
1057 pkgpart => $pkgpart };
1058 $error = $cust_pkg->insert( 'change' => $change );
1060 $dbh->rollback if $oldAutoCommit;
1063 push @$return_cust_pkg, $cust_pkg;
1065 # $return_cust_pkg now contains refs to all of the newly
1068 # Transfer services and cancel old packages.
1069 foreach my $old_pkgnum (@$remove_pkgnum) {
1070 my $old_pkg = qsearchs ('cust_pkg', { pkgnum => $old_pkgnum });
1072 foreach my $new_pkg (@$return_cust_pkg) {
1073 $error = $old_pkg->transfer($new_pkg);
1074 if ($error and $error == 0) {
1075 # $old_pkg->transfer failed.
1076 $dbh->rollback if $oldAutoCommit;
1081 if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
1082 warn "trying transfer again with change_svcpart option\n" if $DEBUG;
1083 foreach my $new_pkg (@$return_cust_pkg) {
1084 $error = $old_pkg->transfer($new_pkg, 'change_svcpart'=>1 );
1085 if ($error and $error == 0) {
1086 # $old_pkg->transfer failed.
1087 $dbh->rollback if $oldAutoCommit;
1094 # Transfers were successful, but we went through all of the
1095 # new packages and still had services left on the old package.
1096 # We can't cancel the package under the circumstances, so abort.
1097 $dbh->rollback if $oldAutoCommit;
1098 return "Unable to transfer all services from package ".$old_pkg->pkgnum;
1100 $error = $old_pkg->cancel;
1106 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1114 sub order is not OO. Perhaps it should be moved to FS::cust_main and made so?
1116 In sub order, the @pkgparts array (passed by reference) is clobbered.
1118 Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard
1119 method to pass dates to the recur_prog expression, it should do so.
1121 FS::svc_acct, FS::svc_domain, FS::svc_www, FS::svc_ip and FS::svc_forward are
1122 loaded via 'use' at compile time, rather than via 'require' in sub { setup,
1123 suspend, unsuspend, cancel } because they use %FS::UID::callback to load
1124 configuration values. Probably need a subroutine which decides what to do
1125 based on whether or not we've fetched the user yet, rather than a hash. See
1126 FS::UID and the TODO.
1128 Now that things are transactional should the check in the insert method be
1133 L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>,
1134 L<FS::pkg_svc>, schema.html from the base documentation