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;
17 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
19 # because they load configuraion by setting FS::UID::callback (see TODO)
25 # for sending cancel emails in sub cancel
28 @ISA = qw( FS::Record );
32 $disable_agentcheck = 0;
34 # The order in which to unprovision services.
35 @SVCDB_CANCEL_SEQ = qw( svc_external
44 my ( $hashref, $cache ) = @_;
45 #if ( $hashref->{'pkgpart'} ) {
46 if ( $hashref->{'pkg'} ) {
47 # #@{ $self->{'_pkgnum'} } = ();
48 # my $subcache = $cache->subcache('pkgpart', 'part_pkg');
49 # $self->{'_pkgpart'} = $subcache;
50 # #push @{ $self->{'_pkgnum'} },
51 # FS::part_pkg->new_or_cached($hashref, $subcache);
52 $self->{'_pkgpart'} = FS::part_pkg->new($hashref);
54 if ( exists $hashref->{'svcnum'} ) {
55 #@{ $self->{'_pkgnum'} } = ();
56 my $subcache = $cache->subcache('svcnum', 'cust_svc', $hashref->{pkgnum});
57 $self->{'_svcnum'} = $subcache;
58 #push @{ $self->{'_pkgnum'} },
59 FS::cust_svc->new_or_cached($hashref, $subcache) if $hashref->{svcnum};
65 FS::cust_pkg - Object methods for cust_pkg objects
71 $record = new FS::cust_pkg \%hash;
72 $record = new FS::cust_pkg { 'column' => 'value' };
74 $error = $record->insert;
76 $error = $new_record->replace($old_record);
78 $error = $record->delete;
80 $error = $record->check;
82 $error = $record->cancel;
84 $error = $record->suspend;
86 $error = $record->unsuspend;
88 $part_pkg = $record->part_pkg;
90 @labels = $record->labels;
92 $seconds = $record->seconds_since($timestamp);
94 $error = FS::cust_pkg::order( $custnum, \@pkgparts );
95 $error = FS::cust_pkg::order( $custnum, \@pkgparts, \@remove_pkgnums ] );
99 An FS::cust_pkg object represents a customer billing item. FS::cust_pkg
100 inherits from FS::Record. The following fields are currently supported:
104 =item pkgnum - primary key (assigned automatically for new billing items)
106 =item custnum - Customer (see L<FS::cust_main>)
108 =item pkgpart - Billing item definition (see L<FS::part_pkg>)
112 =item bill - date (next bill date)
114 =item last_bill - last bill date
122 =item otaker - order taker (assigned automatically if null, see L<FS::UID>)
124 =item manual_flag - If this field is set to 1, disables the automatic
125 unsuspension of this package when using the B<unsuspendauto> config file.
129 Note: setup, bill, susp, expire and cancel are specified as UNIX timestamps;
130 see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for
131 conversion functions.
139 Create a new billing item. To add the item to the database, see L<"insert">.
143 sub table { 'cust_pkg'; }
145 =item insert [ OPTION => VALUE ... ]
147 Adds this billing item to the database ("Orders" the item). If there is an
148 error, returns the error, otherwise returns false.
150 If the additional field I<promo_code> is defined instead of I<pkgpart>, it
151 will be used to look up the package definition and agent restrictions will be
154 The following options are available: I<change>
156 I<change>, if set true, supresses any referral credit to a referring customer.
161 my( $self, %options ) = @_;
163 local $SIG{HUP} = 'IGNORE';
164 local $SIG{INT} = 'IGNORE';
165 local $SIG{QUIT} = 'IGNORE';
166 local $SIG{TERM} = 'IGNORE';
167 local $SIG{TSTP} = 'IGNORE';
168 local $SIG{PIPE} = 'IGNORE';
170 my $oldAutoCommit = $FS::UID::AutoCommit;
171 local $FS::UID::AutoCommit = 0;
174 my $error = $self->SUPER::insert;
176 $dbh->rollback if $oldAutoCommit;
180 #if ( $self->reg_code ) {
181 # my $reg_code = qsearchs('reg_code', { 'code' => $self->reg_code } );
182 # $error = $reg_code->delete;
184 # $dbh->rollback if $oldAutoCommit;
189 my $conf = new FS::Conf;
190 my $cust_main = $self->cust_main;
191 my $part_pkg = $self->part_pkg;
192 if ( $conf->exists('referral_credit')
193 && $cust_main->referral_custnum
194 && ! $options{'change'}
195 && $part_pkg->freq !~ /^0\D?$/
198 my $referring_cust_main = $cust_main->referring_cust_main;
199 if ( $referring_cust_main->status ne 'cancelled' ) {
201 if ( $part_pkg->freq !~ /^\d+$/ ) {
202 warn 'WARNING: Not crediting customer '. $cust_main->referral_custnum.
203 ' for package '. $self->pkgnum.
204 ' ( customer '. $self->custnum. ')'.
205 ' - One-time referral credits not (yet) available for '.
206 ' packages with '. $part_pkg->freq_pretty. ' frequency';
209 my $amount = sprintf( "%.2f", $part_pkg->base_recur / $part_pkg->freq );
211 $referring_cust_main->credit( $amount,
212 'Referral credit for '. $cust_main->name
215 $dbh->rollback if $oldAutoCommit;
216 return "Error crediting customer ". $cust_main->referral_custnum.
217 " for referral: $error";
225 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
232 This method now works but you probably shouldn't use it.
234 You don't want to delete billing items, because there would then be no record
235 the customer ever purchased the item. Instead, see the cancel method.
240 # return "Can't delete cust_pkg records!";
243 =item replace OLD_RECORD
245 Replaces the OLD_RECORD with this one in the database. If there is an error,
246 returns the error, otherwise returns false.
248 Currently, custnum, setup, bill, susp, expire, and cancel may be changed.
250 Changing pkgpart may have disasterous effects. See the order subroutine.
252 setup and bill are normally updated by calling the bill method of a customer
253 object (see L<FS::cust_main>).
255 suspend is normally updated by the suspend and unsuspend methods.
257 cancel is normally updated by the cancel method (and also the order subroutine
263 my( $new, $old ) = ( shift, shift );
265 #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
266 return "Can't change otaker!" if $old->otaker ne $new->otaker;
269 #return "Can't change setup once it exists!"
270 # if $old->getfield('setup') &&
271 # $old->getfield('setup') != $new->getfield('setup');
273 #some logic for bill, susp, cancel?
275 local($disable_agentcheck) = 1 if $old->pkgpart == $new->pkgpart;
277 $new->SUPER::replace($old);
282 Checks all fields to make sure this is a valid billing item. If there is an
283 error, returns the error, otherwise returns false. Called by the insert and
292 $self->ut_numbern('pkgnum')
293 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
294 || $self->ut_numbern('pkgpart')
295 || $self->ut_numbern('setup')
296 || $self->ut_numbern('bill')
297 || $self->ut_numbern('susp')
298 || $self->ut_numbern('cancel')
300 return $error if $error;
302 if ( $self->reg_code ) {
304 unless ( grep { $self->pkgpart == $_->pkgpart }
305 map { $_->reg_code_pkg }
306 qsearchs( 'reg_code', { 'code' => $self->reg_code,
307 'agentnum' => $self->cust_main->agentnum })
309 return "Unknown registraiton code";
312 } elsif ( $self->promo_code ) {
315 qsearchs('part_pkg', {
316 'pkgpart' => $self->pkgpart,
317 'promo_code' => { op=>'ILIKE', value=>$self->promo_code },
319 return 'Unknown promotional code' unless $promo_part_pkg;
323 unless ( $disable_agentcheck ) {
325 qsearchs( 'agent', { 'agentnum' => $self->cust_main->agentnum } );
326 my $pkgpart_href = $agent->pkgpart_hashref;
327 return "agent ". $agent->agentnum.
328 " can't purchase pkgpart ". $self->pkgpart
329 unless $pkgpart_href->{ $self->pkgpart };
332 $error = $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' );
333 return $error if $error;
337 $self->otaker(getotaker) unless $self->otaker;
338 $self->otaker =~ /^([\w\.\-]{0,16})$/ or return "Illegal otaker";
341 if ( $self->dbdef_table->column('manual_flag') ) {
342 $self->manual_flag('') if $self->manual_flag eq ' ';
343 $self->manual_flag =~ /^([01]?)$/
344 or return "Illegal manual_flag ". $self->manual_flag;
345 $self->manual_flag($1);
351 =item cancel [ OPTION => VALUE ... ]
353 Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
354 in this package, then cancels the package itself (sets the cancel field to
357 Available options are: I<quiet>
359 I<quiet> can be set true to supress email cancellation notices.
361 If there is an error, returns the error, otherwise returns false.
366 my( $self, %options ) = @_;
369 local $SIG{HUP} = 'IGNORE';
370 local $SIG{INT} = 'IGNORE';
371 local $SIG{QUIT} = 'IGNORE';
372 local $SIG{TERM} = 'IGNORE';
373 local $SIG{TSTP} = 'IGNORE';
374 local $SIG{PIPE} = 'IGNORE';
376 my $oldAutoCommit = $FS::UID::AutoCommit;
377 local $FS::UID::AutoCommit = 0;
381 foreach my $cust_svc (
382 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
384 push @{ $svc{$cust_svc->part_svc->svcdb} }, $cust_svc;
387 foreach my $svcdb (@SVCDB_CANCEL_SEQ) {
388 foreach my $cust_svc (@{ $svc{$svcdb} }) {
389 my $error = $cust_svc->cancel;
392 $dbh->rollback if $oldAutoCommit;
393 return "Error cancelling cust_svc: $error";
398 # Add a credit for remaining service
399 my $remaining_value = $self->calc_remain();
400 if ( $remaining_value > 0 ) {
401 my $error = $self->cust_main->credit(
403 'Credit for unused time on'. $self->part_pkg->pkg,
406 $dbh->rollback if $oldAutoCommit;
407 return "Error crediting customer \$$remaining_value for unused time on".
408 $self->part_pkg->pkg. ": $error";
412 unless ( $self->getfield('cancel') ) {
413 my %hash = $self->hash;
414 $hash{'cancel'} = time;
415 my $new = new FS::cust_pkg ( \%hash );
416 $error = $new->replace($self);
418 $dbh->rollback if $oldAutoCommit;
423 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
425 my $conf = new FS::Conf;
426 my @invoicing_list = grep { $_ ne 'POST' } $self->cust_main->invoicing_list;
427 if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) {
428 my $conf = new FS::Conf;
429 my $error = send_email(
430 'from' => $conf->config('invoice_from'),
431 'to' => \@invoicing_list,
432 'subject' => $conf->config('cancelsubject'),
433 'body' => [ map "$_\n", $conf->config('cancelmessage') ],
435 #should this do something on errors?
444 Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
445 package, then suspends the package itself (sets the susp field to now).
447 If there is an error, returns the error, otherwise returns false.
455 local $SIG{HUP} = 'IGNORE';
456 local $SIG{INT} = 'IGNORE';
457 local $SIG{QUIT} = 'IGNORE';
458 local $SIG{TERM} = 'IGNORE';
459 local $SIG{TSTP} = 'IGNORE';
460 local $SIG{PIPE} = 'IGNORE';
462 my $oldAutoCommit = $FS::UID::AutoCommit;
463 local $FS::UID::AutoCommit = 0;
466 foreach my $cust_svc (
467 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
469 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
471 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
472 $dbh->rollback if $oldAutoCommit;
473 return "Illegal svcdb value in part_svc!";
476 require "FS/$svcdb.pm";
478 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
480 $error = $svc->suspend;
482 $dbh->rollback if $oldAutoCommit;
489 unless ( $self->getfield('susp') ) {
490 my %hash = $self->hash;
491 $hash{'susp'} = time;
492 my $new = new FS::cust_pkg ( \%hash );
493 $error = $new->replace($self);
495 $dbh->rollback if $oldAutoCommit;
500 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
507 Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
508 package, then unsuspends the package itself (clears the susp field).
510 If there is an error, returns the error, otherwise returns false.
518 local $SIG{HUP} = 'IGNORE';
519 local $SIG{INT} = 'IGNORE';
520 local $SIG{QUIT} = 'IGNORE';
521 local $SIG{TERM} = 'IGNORE';
522 local $SIG{TSTP} = 'IGNORE';
523 local $SIG{PIPE} = 'IGNORE';
525 my $oldAutoCommit = $FS::UID::AutoCommit;
526 local $FS::UID::AutoCommit = 0;
529 foreach my $cust_svc (
530 qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
532 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
534 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
535 $dbh->rollback if $oldAutoCommit;
536 return "Illegal svcdb value in part_svc!";
539 require "FS/$svcdb.pm";
541 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
543 $error = $svc->unsuspend;
545 $dbh->rollback if $oldAutoCommit;
552 unless ( ! $self->getfield('susp') ) {
553 my %hash = $self->hash;
554 my $inactive = time - $hash{'susp'};
556 $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive
557 if $inactive > 0 && ( $hash{'bill'} || $hash{'setup'} );
558 my $new = new FS::cust_pkg ( \%hash );
559 $error = $new->replace($self);
561 $dbh->rollback if $oldAutoCommit;
566 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
573 Returns the last bill date, or if there is no last bill date, the setup date.
574 Useful for billing metered services.
580 if ( $self->dbdef_table->column('last_bill') ) {
581 return $self->setfield('last_bill', $_[0]) if @_;
582 return $self->getfield('last_bill') if $self->getfield('last_bill');
584 my $cust_bill_pkg = qsearchs('cust_bill_pkg', { 'pkgnum' => $self->pkgnum,
585 'edate' => $self->bill, } );
586 $cust_bill_pkg ? $cust_bill_pkg->sdate : $self->setup || 0;
591 Returns the definition for this billing item, as an FS::part_pkg object (see
598 #exists( $self->{'_pkgpart'} )
600 ? $self->{'_pkgpart'}
601 : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
606 Calls the I<calc_setup> of the FS::part_pkg object associated with this billing
613 $self->part_pkg->calc_setup($self, @_);
618 Calls the I<calc_recur> of the FS::part_pkg object associated with this billing
625 $self->part_pkg->calc_recur($self, @_);
630 Calls the I<calc_remain> of the FS::part_pkg object associated with this
637 $self->part_pkg->calc_remain($self, @_);
642 Calls the I<calc_cancel> of the FS::part_pkg object associated with this
649 $self->part_pkg->calc_cancel($self, @_);
652 =item cust_svc [ SVCPART ]
654 Returns the services for this package, as FS::cust_svc objects (see
655 L<FS::cust_svc>). If a svcpart is specified, return only the matching
664 return qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum,
665 'svcpart' => shift, } );
668 #if ( $self->{'_svcnum'} ) {
669 # values %{ $self->{'_svcnum'}->cache };
671 $self->_sort_cust_svc(
672 [ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ]
678 =item h_cust_svc END_TIMESTAMP [ START_TIMESTAMP ]
680 Returns historical services for this package created before END TIMESTAMP and
681 (optionally) not cancelled before START_TIMESTAMP, as FS::h_cust_svc objects
682 (see L<FS::h_cust_svc>).
689 $self->_sort_cust_svc(
690 [ qsearch( 'h_cust_svc',
691 { 'pkgnum' => $self->pkgnum, },
692 FS::h_cust_svc->sql_h_search(@_),
699 my( $self, $arrayref ) = @_;
702 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
704 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
705 'svcpart' => $_->svcpart } );
707 $pkg_svc ? $pkg_svc->primary_svc : '',
708 $pkg_svc ? $pkg_svc->quantity : 0,
715 =item num_cust_svc [ SVCPART ]
717 Returns the number of provisioned services for this package. If a svcpart is
718 specified, counts only the matching services.
724 my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?';
725 $sql .= ' AND svcpart = ?' if @_;
726 my $sth = dbh->prepare($sql) or die dbh->errstr;
727 $sth->execute($self->pkgnum, @_) or die $sth->errstr;
728 $sth->fetchrow_arrayref->[0];
731 =item available_part_svc
733 Returns a list FS::part_svc objects representing services included in this
734 package but not yet provisioned. Each FS::part_svc object also has an extra
735 field, I<num_avail>, which specifies the number of available services.
739 sub available_part_svc {
741 grep { $_->num_avail > 0 }
743 my $part_svc = $_->part_svc;
744 $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking
745 $_->quantity - $self->num_cust_svc($_->svcpart);
748 $self->part_pkg->pkg_svc;
753 Returns a list of lists, calling the label method for all services
754 (see L<FS::cust_svc>) of this billing item.
760 map { [ $_->label ] } $self->cust_svc;
763 =item h_labels END_TIMESTAMP [ START_TIMESTAMP ]
765 Like the labels method, but returns historical information on services that
766 were active as of END_TIMESTAMP and (optionally) not cancelled before
769 Returns a list of lists, calling the label method for all (historical) services
770 (see L<FS::h_cust_svc>) of this billing item.
776 map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
779 =item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
781 Like h_labels, except returns a simple flat list, and shortens long
782 (currently >5) lists of identical services to one line that lists the service
783 label and the number of individual services rather than individual items.
791 #tie %labels, 'Tie::IxHash';
792 push @{ $labels{$_->[0]} }, $_->[1]
793 foreach $self->h_labels(@_);
795 foreach my $label ( keys %labels ) {
796 my @values = @{ $labels{$label} };
797 my $num = scalar(@values);
799 push @labels, "$label ($num)";
801 push @labels, map { "$label: $_" } @values;
811 Returns the parent customer object (see L<FS::cust_main>).
817 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
820 =item seconds_since TIMESTAMP
822 Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
823 package have been online since TIMESTAMP, according to the session monitor.
825 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
826 L<Time::Local> and L<Date::Parse> for conversion functions.
831 my($self, $since) = @_;
834 foreach my $cust_svc (
835 grep { $_->part_svc->svcdb eq 'svc_acct' } $self->cust_svc
837 $seconds += $cust_svc->seconds_since($since);
844 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
846 Returns the numbers of seconds all accounts (see L<FS::svc_acct>) in this
847 package have been online between TIMESTAMP_START (inclusive) and TIMESTAMP_END
850 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
851 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
857 sub seconds_since_sqlradacct {
858 my($self, $start, $end) = @_;
862 foreach my $cust_svc (
864 my $part_svc = $_->part_svc;
865 $part_svc->svcdb eq 'svc_acct'
866 && scalar($part_svc->part_export('sqlradius'));
869 $seconds += $cust_svc->seconds_since_sqlradacct($start, $end);
876 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
878 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
879 in this package for sessions ending between TIMESTAMP_START (inclusive) and
883 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
884 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
889 sub attribute_since_sqlradacct {
890 my($self, $start, $end, $attrib) = @_;
894 foreach my $cust_svc (
896 my $part_svc = $_->part_svc;
897 $part_svc->svcdb eq 'svc_acct'
898 && scalar($part_svc->part_export('sqlradius'));
901 $sum += $cust_svc->attribute_since_sqlradacct($start, $end, $attrib);
908 =item transfer DEST_PKGNUM | DEST_CUST_PKG, [ OPTION => VALUE ... ]
910 Transfers as many services as possible from this package to another package.
912 The destination package can be specified by pkgnum by passing an FS::cust_pkg
913 object. The destination package must already exist.
915 Services are moved only if the destination allows services with the correct
916 I<svcpart> (not svcdb), unless the B<change_svcpart> option is set true. Use
917 this option with caution! No provision is made for export differences
918 between the old and new service definitions. Probably only should be used
919 when your exports for all service definitions of a given svcdb are identical.
920 (attempt a transfer without it first, to move all possible svcpart-matching
923 Any services that can't be moved remain in the original package.
925 Returns an error, if there is one; otherwise, returns the number of services
926 that couldn't be moved.
931 my ($self, $dest_pkgnum, %opt) = @_;
937 if (ref ($dest_pkgnum) eq 'FS::cust_pkg') {
938 $dest = $dest_pkgnum;
939 $dest_pkgnum = $dest->pkgnum;
941 $dest = qsearchs('cust_pkg', { pkgnum => $dest_pkgnum });
944 return ('Package does not exist: '.$dest_pkgnum) unless $dest;
946 foreach my $pkg_svc ( $dest->part_pkg->pkg_svc ) {
947 $target{$pkg_svc->svcpart} = $pkg_svc->quantity;
950 foreach my $cust_svc ($dest->cust_svc) {
951 $target{$cust_svc->svcpart}--;
954 my %svcpart2svcparts = ();
955 if ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
956 warn "change_svcpart option received, creating alternates list\n" if $DEBUG;
957 foreach my $svcpart ( map { $_->svcpart } $self->cust_svc ) {
958 next if exists $svcpart2svcparts{$svcpart};
959 my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
960 $svcpart2svcparts{$svcpart} = [
962 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
964 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $dest->pkgpart,
967 $pkg_svc ? $pkg_svc->primary_svc : '',
968 $pkg_svc ? $pkg_svc->quantity : 0,
972 grep { $_ != $svcpart }
974 qsearch('part_svc', { 'svcdb' => $part_svc->svcdb } )
976 warn "alternates for svcpart $svcpart: ".
977 join(', ', @{$svcpart2svcparts{$svcpart}}). "\n"
982 foreach my $cust_svc ($self->cust_svc) {
983 if($target{$cust_svc->svcpart} > 0) {
984 $target{$cust_svc->svcpart}--;
985 my $new = new FS::cust_svc {
986 svcnum => $cust_svc->svcnum,
987 svcpart => $cust_svc->svcpart,
988 pkgnum => $dest_pkgnum,
990 my $error = $new->replace($cust_svc);
991 return $error if $error;
992 } elsif ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
994 warn "looking for alternates for svcpart ". $cust_svc->svcpart. "\n";
995 warn "alternates to consider: ".
996 join(', ', @{$svcpart2svcparts{$cust_svc->svcpart}}). "\n";
998 my @alternate = grep {
999 warn "considering alternate svcpart $_: ".
1000 "$target{$_} available in new package\n"
1003 } @{$svcpart2svcparts{$cust_svc->svcpart}};
1005 warn "alternate(s) found\n" if $DEBUG;
1006 my $change_svcpart = $alternate[0];
1007 $target{$change_svcpart}--;
1008 my $new = new FS::cust_svc {
1009 svcnum => $cust_svc->svcnum,
1010 svcpart => $change_svcpart,
1011 pkgnum => $dest_pkgnum,
1013 my $error = $new->replace($cust_svc);
1014 return $error if $error;
1027 This method is deprecated. See the I<depend_jobnum> option to the insert and
1028 order_pkgs methods in FS::cust_main for a better way to defer provisioning.
1035 local $SIG{HUP} = 'IGNORE';
1036 local $SIG{INT} = 'IGNORE';
1037 local $SIG{QUIT} = 'IGNORE';
1038 local $SIG{TERM} = 'IGNORE';
1039 local $SIG{TSTP} = 'IGNORE';
1040 local $SIG{PIPE} = 'IGNORE';
1042 my $oldAutoCommit = $FS::UID::AutoCommit;
1043 local $FS::UID::AutoCommit = 0;
1046 foreach my $cust_svc ( $self->cust_svc ) {
1047 #false laziness w/svc_Common::insert
1048 my $svc_x = $cust_svc->svc_x;
1049 foreach my $part_export ( $cust_svc->part_svc->part_export ) {
1050 my $error = $part_export->export_insert($svc_x);
1052 $dbh->rollback if $oldAutoCommit;
1058 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1069 =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ] ]
1071 CUSTNUM is a customer (see L<FS::cust_main>)
1073 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
1074 L<FS::part_pkg>) to order for this customer. Duplicates are of course
1077 REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
1078 remove for this customer. The services (see L<FS::cust_svc>) are moved to the
1079 new billing items. An error is returned if this is not possible (see
1080 L<FS::pkg_svc>). An empty arrayref is equivalent to not specifying this
1083 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
1084 newly-created cust_pkg objects.
1089 my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
1091 my $conf = new FS::Conf;
1093 # Transactionize this whole mess
1094 local $SIG{HUP} = 'IGNORE';
1095 local $SIG{INT} = 'IGNORE';
1096 local $SIG{QUIT} = 'IGNORE';
1097 local $SIG{TERM} = 'IGNORE';
1098 local $SIG{TSTP} = 'IGNORE';
1099 local $SIG{PIPE} = 'IGNORE';
1101 my $oldAutoCommit = $FS::UID::AutoCommit;
1102 local $FS::UID::AutoCommit = 0;
1106 my $cust_main = qsearchs('cust_main', { custnum => $custnum });
1107 return "Customer not found: $custnum" unless $cust_main;
1109 my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
1112 my $change = scalar(@old_cust_pkg) != 0;
1115 if ( scalar(@old_cust_pkg) == 1 ) {
1116 #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( last_bill bill );
1117 $hash{'setup'} = time;
1120 # Create the new packages.
1121 foreach my $pkgpart (@$pkgparts) {
1122 my $cust_pkg = new FS::cust_pkg { custnum => $custnum,
1123 pkgpart => $pkgpart,
1126 $error = $cust_pkg->insert( 'change' => $change );
1128 $dbh->rollback if $oldAutoCommit;
1131 push @$return_cust_pkg, $cust_pkg;
1133 # $return_cust_pkg now contains refs to all of the newly
1136 # Transfer services and cancel old packages.
1137 foreach my $old_pkg (@old_cust_pkg) {
1139 foreach my $new_pkg (@$return_cust_pkg) {
1140 $error = $old_pkg->transfer($new_pkg);
1141 if ($error and $error == 0) {
1142 # $old_pkg->transfer failed.
1143 $dbh->rollback if $oldAutoCommit;
1148 if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
1149 warn "trying transfer again with change_svcpart option\n" if $DEBUG;
1150 foreach my $new_pkg (@$return_cust_pkg) {
1151 $error = $old_pkg->transfer($new_pkg, 'change_svcpart'=>1 );
1152 if ($error and $error == 0) {
1153 # $old_pkg->transfer failed.
1154 $dbh->rollback if $oldAutoCommit;
1161 # Transfers were successful, but we went through all of the
1162 # new packages and still had services left on the old package.
1163 # We can't cancel the package under the circumstances, so abort.
1164 $dbh->rollback if $oldAutoCommit;
1165 return "Unable to transfer all services from package ".$old_pkg->pkgnum;
1167 $error = $old_pkg->cancel;
1173 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1181 sub order is not OO. Perhaps it should be moved to FS::cust_main and made so?
1183 In sub order, the @pkgparts array (passed by reference) is clobbered.
1185 Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard
1186 method to pass dates to the recur_prog expression, it should do so.
1188 FS::svc_acct, FS::svc_domain, FS::svc_www, FS::svc_ip and FS::svc_forward are
1189 loaded via 'use' at compile time, rather than via 'require' in sub { setup,
1190 suspend, unsuspend, cancel } because they use %FS::UID::callback to load
1191 configuration values. Probably need a subroutine which decides what to do
1192 based on whether or not we've fetched the user yet, rather than a hash. See
1193 FS::UID and the TODO.
1195 Now that things are transactional should the check in the insert method be
1200 L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>,
1201 L<FS::pkg_svc>, schema.html from the base documentation