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'; }
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
155 This method now works but you probably shouldn't use it.
157 You don't want to delete billing items, because there would then be no record
158 the customer ever purchased the item. Instead, see the cancel method.
163 # return "Can't delete cust_pkg records!";
166 =item replace OLD_RECORD
168 Replaces the OLD_RECORD with this one in the database. If there is an error,
169 returns the error, otherwise returns false.
171 Currently, custnum, setup, bill, susp, expire, and cancel may be changed.
173 Changing pkgpart may have disasterous effects. See the order subroutine.
175 setup and bill are normally updated by calling the bill method of a customer
176 object (see L<FS::cust_main>).
178 suspend is normally updated by the suspend and unsuspend methods.
180 cancel is normally updated by the cancel method (and also the order subroutine
186 my( $new, $old ) = ( shift, shift );
188 #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
189 return "Can't change otaker!" if $old->otaker ne $new->otaker;
192 #return "Can't change setup once it exists!"
193 # if $old->getfield('setup') &&
194 # $old->getfield('setup') != $new->getfield('setup');
196 #some logic for bill, susp, cancel?
198 local($disable_agentcheck) = 1 if $old->pkgpart == $new->pkgpart;
200 $new->SUPER::replace($old);
205 Checks all fields to make sure this is a valid billing item. If there is an
206 error, returns the error, otherwise returns false. Called by the insert and
215 $self->ut_numbern('pkgnum')
216 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
217 || $self->ut_numbern('pkgpart')
218 || $self->ut_numbern('setup')
219 || $self->ut_numbern('bill')
220 || $self->ut_numbern('susp')
221 || $self->ut_numbern('cancel')
223 return $error if $error;
225 if ( $self->promo_code ) {
228 qsearchs('part_pkg', {
229 'pkgpart' => $self->pkgpart,
230 'promo_code' => { op=>'ILIKE', value=>$self->promo_code },
232 return 'Unknown promotional code' unless $promo_part_pkg;
233 $self->pkgpart($promo_part_pkg->pkgpart);
237 unless ( $disable_agentcheck ) {
239 qsearchs( 'agent', { 'agentnum' => $self->cust_main->agentnum } );
240 my $pkgpart_href = $agent->pkgpart_hashref;
241 return "agent ". $agent->agentnum.
242 " can't purchase pkgpart ". $self->pkgpart
243 unless $pkgpart_href->{ $self->pkgpart };
246 $error = $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' );
247 return $error if $error;
251 $self->otaker(getotaker) unless $self->otaker;
252 $self->otaker =~ /^([\w\.\-]{0,16})$/ or return "Illegal otaker";
255 if ( $self->dbdef_table->column('manual_flag') ) {
256 $self->manual_flag('') if $self->manual_flag eq ' ';
257 $self->manual_flag =~ /^([01]?)$/
258 or return "Illegal manual_flag ". $self->manual_flag;
259 $self->manual_flag($1);
265 =item cancel [ OPTION => VALUE ... ]
267 Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
268 in this package, then cancels the package itself (sets the cancel field to
271 Available options are: I<quiet>
273 I<quiet> can be set true to supress email cancellation notices.
275 If there is an error, returns the error, otherwise returns false.
280 my( $self, %options ) = @_;
283 local $SIG{HUP} = 'IGNORE';
284 local $SIG{INT} = 'IGNORE';
285 local $SIG{QUIT} = 'IGNORE';
286 local $SIG{TERM} = 'IGNORE';
287 local $SIG{TSTP} = 'IGNORE';
288 local $SIG{PIPE} = 'IGNORE';
290 my $oldAutoCommit = $FS::UID::AutoCommit;
291 local $FS::UID::AutoCommit = 0;
295 foreach my $cust_svc (
296 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
298 push @{ $svc{$cust_svc->part_svc->svcdb} }, $cust_svc;
301 foreach my $svcdb (@SVCDB_CANCEL_SEQ) {
302 foreach my $cust_svc (@{ $svc{$svcdb} }) {
303 my $error = $cust_svc->cancel;
306 $dbh->rollback if $oldAutoCommit;
307 return "Error cancelling cust_svc: $error";
312 unless ( $self->getfield('cancel') ) {
313 my %hash = $self->hash;
314 $hash{'cancel'} = time;
315 my $new = new FS::cust_pkg ( \%hash );
316 $error = $new->replace($self);
318 $dbh->rollback if $oldAutoCommit;
323 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
325 my $conf = new FS::Conf;
326 my @invoicing_list = grep { $_ ne 'POST' } $self->cust_main->invoicing_list;
327 if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) {
328 my $conf = new FS::Conf;
329 my $error = send_email(
330 'from' => $conf->config('invoice_from'),
331 'to' => \@invoicing_list,
332 'subject' => $conf->config('cancelsubject'),
333 'body' => [ map "$_\n", $conf->config('cancelmessage') ],
335 #should this do something on errors?
344 Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
345 package, then suspends the package itself (sets the susp field to now).
347 If there is an error, returns the error, otherwise returns false.
355 local $SIG{HUP} = 'IGNORE';
356 local $SIG{INT} = 'IGNORE';
357 local $SIG{QUIT} = 'IGNORE';
358 local $SIG{TERM} = 'IGNORE';
359 local $SIG{TSTP} = 'IGNORE';
360 local $SIG{PIPE} = 'IGNORE';
362 my $oldAutoCommit = $FS::UID::AutoCommit;
363 local $FS::UID::AutoCommit = 0;
366 foreach my $cust_svc (
367 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
369 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
371 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
372 $dbh->rollback if $oldAutoCommit;
373 return "Illegal svcdb value in part_svc!";
376 require "FS/$svcdb.pm";
378 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
380 $error = $svc->suspend;
382 $dbh->rollback if $oldAutoCommit;
389 unless ( $self->getfield('susp') ) {
390 my %hash = $self->hash;
391 $hash{'susp'} = time;
392 my $new = new FS::cust_pkg ( \%hash );
393 $error = $new->replace($self);
395 $dbh->rollback if $oldAutoCommit;
400 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
407 Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
408 package, then unsuspends the package itself (clears the susp field).
410 If there is an error, returns the error, otherwise returns false.
418 local $SIG{HUP} = 'IGNORE';
419 local $SIG{INT} = 'IGNORE';
420 local $SIG{QUIT} = 'IGNORE';
421 local $SIG{TERM} = 'IGNORE';
422 local $SIG{TSTP} = 'IGNORE';
423 local $SIG{PIPE} = 'IGNORE';
425 my $oldAutoCommit = $FS::UID::AutoCommit;
426 local $FS::UID::AutoCommit = 0;
429 foreach my $cust_svc (
430 qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
432 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
434 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
435 $dbh->rollback if $oldAutoCommit;
436 return "Illegal svcdb value in part_svc!";
439 require "FS/$svcdb.pm";
441 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
443 $error = $svc->unsuspend;
445 $dbh->rollback if $oldAutoCommit;
452 unless ( ! $self->getfield('susp') ) {
453 my %hash = $self->hash;
454 my $inactive = time - $hash{'susp'};
456 $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive
457 if $inactive > 0 && ( $hash{'bill'} || $hash{'setup'} );
458 my $new = new FS::cust_pkg ( \%hash );
459 $error = $new->replace($self);
461 $dbh->rollback if $oldAutoCommit;
466 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
473 Returns the last bill date, or if there is no last bill date, the setup date.
474 Useful for billing metered services.
480 if ( $self->dbdef_table->column('last_bill') ) {
481 return $self->setfield('last_bill', $_[0]) if @_;
482 return $self->getfield('last_bill') if $self->getfield('last_bill');
484 my $cust_bill_pkg = qsearchs('cust_bill_pkg', { 'pkgnum' => $self->pkgnum,
485 'edate' => $self->bill, } );
486 $cust_bill_pkg ? $cust_bill_pkg->sdate : $self->setup || 0;
491 Returns the definition for this billing item, as an FS::part_pkg object (see
498 #exists( $self->{'_pkgpart'} )
500 ? $self->{'_pkgpart'}
501 : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
506 Calls the I<calc_setup> of the FS::part_pkg object associated with this billing
513 $self->part_pkg->calc_setup($self, @_);
518 Calls the I<calc_recur> of the FS::part_pkg object associated with this billing
525 $self->part_pkg->calc_recur($self, @_);
528 =item cust_svc [ SVCPART ]
530 Returns the services for this package, as FS::cust_svc objects (see
531 L<FS::cust_svc>). If a svcpart is specified, return only the matching
540 return qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum,
541 'svcpart' => shift, } );
544 #if ( $self->{'_svcnum'} ) {
545 # values %{ $self->{'_svcnum'}->cache };
547 $self->_sort_cust_svc(
548 [ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ]
554 =item h_cust_svc END_TIMESTAMP [ START_TIMESTAMP ]
556 Returns historical services for this package created before END TIMESTAMP and
557 (optionally) not cancelled before START_TIMESTAMP, as FS::h_cust_svc objects
558 (see L<FS::h_cust_svc>).
565 $self->_sort_cust_svc(
566 [ qsearch( 'h_cust_svc',
567 { 'pkgnum' => $self->pkgnum, },
568 FS::h_cust_svc->sql_h_search(@_),
575 my( $self, $arrayref ) = @_;
578 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
580 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
581 'svcpart' => $_->svcpart } );
583 $pkg_svc ? $pkg_svc->primary_svc : '',
584 $pkg_svc ? $pkg_svc->quantity : 0,
591 =item num_cust_svc [ SVCPART ]
593 Returns the number of provisioned services for this package. If a svcpart is
594 specified, counts only the matching services.
600 my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?';
601 $sql .= ' AND svcpart = ?' if @_;
602 my $sth = dbh->prepare($sql) or die dbh->errstr;
603 $sth->execute($self->pkgnum, @_) or die $sth->errstr;
604 $sth->fetchrow_arrayref->[0];
607 =item available_part_svc
609 Returns a list FS::part_svc objects representing services included in this
610 package but not yet provisioned. Each FS::part_svc object also has an extra
611 field, I<num_avail>, which specifies the number of available services.
615 sub available_part_svc {
617 grep { $_->num_avail > 0 }
619 my $part_svc = $_->part_svc;
620 $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking
621 $_->quantity - $self->num_cust_svc($_->svcpart);
624 $self->part_pkg->pkg_svc;
629 Returns a list of lists, calling the label method for all services
630 (see L<FS::cust_svc>) of this billing item.
636 map { [ $_->label ] } $self->cust_svc;
639 =item h_labels END_TIMESTAMP [ START_TIMESTAMP ]
641 Like the labels method, but returns historical information on services that
642 were active as of END_TIMESTAMP and (optionally) not cancelled before
645 Returns a list of lists, calling the label method for all (historical) services
646 (see L<FS::h_cust_svc>) of this billing item.
652 map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
655 =item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
657 Like h_labels, except returns a simple flat list, and shortens long
658 (currently >5) lists of identical services to one line that lists the service
659 label and the number of individual services rather than individual items.
667 #tie %labels, 'Tie::IxHash';
668 push @{ $labels{$_->[0]} }, $_->[1]
669 foreach $self->h_labels(@_);
671 foreach my $label ( keys %labels ) {
672 my @values = @{ $labels{$label} };
673 my $num = scalar(@values);
675 push @labels, "$label ($num)";
677 push @labels, map { "$label: $_" } @values;
687 Returns the parent customer object (see L<FS::cust_main>).
693 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
696 =item seconds_since TIMESTAMP
698 Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
699 package have been online since TIMESTAMP, according to the session monitor.
701 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
702 L<Time::Local> and L<Date::Parse> for conversion functions.
707 my($self, $since) = @_;
710 foreach my $cust_svc (
711 grep { $_->part_svc->svcdb eq 'svc_acct' } $self->cust_svc
713 $seconds += $cust_svc->seconds_since($since);
720 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
722 Returns the numbers of seconds all accounts (see L<FS::svc_acct>) in this
723 package have been online between TIMESTAMP_START (inclusive) and TIMESTAMP_END
726 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
727 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
733 sub seconds_since_sqlradacct {
734 my($self, $start, $end) = @_;
738 foreach my $cust_svc (
740 my $part_svc = $_->part_svc;
741 $part_svc->svcdb eq 'svc_acct'
742 && scalar($part_svc->part_export('sqlradius'));
745 $seconds += $cust_svc->seconds_since_sqlradacct($start, $end);
752 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
754 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
755 in this package for sessions ending between TIMESTAMP_START (inclusive) and
759 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
760 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
765 sub attribute_since_sqlradacct {
766 my($self, $start, $end, $attrib) = @_;
770 foreach my $cust_svc (
772 my $part_svc = $_->part_svc;
773 $part_svc->svcdb eq 'svc_acct'
774 && scalar($part_svc->part_export('sqlradius'));
777 $sum += $cust_svc->attribute_since_sqlradacct($start, $end, $attrib);
784 =item transfer DEST_PKGNUM | DEST_CUST_PKG, [ OPTION => VALUE ... ]
786 Transfers as many services as possible from this package to another package.
788 The destination package can be specified by pkgnum by passing an FS::cust_pkg
789 object. The destination package must already exist.
791 Services are moved only if the destination allows services with the correct
792 I<svcpart> (not svcdb), unless the B<change_svcpart> option is set true. Use
793 this option with caution! No provision is made for export differences
794 between the old and new service definitions. Probably only should be used
795 when your exports for all service definitions of a given svcdb are identical.
796 (attempt a transfer without it first, to move all possible svcpart-matching
799 Any services that can't be moved remain in the original package.
801 Returns an error, if there is one; otherwise, returns the number of services
802 that couldn't be moved.
807 my ($self, $dest_pkgnum, %opt) = @_;
813 if (ref ($dest_pkgnum) eq 'FS::cust_pkg') {
814 $dest = $dest_pkgnum;
815 $dest_pkgnum = $dest->pkgnum;
817 $dest = qsearchs('cust_pkg', { pkgnum => $dest_pkgnum });
820 return ('Package does not exist: '.$dest_pkgnum) unless $dest;
822 foreach my $pkg_svc ( $dest->part_pkg->pkg_svc ) {
823 $target{$pkg_svc->svcpart} = $pkg_svc->quantity;
826 foreach my $cust_svc ($dest->cust_svc) {
827 $target{$cust_svc->svcpart}--;
830 my %svcpart2svcparts = ();
831 if ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
832 warn "change_svcpart option received, creating alternates list\n" if $DEBUG;
833 foreach my $svcpart ( map { $_->svcpart } $self->cust_svc ) {
834 next if exists $svcpart2svcparts{$svcpart};
835 my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
836 $svcpart2svcparts{$svcpart} = [
838 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
840 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $dest->pkgpart,
843 $pkg_svc ? $pkg_svc->primary_svc : '',
844 $pkg_svc ? $pkg_svc->quantity : 0,
848 grep { $_ != $svcpart }
850 qsearch('part_svc', { 'svcdb' => $part_svc->svcdb } )
852 warn "alternates for svcpart $svcpart: ".
853 join(', ', @{$svcpart2svcparts{$svcpart}}). "\n"
858 foreach my $cust_svc ($self->cust_svc) {
859 if($target{$cust_svc->svcpart} > 0) {
860 $target{$cust_svc->svcpart}--;
861 my $new = new FS::cust_svc {
862 svcnum => $cust_svc->svcnum,
863 svcpart => $cust_svc->svcpart,
864 pkgnum => $dest_pkgnum,
866 my $error = $new->replace($cust_svc);
867 return $error if $error;
868 } elsif ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
870 warn "looking for alternates for svcpart ". $cust_svc->svcpart. "\n";
871 warn "alternates to consider: ".
872 join(', ', @{$svcpart2svcparts{$cust_svc->svcpart}}). "\n";
874 my @alternate = grep {
875 warn "considering alternate svcpart $_: ".
876 "$target{$_} available in new package\n"
879 } @{$svcpart2svcparts{$cust_svc->svcpart}};
881 warn "alternate(s) found\n" if $DEBUG;
882 my $change_svcpart = $alternate[0];
883 $target{$change_svcpart}--;
884 my $new = new FS::cust_svc {
885 svcnum => $cust_svc->svcnum,
886 svcpart => $change_svcpart,
887 pkgnum => $dest_pkgnum,
889 my $error = $new->replace($cust_svc);
890 return $error if $error;
903 This method is deprecated. See the I<depend_jobnum> option to the insert and
904 order_pkgs methods in FS::cust_main for a better way to defer provisioning.
911 local $SIG{HUP} = 'IGNORE';
912 local $SIG{INT} = 'IGNORE';
913 local $SIG{QUIT} = 'IGNORE';
914 local $SIG{TERM} = 'IGNORE';
915 local $SIG{TSTP} = 'IGNORE';
916 local $SIG{PIPE} = 'IGNORE';
918 my $oldAutoCommit = $FS::UID::AutoCommit;
919 local $FS::UID::AutoCommit = 0;
922 foreach my $cust_svc ( $self->cust_svc ) {
923 #false laziness w/svc_Common::insert
924 my $svc_x = $cust_svc->svc_x;
925 foreach my $part_export ( $cust_svc->part_svc->part_export ) {
926 my $error = $part_export->export_insert($svc_x);
928 $dbh->rollback if $oldAutoCommit;
934 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
945 =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ] ]
947 CUSTNUM is a customer (see L<FS::cust_main>)
949 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
950 L<FS::part_pkg>) to order for this customer. Duplicates are of course
953 REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
954 remove for this customer. The services (see L<FS::cust_svc>) are moved to the
955 new billing items. An error is returned if this is not possible (see
956 L<FS::pkg_svc>). An empty arrayref is equivalent to not specifying this
959 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
960 newly-created cust_pkg objects.
965 my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
967 my $conf = new FS::Conf;
969 # Transactionize this whole mess
970 local $SIG{HUP} = 'IGNORE';
971 local $SIG{INT} = 'IGNORE';
972 local $SIG{QUIT} = 'IGNORE';
973 local $SIG{TERM} = 'IGNORE';
974 local $SIG{TSTP} = 'IGNORE';
975 local $SIG{PIPE} = 'IGNORE';
977 my $oldAutoCommit = $FS::UID::AutoCommit;
978 local $FS::UID::AutoCommit = 0;
982 my $cust_main = qsearchs('cust_main', { custnum => $custnum });
983 return "Customer not found: $custnum" unless $cust_main;
985 # Create the new packages.
987 foreach (@$pkgparts) {
988 $cust_pkg = new FS::cust_pkg { custnum => $custnum,
990 $error = $cust_pkg->insert;
992 $dbh->rollback if $oldAutoCommit;
995 push @$return_cust_pkg, $cust_pkg;
997 # $return_cust_pkg now contains refs to all of the newly
1000 # Transfer services and cancel old packages.
1001 foreach my $old_pkgnum (@$remove_pkgnum) {
1002 my $old_pkg = qsearchs ('cust_pkg', { pkgnum => $old_pkgnum });
1004 foreach my $new_pkg (@$return_cust_pkg) {
1005 $error = $old_pkg->transfer($new_pkg);
1006 if ($error and $error == 0) {
1007 # $old_pkg->transfer failed.
1008 $dbh->rollback if $oldAutoCommit;
1013 if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
1014 warn "trying transfer again with change_svcpart option\n" if $DEBUG;
1015 foreach my $new_pkg (@$return_cust_pkg) {
1016 $error = $old_pkg->transfer($new_pkg, 'change_svcpart'=>1 );
1017 if ($error and $error == 0) {
1018 # $old_pkg->transfer failed.
1019 $dbh->rollback if $oldAutoCommit;
1026 # Transfers were successful, but we went through all of the
1027 # new packages and still had services left on the old package.
1028 # We can't cancel the package under the circumstances, so abort.
1029 $dbh->rollback if $oldAutoCommit;
1030 return "Unable to transfer all services from package ".$old_pkg->pkgnum;
1032 $error = $old_pkg->cancel;
1038 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1046 sub order is not OO. Perhaps it should be moved to FS::cust_main and made so?
1048 In sub order, the @pkgparts array (passed by reference) is clobbered.
1050 Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard
1051 method to pass dates to the recur_prog expression, it should do so.
1053 FS::svc_acct, FS::svc_domain, FS::svc_www, FS::svc_ip and FS::svc_forward are
1054 loaded via 'use' at compile time, rather than via 'require' in sub { setup,
1055 suspend, unsuspend, cancel } because they use %FS::UID::callback to load
1056 configuration values. Probably need a subroutine which decides what to do
1057 based on whether or not we've fetched the user yet, rather than a hash. See
1058 FS::UID and the TODO.
1060 Now that things are transactional should the check in the insert method be
1065 L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>,
1066 L<FS::pkg_svc>, schema.html from the base documentation