4 use vars qw(@ISA $disable_agentcheck $DEBUG);
5 use List::Util qw(max);
7 use FS::UID qw( getotaker dbh );
8 use FS::Misc qw( send_email );
9 use FS::Record qw( qsearch qsearchs );
10 use FS::cust_main_Mixin;
16 use FS::cust_bill_pkg;
20 use FS::cust_pkg_reason;
24 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
26 # because they load configuration by setting FS::UID::callback (see TODO)
32 # for sending cancel emails in sub cancel
35 @ISA = qw( FS::cust_main_Mixin FS::option_Common FS::Record );
39 $disable_agentcheck = 0;
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
123 =item otaker - order taker (assigned automatically if null, see L<FS::UID>)
125 =item manual_flag - If this field is set to 1, disables the automatic
126 unsuspension of this package when using the B<unsuspendauto> config file.
128 =item quantity - If not set, defaults to 1
132 Note: setup, bill, adjourn, susp, expire and cancel are specified as UNIX timestamps;
133 see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for
134 conversion functions.
142 Create a new billing item. To add the item to the database, see L<"insert">.
146 sub table { 'cust_pkg'; }
147 sub cust_linked { $_[0]->cust_main_custnum; }
148 sub cust_unlinked_msg {
150 "WARNING: can't find cust_main.custnum ". $self->custnum.
151 ' (cust_pkg.pkgnum '. $self->pkgnum. ')';
154 =item insert [ OPTION => VALUE ... ]
156 Adds this billing item to the database ("Orders" the item). If there is an
157 error, returns the error, otherwise returns false.
159 If the additional field I<promo_code> is defined instead of I<pkgpart>, it
160 will be used to look up the package definition and agent restrictions will be
163 The following options are available: I<change>
165 I<change>, if set true, supresses any referral credit to a referring customer.
170 my( $self, %options ) = @_;
172 local $SIG{HUP} = 'IGNORE';
173 local $SIG{INT} = 'IGNORE';
174 local $SIG{QUIT} = 'IGNORE';
175 local $SIG{TERM} = 'IGNORE';
176 local $SIG{TSTP} = 'IGNORE';
177 local $SIG{PIPE} = 'IGNORE';
179 my $oldAutoCommit = $FS::UID::AutoCommit;
180 local $FS::UID::AutoCommit = 0;
183 my $error = $self->SUPER::insert($options{options} ? %{$options{options}} : ());
185 $dbh->rollback if $oldAutoCommit;
189 #if ( $self->reg_code ) {
190 # my $reg_code = qsearchs('reg_code', { 'code' => $self->reg_code } );
191 # $error = $reg_code->delete;
193 # $dbh->rollback if $oldAutoCommit;
198 my $conf = new FS::Conf;
199 my $cust_main = $self->cust_main;
200 my $part_pkg = $self->part_pkg;
201 if ( $conf->exists('referral_credit')
202 && $cust_main->referral_custnum
203 && ! $options{'change'}
204 && $part_pkg->freq !~ /^0\D?$/
207 my $referring_cust_main = $cust_main->referring_cust_main;
208 if ( $referring_cust_main->status ne 'cancelled' ) {
210 if ( $part_pkg->freq !~ /^\d+$/ ) {
211 warn 'WARNING: Not crediting customer '. $cust_main->referral_custnum.
212 ' for package '. $self->pkgnum.
213 ' ( customer '. $self->custnum. ')'.
214 ' - One-time referral credits not (yet) available for '.
215 ' packages with '. $part_pkg->freq_pretty. ' frequency';
218 my $amount = sprintf( "%.2f", $part_pkg->base_recur / $part_pkg->freq );
220 $referring_cust_main->
222 'Referral credit for '.$cust_main->name,
223 'reason_type' => $conf->config('referral_credit_type')
226 $dbh->rollback if $oldAutoCommit;
227 return "Error crediting customer ". $cust_main->referral_custnum.
228 " for referral: $error";
236 if ($conf->config('welcome_letter') && $self->cust_main->num_pkgs == 1) {
237 my $queue = new FS::queue {
238 'job' => 'FS::cust_main::queueable_print',
240 $error = $queue->insert(
241 'custnum' => $self->custnum,
242 'template' => 'welcome_letter',
246 warn "can't send welcome letter: $error";
251 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
258 This method now works but you probably shouldn't use it.
260 You don't want to delete billing items, because there would then be no record
261 the customer ever purchased the item. Instead, see the cancel method.
266 # return "Can't delete cust_pkg records!";
269 =item replace OLD_RECORD
271 Replaces the OLD_RECORD with this one in the database. If there is an error,
272 returns the error, otherwise returns false.
274 Currently, custnum, setup, bill, adjourn, susp, expire, and cancel may be changed.
276 Changing pkgpart may have disasterous effects. See the order subroutine.
278 setup and bill are normally updated by calling the bill method of a customer
279 object (see L<FS::cust_main>).
281 suspend is normally updated by the suspend and unsuspend methods.
283 cancel is normally updated by the cancel method (and also the order subroutine
291 my( $new, $old, %options ) = @_;
293 # We absolutely have to have an old vs. new record to make this work.
294 if (!defined($old)) {
295 $old = qsearchs( 'cust_pkg', { 'pkgnum' => $new->pkgnum } );
297 #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
298 return "Can't change otaker!" if $old->otaker ne $new->otaker;
301 #return "Can't change setup once it exists!"
302 # if $old->getfield('setup') &&
303 # $old->getfield('setup') != $new->getfield('setup');
305 #some logic for bill, susp, cancel?
307 local($disable_agentcheck) = 1 if $old->pkgpart == $new->pkgpart;
309 local $SIG{HUP} = 'IGNORE';
310 local $SIG{INT} = 'IGNORE';
311 local $SIG{QUIT} = 'IGNORE';
312 local $SIG{TERM} = 'IGNORE';
313 local $SIG{TSTP} = 'IGNORE';
314 local $SIG{PIPE} = 'IGNORE';
316 my $oldAutoCommit = $FS::UID::AutoCommit;
317 local $FS::UID::AutoCommit = 0;
320 foreach my $method ( qw(adjourn expire) ) { # How many reasons?
321 if ($options{'reason'} && $new->$method && $old->$method ne $new->$method) {
322 my $error = $new->insert_reason( 'reason' => $options{'reason'},
323 'date' => $new->$method,
326 dbh->rollback if $oldAutoCommit;
327 return "Error inserting cust_pkg_reason: $error";
332 #save off and freeze RADIUS attributes for any associated svc_acct records
334 if ( $old->part_pkg->is_prepaid || $new->part_pkg->is_prepaid ) {
336 #also check for specific exports?
337 # to avoid spurious modify export events
338 @svc_acct = map { $_->svc_x }
339 grep { $_->part_svc->svcdb eq 'svc_acct' }
342 $_->snapshot foreach @svc_acct;
346 my $error = $new->SUPER::replace($old,
347 $options{options} ? ${options{options}} : ()
350 $dbh->rollback if $oldAutoCommit;
354 #for prepaid packages,
355 #trigger export of new RADIUS Expiration attribute when cust_pkg.bill changes
356 foreach my $old_svc_acct ( @svc_acct ) {
357 my $new_svc_acct = new FS::svc_acct { $old_svc_acct->hash };
358 my $s_error = $new_svc_acct->replace($old_svc_acct);
360 $dbh->rollback if $oldAutoCommit;
365 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
372 Checks all fields to make sure this is a valid billing item. If there is an
373 error, returns the error, otherwise returns false. Called by the insert and
382 $self->ut_numbern('pkgnum')
383 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
384 || $self->ut_numbern('pkgpart')
385 || $self->ut_numbern('setup')
386 || $self->ut_numbern('bill')
387 || $self->ut_numbern('susp')
388 || $self->ut_numbern('cancel')
389 || $self->ut_numbern('adjourn')
390 || $self->ut_numbern('expire')
392 return $error if $error;
394 if ( $self->reg_code ) {
396 unless ( grep { $self->pkgpart == $_->pkgpart }
397 map { $_->reg_code_pkg }
398 qsearchs( 'reg_code', { 'code' => $self->reg_code,
399 'agentnum' => $self->cust_main->agentnum })
401 return "Unknown registration code";
404 } elsif ( $self->promo_code ) {
407 qsearchs('part_pkg', {
408 'pkgpart' => $self->pkgpart,
409 'promo_code' => { op=>'ILIKE', value=>$self->promo_code },
411 return 'Unknown promotional code' unless $promo_part_pkg;
415 unless ( $disable_agentcheck ) {
417 qsearchs( 'agent', { 'agentnum' => $self->cust_main->agentnum } );
418 my $pkgpart_href = $agent->pkgpart_hashref;
419 return "agent ". $agent->agentnum.
420 " can't purchase pkgpart ". $self->pkgpart
421 unless $pkgpart_href->{ $self->pkgpart };
424 $error = $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' );
425 return $error if $error;
429 $self->otaker(getotaker) unless $self->otaker;
430 $self->otaker =~ /^(\w{1,32})$/ or return "Illegal otaker";
433 if ( $self->dbdef_table->column('manual_flag') ) {
434 $self->manual_flag('') if $self->manual_flag eq ' ';
435 $self->manual_flag =~ /^([01]?)$/
436 or return "Illegal manual_flag ". $self->manual_flag;
437 $self->manual_flag($1);
443 =item cancel [ OPTION => VALUE ... ]
445 Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
446 in this package, then cancels the package itself (sets the cancel field to
449 Available options are: I<quiet>
451 I<quiet> can be set true to supress email cancellation notices.
453 If there is an error, returns the error, otherwise returns false.
458 my( $self, %options ) = @_;
461 local $SIG{HUP} = 'IGNORE';
462 local $SIG{INT} = 'IGNORE';
463 local $SIG{QUIT} = 'IGNORE';
464 local $SIG{TERM} = 'IGNORE';
465 local $SIG{TSTP} = 'IGNORE';
466 local $SIG{PIPE} = 'IGNORE';
468 my $oldAutoCommit = $FS::UID::AutoCommit;
469 local $FS::UID::AutoCommit = 0;
472 if ($options{'reason'}) {
473 $error = $self->insert_reason( 'reason' => $options{'reason'} );
475 dbh->rollback if $oldAutoCommit;
476 return "Error inserting cust_pkg_reason: $error";
481 foreach my $cust_svc (
484 sort { $a->[1] <=> $b->[1] }
485 map { [ $_, $_->svc_x->table_info->{'cancel_weight'} ]; }
486 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
489 my $error = $cust_svc->cancel;
492 $dbh->rollback if $oldAutoCommit;
493 return "Error cancelling cust_svc: $error";
497 # Add a credit for remaining service
498 my $remaining_value = $self->calc_remain();
499 if ( $remaining_value > 0 ) {
500 my $conf = new FS::Conf;
501 my $error = $self->cust_main->credit(
503 'Credit for unused time on '. $self->part_pkg->pkg,
504 'reason_type' => $conf->config('cancel_credit_type'),
507 $dbh->rollback if $oldAutoCommit;
508 return "Error crediting customer \$$remaining_value for unused time on".
509 $self->part_pkg->pkg. ": $error";
513 unless ( $self->getfield('cancel') ) {
514 my %hash = $self->hash;
515 $hash{'cancel'} = time;
516 my $new = new FS::cust_pkg ( \%hash );
517 $error = $new->replace( $self, options => { $self->options } );
519 $dbh->rollback if $oldAutoCommit;
524 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
526 my $conf = new FS::Conf;
527 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list;
528 if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) {
529 my $conf = new FS::Conf;
530 my $error = send_email(
531 'from' => $conf->config('invoice_from'),
532 'to' => \@invoicing_list,
533 'subject' => ( $conf->config('cancelsubject') || 'Cancellation Notice' ),
534 'body' => [ map "$_\n", $conf->config('cancelmessage') ],
536 #should this do something on errors?
545 Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
546 package, then suspends the package itself (sets the susp field to now).
548 If there is an error, returns the error, otherwise returns false.
553 my( $self, %options ) = @_;
556 local $SIG{HUP} = 'IGNORE';
557 local $SIG{INT} = 'IGNORE';
558 local $SIG{QUIT} = 'IGNORE';
559 local $SIG{TERM} = 'IGNORE';
560 local $SIG{TSTP} = 'IGNORE';
561 local $SIG{PIPE} = 'IGNORE';
563 my $oldAutoCommit = $FS::UID::AutoCommit;
564 local $FS::UID::AutoCommit = 0;
567 if ($options{'reason'}) {
568 $error = $self->insert_reason( 'reason' => $options{'reason'} );
570 dbh->rollback if $oldAutoCommit;
571 return "Error inserting cust_pkg_reason: $error";
575 foreach my $cust_svc (
576 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
578 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
580 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
581 $dbh->rollback if $oldAutoCommit;
582 return "Illegal svcdb value in part_svc!";
585 require "FS/$svcdb.pm";
587 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
589 $error = $svc->suspend;
591 $dbh->rollback if $oldAutoCommit;
598 unless ( $self->getfield('susp') ) {
599 my %hash = $self->hash;
600 $hash{'susp'} = time;
601 my $new = new FS::cust_pkg ( \%hash );
602 $error = $new->replace( $self, options => { $self->options } );
604 $dbh->rollback if $oldAutoCommit;
609 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
614 =item unsuspend [ OPTION => VALUE ... ]
616 Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
617 package, then unsuspends the package itself (clears the susp field and the
618 adjourn field if it is in the past).
620 Available options are: I<adjust_next_bill>.
622 I<adjust_next_bill> can be set true to adjust the next bill date forward by
623 the amount of time the account was inactive. This was set true by default
624 since 1.4.2 and 1.5.0pre6; however, starting with 1.7.0 this needs to be
625 explicitly requested. Price plans for which this makes sense (anniversary-date
626 based than prorate or subscription) could have an option to enable this
629 If there is an error, returns the error, otherwise returns false.
634 my( $self, %opt ) = @_;
637 local $SIG{HUP} = 'IGNORE';
638 local $SIG{INT} = 'IGNORE';
639 local $SIG{QUIT} = 'IGNORE';
640 local $SIG{TERM} = 'IGNORE';
641 local $SIG{TSTP} = 'IGNORE';
642 local $SIG{PIPE} = 'IGNORE';
644 my $oldAutoCommit = $FS::UID::AutoCommit;
645 local $FS::UID::AutoCommit = 0;
648 foreach my $cust_svc (
649 qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
651 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
653 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
654 $dbh->rollback if $oldAutoCommit;
655 return "Illegal svcdb value in part_svc!";
658 require "FS/$svcdb.pm";
660 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
662 $error = $svc->unsuspend;
664 $dbh->rollback if $oldAutoCommit;
671 unless ( ! $self->getfield('susp') ) {
672 my %hash = $self->hash;
673 my $inactive = time - $hash{'susp'};
675 my $conf = new FS::Conf;
677 $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive
678 if ( $opt{'adjust_next_bill'}
679 || $conf->config('unsuspend-always_adjust_next_bill_date') )
680 && $inactive > 0 && ( $hash{'bill'} || $hash{'setup'} );
683 $hash{'adjourn'} = '' if $hash{'adjourn'} < time;
684 my $new = new FS::cust_pkg ( \%hash );
685 $error = $new->replace( $self, options => { $self->options } );
687 $dbh->rollback if $oldAutoCommit;
692 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
699 Returns the last bill date, or if there is no last bill date, the setup date.
700 Useful for billing metered services.
706 if ( $self->dbdef_table->column('last_bill') ) {
707 return $self->setfield('last_bill', $_[0]) if @_;
708 return $self->getfield('last_bill') if $self->getfield('last_bill');
710 my $cust_bill_pkg = qsearchs('cust_bill_pkg', { 'pkgnum' => $self->pkgnum,
711 'edate' => $self->bill, } );
712 $cust_bill_pkg ? $cust_bill_pkg->sdate : $self->setup || 0;
715 =item last_cust_pkg_reason
717 Returns the most recent FS::reason associated with the package.
721 sub last_cust_pkg_reason {
724 'table' => 'cust_pkg_reason',
725 'hashref' => { 'pkgnum' => $self->pkgnum, },
726 'extra_sql'=> "AND date <= ". time,
727 'order_by' => 'ORDER BY date DESC LIMIT 1',
733 Returns the most recent FS::reason associated with the package.
738 my $cust_pkg_reason = shift->last_cust_pkg_reason;
739 $cust_pkg_reason->reason
745 Returns the definition for this billing item, as an FS::part_pkg object (see
752 #exists( $self->{'_pkgpart'} )
754 ? $self->{'_pkgpart'}
755 : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
760 Returns the cancelled package this package was changed from, if any.
766 return '' unless $self->change_pkgnum;
767 qsearchs('cust_pkg', { 'pkgnum' => $self->change_pkgnum } );
772 Calls the I<calc_setup> of the FS::part_pkg object associated with this billing
779 $self->part_pkg->calc_setup($self, @_);
784 Calls the I<calc_recur> of the FS::part_pkg object associated with this billing
791 $self->part_pkg->calc_recur($self, @_);
796 Calls the I<calc_remain> of the FS::part_pkg object associated with this
803 $self->part_pkg->calc_remain($self, @_);
808 Calls the I<calc_cancel> of the FS::part_pkg object associated with this
815 $self->part_pkg->calc_cancel($self, @_);
820 Returns any invoice line items for this package (see L<FS::cust_bill_pkg>).
826 qsearch( 'cust_bill_pkg', { 'pkgnum' => $self->pkgnum } );
829 =item cust_svc [ SVCPART ]
831 Returns the services for this package, as FS::cust_svc objects (see
832 L<FS::cust_svc>). If a svcpart is specified, return only the matching
841 return qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum,
842 'svcpart' => shift, } );
845 #if ( $self->{'_svcnum'} ) {
846 # values %{ $self->{'_svcnum'}->cache };
848 $self->_sort_cust_svc(
849 [ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ]
855 =item overlimit [ SVCPART ]
857 Returns the services for this package which have exceeded their
858 usage limit as FS::cust_svc objects (see L<FS::cust_svc>). If a svcpart
859 is specified, return only the matching services.
865 grep { $_->overlimit } $self->cust_svc;
868 =item h_cust_svc END_TIMESTAMP [ START_TIMESTAMP ]
870 Returns historical services for this package created before END TIMESTAMP and
871 (optionally) not cancelled before START_TIMESTAMP, as FS::h_cust_svc objects
872 (see L<FS::h_cust_svc>).
879 $self->_sort_cust_svc(
880 [ qsearch( 'h_cust_svc',
881 { 'pkgnum' => $self->pkgnum, },
882 FS::h_cust_svc->sql_h_search(@_),
889 my( $self, $arrayref ) = @_;
892 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
894 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
895 'svcpart' => $_->svcpart } );
897 $pkg_svc ? $pkg_svc->primary_svc : '',
898 $pkg_svc ? $pkg_svc->quantity : 0,
905 =item num_cust_svc [ SVCPART ]
907 Returns the number of provisioned services for this package. If a svcpart is
908 specified, counts only the matching services.
914 my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?';
915 $sql .= ' AND svcpart = ?' if @_;
916 my $sth = dbh->prepare($sql) or die dbh->errstr;
917 $sth->execute($self->pkgnum, @_) or die $sth->errstr;
918 $sth->fetchrow_arrayref->[0];
921 =item available_part_svc
923 Returns a list of FS::part_svc objects representing services included in this
924 package but not yet provisioned. Each FS::part_svc object also has an extra
925 field, I<num_avail>, which specifies the number of available services.
929 sub available_part_svc {
931 grep { $_->num_avail > 0 }
933 my $part_svc = $_->part_svc;
934 $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking
935 $_->quantity - $self->num_cust_svc($_->svcpart);
938 $self->part_pkg->pkg_svc;
943 Returns a list of FS::part_svc objects representing provisioned and available
944 services included in this package. Each FS::part_svc object also has the
945 following extra fields:
949 =item num_cust_svc (count)
951 =item num_avail (quantity - count)
953 =item cust_pkg_svc (services) - array reference containing the provisioned services, as cust_svc objects
956 label -> ($cust_svc->label)[1]
965 #XXX some sort of sort order besides numeric by svcpart...
966 my @part_svc = sort { $a->svcpart <=> $b->svcpart } map {
968 my $part_svc = $pkg_svc->part_svc;
969 my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
970 $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #more evil
971 $part_svc->{'Hash'}{'num_avail'} =
972 max( 0, $pkg_svc->quantity - $num_cust_svc );
973 $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
975 } $self->part_pkg->pkg_svc;
978 push @part_svc, map {
980 my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
981 $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #speak no evail
982 $part_svc->{'Hash'}{'num_avail'} = 0; #0-$num_cust_svc ?
983 $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
985 } $self->extra_part_svc;
993 Returns a list of FS::part_svc objects corresponding to services in this
994 package which are still provisioned but not (any longer) available in the
1002 my $pkgnum = $self->pkgnum;
1003 my $pkgpart = $self->pkgpart;
1006 'table' => 'part_svc',
1008 'extra_sql' => "WHERE 0 = ( SELECT COUNT(*) FROM pkg_svc
1009 WHERE pkg_svc.svcpart = part_svc.svcpart
1010 AND pkg_svc.pkgpart = $pkgpart
1013 AND 0 < ( SELECT count(*)
1015 LEFT JOIN cust_pkg using ( pkgnum )
1016 WHERE cust_svc.svcpart = part_svc.svcpart
1017 AND pkgnum = $pkgnum
1024 Returns a short status string for this package, currently:
1028 =item not yet billed
1030 =item one-time charge
1045 my $freq = length($self->freq) ? $self->freq : $self->part_pkg->freq;
1047 return 'cancelled' if $self->get('cancel');
1048 return 'suspended' if $self->susp;
1049 return 'not yet billed' unless $self->setup;
1050 return 'one-time charge' if $freq =~ /^(0|$)/;
1056 Class method that returns the list of possible status strings for pacakges
1057 (see L<the status method|/status>). For example:
1059 @statuses = FS::cust_pkg->statuses();
1063 tie my %statuscolor, 'Tie::IxHash',
1064 'not yet billed' => '000000',
1065 'one-time charge' => '000000',
1066 'active' => '00CC00',
1067 'suspended' => 'FF9900',
1068 'cancelled' => 'FF0000',
1072 my $self = shift; #could be class...
1073 grep { $_ !~ /^(not yet billed)$/ } #this is a dumb status anyway
1074 # mayble split btw one-time vs. recur
1080 Returns a hex triplet color string for this package's status.
1086 $statuscolor{$self->status};
1091 Returns a list of lists, calling the label method for all services
1092 (see L<FS::cust_svc>) of this billing item.
1098 map { [ $_->label ] } $self->cust_svc;
1101 =item h_labels END_TIMESTAMP [ START_TIMESTAMP ]
1103 Like the labels method, but returns historical information on services that
1104 were active as of END_TIMESTAMP and (optionally) not cancelled before
1107 Returns a list of lists, calling the label method for all (historical) services
1108 (see L<FS::h_cust_svc>) of this billing item.
1114 map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
1117 =item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
1119 Like h_labels, except returns a simple flat list, and shortens long
1120 (currently >5) lists of identical services to one line that lists the service
1121 label and the number of individual services rather than individual items.
1125 sub h_labels_short {
1129 #tie %labels, 'Tie::IxHash';
1130 push @{ $labels{$_->[0]} }, $_->[1]
1131 foreach $self->h_labels(@_);
1133 foreach my $label ( keys %labels ) {
1134 my @values = @{ $labels{$label} };
1135 my $num = scalar(@values);
1137 push @labels, "$label ($num)";
1139 push @labels, map { "$label: $_" } @values;
1149 Returns the parent customer object (see L<FS::cust_main>).
1155 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
1158 =item seconds_since TIMESTAMP
1160 Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
1161 package have been online since TIMESTAMP, according to the session monitor.
1163 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1164 L<Time::Local> and L<Date::Parse> for conversion functions.
1169 my($self, $since) = @_;
1172 foreach my $cust_svc (
1173 grep { $_->part_svc->svcdb eq 'svc_acct' } $self->cust_svc
1175 $seconds += $cust_svc->seconds_since($since);
1182 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1184 Returns the numbers of seconds all accounts (see L<FS::svc_acct>) in this
1185 package have been online between TIMESTAMP_START (inclusive) and TIMESTAMP_END
1188 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1189 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1195 sub seconds_since_sqlradacct {
1196 my($self, $start, $end) = @_;
1200 foreach my $cust_svc (
1202 my $part_svc = $_->part_svc;
1203 $part_svc->svcdb eq 'svc_acct'
1204 && scalar($part_svc->part_export('sqlradius'));
1207 $seconds += $cust_svc->seconds_since_sqlradacct($start, $end);
1214 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1216 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1217 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1221 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1222 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1227 sub attribute_since_sqlradacct {
1228 my($self, $start, $end, $attrib) = @_;
1232 foreach my $cust_svc (
1234 my $part_svc = $_->part_svc;
1235 $part_svc->svcdb eq 'svc_acct'
1236 && scalar($part_svc->part_export('sqlradius'));
1239 $sum += $cust_svc->attribute_since_sqlradacct($start, $end, $attrib);
1251 my( $self, $value ) = @_;
1252 if ( defined($value) ) {
1253 $self->setfield('quantity', $value);
1255 $self->getfield('quantity') || 1;
1258 =item transfer DEST_PKGNUM | DEST_CUST_PKG, [ OPTION => VALUE ... ]
1260 Transfers as many services as possible from this package to another package.
1262 The destination package can be specified by pkgnum by passing an FS::cust_pkg
1263 object. The destination package must already exist.
1265 Services are moved only if the destination allows services with the correct
1266 I<svcpart> (not svcdb), unless the B<change_svcpart> option is set true. Use
1267 this option with caution! No provision is made for export differences
1268 between the old and new service definitions. Probably only should be used
1269 when your exports for all service definitions of a given svcdb are identical.
1270 (attempt a transfer without it first, to move all possible svcpart-matching
1273 Any services that can't be moved remain in the original package.
1275 Returns an error, if there is one; otherwise, returns the number of services
1276 that couldn't be moved.
1281 my ($self, $dest_pkgnum, %opt) = @_;
1287 if (ref ($dest_pkgnum) eq 'FS::cust_pkg') {
1288 $dest = $dest_pkgnum;
1289 $dest_pkgnum = $dest->pkgnum;
1291 $dest = qsearchs('cust_pkg', { pkgnum => $dest_pkgnum });
1294 return ('Package does not exist: '.$dest_pkgnum) unless $dest;
1296 foreach my $pkg_svc ( $dest->part_pkg->pkg_svc ) {
1297 $target{$pkg_svc->svcpart} = $pkg_svc->quantity;
1300 foreach my $cust_svc ($dest->cust_svc) {
1301 $target{$cust_svc->svcpart}--;
1304 my %svcpart2svcparts = ();
1305 if ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
1306 warn "change_svcpart option received, creating alternates list\n" if $DEBUG;
1307 foreach my $svcpart ( map { $_->svcpart } $self->cust_svc ) {
1308 next if exists $svcpart2svcparts{$svcpart};
1309 my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
1310 $svcpart2svcparts{$svcpart} = [
1312 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
1314 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $dest->pkgpart,
1315 'svcpart' => $_ } );
1317 $pkg_svc ? $pkg_svc->primary_svc : '',
1318 $pkg_svc ? $pkg_svc->quantity : 0,
1322 grep { $_ != $svcpart }
1324 qsearch('part_svc', { 'svcdb' => $part_svc->svcdb } )
1326 warn "alternates for svcpart $svcpart: ".
1327 join(', ', @{$svcpart2svcparts{$svcpart}}). "\n"
1332 foreach my $cust_svc ($self->cust_svc) {
1333 if($target{$cust_svc->svcpart} > 0) {
1334 $target{$cust_svc->svcpart}--;
1335 my $new = new FS::cust_svc { $cust_svc->hash };
1336 $new->pkgnum($dest_pkgnum);
1337 my $error = $new->replace($cust_svc);
1338 return $error if $error;
1339 } elsif ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
1341 warn "looking for alternates for svcpart ". $cust_svc->svcpart. "\n";
1342 warn "alternates to consider: ".
1343 join(', ', @{$svcpart2svcparts{$cust_svc->svcpart}}). "\n";
1345 my @alternate = grep {
1346 warn "considering alternate svcpart $_: ".
1347 "$target{$_} available in new package\n"
1350 } @{$svcpart2svcparts{$cust_svc->svcpart}};
1352 warn "alternate(s) found\n" if $DEBUG;
1353 my $change_svcpart = $alternate[0];
1354 $target{$change_svcpart}--;
1355 my $new = new FS::cust_svc { $cust_svc->hash };
1356 $new->svcpart($change_svcpart);
1357 $new->pkgnum($dest_pkgnum);
1358 my $error = $new->replace($cust_svc);
1359 return $error if $error;
1372 This method is deprecated. See the I<depend_jobnum> option to the insert and
1373 order_pkgs methods in FS::cust_main for a better way to defer provisioning.
1380 local $SIG{HUP} = 'IGNORE';
1381 local $SIG{INT} = 'IGNORE';
1382 local $SIG{QUIT} = 'IGNORE';
1383 local $SIG{TERM} = 'IGNORE';
1384 local $SIG{TSTP} = 'IGNORE';
1385 local $SIG{PIPE} = 'IGNORE';
1387 my $oldAutoCommit = $FS::UID::AutoCommit;
1388 local $FS::UID::AutoCommit = 0;
1391 foreach my $cust_svc ( $self->cust_svc ) {
1392 #false laziness w/svc_Common::insert
1393 my $svc_x = $cust_svc->svc_x;
1394 foreach my $part_export ( $cust_svc->part_svc->part_export ) {
1395 my $error = $part_export->export_insert($svc_x);
1397 $dbh->rollback if $oldAutoCommit;
1403 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1410 =head1 CLASS METHODS
1416 Returns an SQL expression identifying recurring packages.
1420 sub recurring_sql { "
1421 '0' != ( select freq from part_pkg
1422 where cust_pkg.pkgpart = part_pkg.pkgpart )
1427 Returns an SQL expression identifying one-time packages.
1432 '0' = ( select freq from part_pkg
1433 where cust_pkg.pkgpart = part_pkg.pkgpart )
1438 Returns an SQL expression identifying active packages.
1443 ". $_[0]->recurring_sql(). "
1444 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1445 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
1450 Returns an SQL expression identifying inactive packages (one-time packages
1451 that are otherwise unsuspended/uncancelled).
1455 sub inactive_sql { "
1456 ". $_[0]->onetime_sql(). "
1457 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1458 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
1464 Returns an SQL expression identifying suspended packages.
1468 sub suspended_sql { susp_sql(@_); }
1470 #$_[0]->recurring_sql(). ' AND '.
1472 ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1473 AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0
1480 Returns an SQL exprression identifying cancelled packages.
1484 sub cancelled_sql { cancel_sql(@_); }
1486 #$_[0]->recurring_sql(). ' AND '.
1487 "cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0";
1490 =item search_sql HASHREF
1494 Returns a qsearch hash expression to search for parameters specified in HASHREF.
1495 Valid parameters are
1503 active, inactive, suspended, cancel (or cancelled)
1507 active, inactive, suspended, one-time charge, inactive, cancel (or cancelled)
1517 arrayref of beginning and ending epoch date
1521 arrayref of beginning and ending epoch date
1525 arrayref of beginning and ending epoch date
1529 arrayref of beginning and ending epoch date
1533 arrayref of beginning and ending epoch date
1537 arrayref of beginning and ending epoch date
1541 arrayref of beginning and ending epoch date
1545 pkgnum or APKG_pkgnum
1549 a value suited to passing to FS::UI::Web::cust_header
1553 specifies the user for agent virtualization
1560 my ($class, $params) = @_;
1567 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
1576 if ( $params->{'magic'} eq 'active'
1577 || $params->{'status'} eq 'active' ) {
1579 push @where, FS::cust_pkg->active_sql();
1581 } elsif ( $params->{'magic'} eq 'inactive'
1582 || $params->{'status'} eq 'inactive' ) {
1584 push @where, FS::cust_pkg->inactive_sql();
1586 } elsif ( $params->{'magic'} eq 'suspended'
1587 || $params->{'status'} eq 'suspended' ) {
1589 push @where, FS::cust_pkg->suspended_sql();
1591 } elsif ( $params->{'magic'} =~ /^cancell?ed$/
1592 || $params->{'status'} =~ /^cancell?ed$/ ) {
1594 push @where, FS::cust_pkg->cancelled_sql();
1596 } elsif ( $params->{'status'} =~ /^(one-time charge|inactive)$/ ) {
1598 push @where, FS::cust_pkg->inactive_sql();
1603 # parse package class
1606 #false lazinessish w/graph/cust_bill_pkg.cgi
1609 if ( exists($params->{'classnum'})
1610 && $params->{'classnum'} =~ /^(\d*)$/
1614 if ( $classnum ) { #a specific class
1615 push @where, "classnum = $classnum";
1617 #@pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) );
1618 #die "classnum $classnum not found!" unless $pkg_class[0];
1619 #$title .= $pkg_class[0]->classname.' ';
1621 } elsif ( $classnum eq '' ) { #the empty class
1623 push @where, "classnum IS NULL";
1624 #$title .= 'Empty class ';
1625 #@pkg_class = ( '(empty class)' );
1626 } elsif ( $classnum eq '0' ) {
1627 #@pkg_class = qsearch('pkg_class', {} ); # { 'disabled' => '' } );
1628 #push @pkg_class, '(empty class)';
1630 die "illegal classnum";
1639 my $pkgpart = join (' OR pkgpart=',
1640 grep {$_} map { /^(\d+)$/; } ($params->{'pkgpart'}));
1641 push @where, '(pkgpart=' . $pkgpart . ')' if $pkgpart;
1649 #false laziness w/report_cust_pkg.html
1652 'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
1653 'active' => { 'susp'=>1, 'cancel'=>1 },
1654 'suspended' => { 'cancel' => 1 },
1659 foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
1661 next unless exists($params->{$field});
1663 my($beginning, $ending) = @{$params->{$field}};
1665 next if $beginning == 0 && $ending == 4294967295;
1668 "cust_pkg.$field IS NOT NULL",
1669 "cust_pkg.$field >= $beginning",
1670 "cust_pkg.$field <= $ending";
1672 $orderby ||= "ORDER BY cust_pkg.$field";
1676 $orderby ||= 'ORDER BY bill';
1679 # parse magic, legacy, etc.
1682 if ( $params->{'magic'} &&
1683 $params->{'magic'} =~ /^(active|inactive|suspended|cancell?ed)$/
1686 $orderby = 'ORDER BY pkgnum';
1688 if ( $params->{'pkgpart'} =~ /^(\d+)$/ ) {
1689 push @where, "pkgpart = $1";
1692 } elsif ( $params->{'query'} eq 'pkgnum' ) {
1694 $orderby = 'ORDER BY pkgnum';
1696 } elsif ( $params->{'query'} eq 'APKG_pkgnum' ) {
1698 $orderby = 'ORDER BY pkgnum';
1701 SELECT count(*) FROM pkg_svc
1702 WHERE pkg_svc.pkgpart = cust_pkg.pkgpart
1703 AND pkg_svc.quantity > ( SELECT count(*) FROM cust_svc
1704 WHERE cust_svc.pkgnum = cust_pkg.pkgnum
1705 AND cust_svc.svcpart = pkg_svc.svcpart
1712 # setup queries, links, subs, etc. for the search
1715 # here is the agent virtualization
1716 if ($params->{CurrentUser}) {
1718 qsearchs('access_user', { username => $params->{CurrentUser} });
1721 push @where, $access_user->agentnums_sql;
1726 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
1729 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
1731 my $addl_from = 'LEFT JOIN cust_main USING ( custnum ) '.
1732 'LEFT JOIN part_pkg USING ( pkgpart ) '.
1733 'LEFT JOIN pkg_class USING ( classnum ) ';
1735 my $count_query = "SELECT COUNT(*) FROM cust_pkg $addl_from $extra_sql";
1738 'table' => 'cust_pkg',
1740 'select' => join(', ',
1742 ( map "part_pkg.$_", qw( pkg freq ) ),
1743 'pkg_class.classname',
1744 'cust_main.custnum as cust_main_custnum',
1745 FS::UI::Web::cust_sql_fields(
1746 $params->{'cust_fields'}
1749 'extra_sql' => "$extra_sql $orderby",
1750 'addl_from' => $addl_from,
1751 'count_query' => $count_query,
1760 =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ] ]
1762 CUSTNUM is a customer (see L<FS::cust_main>)
1764 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
1765 L<FS::part_pkg>) to order for this customer. Duplicates are of course
1768 REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
1769 remove for this customer. The services (see L<FS::cust_svc>) are moved to the
1770 new billing items. An error is returned if this is not possible (see
1771 L<FS::pkg_svc>). An empty arrayref is equivalent to not specifying this
1774 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
1775 newly-created cust_pkg objects.
1780 my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
1782 my $conf = new FS::Conf;
1784 # Transactionize this whole mess
1785 local $SIG{HUP} = 'IGNORE';
1786 local $SIG{INT} = 'IGNORE';
1787 local $SIG{QUIT} = 'IGNORE';
1788 local $SIG{TERM} = 'IGNORE';
1789 local $SIG{TSTP} = 'IGNORE';
1790 local $SIG{PIPE} = 'IGNORE';
1792 my $oldAutoCommit = $FS::UID::AutoCommit;
1793 local $FS::UID::AutoCommit = 0;
1797 my $cust_main = qsearchs('cust_main', { custnum => $custnum });
1798 return "Customer not found: $custnum" unless $cust_main;
1800 my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
1803 my $change = scalar(@old_cust_pkg) != 0;
1806 if ( scalar(@old_cust_pkg) == 1 && scalar(@$pkgparts) == 1 ) {
1810 #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( last_bill bill );
1812 #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( setup );
1813 $hash{'setup'} = $time if $old_cust_pkg[0]->setup;
1815 $hash{'change_date'} = $time;
1816 $hash{"change_$_"} = $old_cust_pkg[0]->$_() foreach qw( pkgnum pkgpart );
1819 # Create the new packages.
1820 foreach my $pkgpart (@$pkgparts) {
1821 my $cust_pkg = new FS::cust_pkg { custnum => $custnum,
1822 pkgpart => $pkgpart,
1825 $error = $cust_pkg->insert( 'change' => $change );
1827 $dbh->rollback if $oldAutoCommit;
1830 push @$return_cust_pkg, $cust_pkg;
1832 # $return_cust_pkg now contains refs to all of the newly
1835 # Transfer services and cancel old packages.
1836 foreach my $old_pkg (@old_cust_pkg) {
1838 foreach my $new_pkg (@$return_cust_pkg) {
1839 $error = $old_pkg->transfer($new_pkg);
1840 if ($error and $error == 0) {
1841 # $old_pkg->transfer failed.
1842 $dbh->rollback if $oldAutoCommit;
1847 if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
1848 warn "trying transfer again with change_svcpart option\n" if $DEBUG;
1849 foreach my $new_pkg (@$return_cust_pkg) {
1850 $error = $old_pkg->transfer($new_pkg, 'change_svcpart'=>1 );
1851 if ($error and $error == 0) {
1852 # $old_pkg->transfer failed.
1853 $dbh->rollback if $oldAutoCommit;
1860 # Transfers were successful, but we went through all of the
1861 # new packages and still had services left on the old package.
1862 # We can't cancel the package under the circumstances, so abort.
1863 $dbh->rollback if $oldAutoCommit;
1864 return "Unable to transfer all services from package ".$old_pkg->pkgnum;
1866 $error = $old_pkg->cancel( quiet=>1 );
1872 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1878 Associates this package with a (suspension or cancellation) reason (see
1879 L<FS::cust_pkg_reason>, possibly inserting a new reason on the fly (see
1882 Available options are:
1886 =item reason - can be set to a cancellation reason (see L<FS:reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason. The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
1892 If there is an error, returns the error, otherwise returns false.
1896 =item bulk_change PKGPARTS_ARYREF, REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ]
1898 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
1899 L<FS::part_pkg>) to order for this customer. Duplicates are of course
1902 REMOVE_PKGNUMS is an list of pkgnums specifying the billing items to
1903 replace. The services (see L<FS::cust_svc>) are moved to the
1904 new billing items. An error is returned if this is not possible (see
1907 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
1908 newly-created cust_pkg objects.
1913 my ($pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
1915 # Transactionize this whole mess
1916 local $SIG{HUP} = 'IGNORE';
1917 local $SIG{INT} = 'IGNORE';
1918 local $SIG{QUIT} = 'IGNORE';
1919 local $SIG{TERM} = 'IGNORE';
1920 local $SIG{TSTP} = 'IGNORE';
1921 local $SIG{PIPE} = 'IGNORE';
1923 my $oldAutoCommit = $FS::UID::AutoCommit;
1924 local $FS::UID::AutoCommit = 0;
1928 my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
1931 while(scalar(@old_cust_pkg)) {
1933 my $custnum = $old_cust_pkg[0]->custnum;
1934 my (@remove) = map { $_->pkgnum }
1935 grep { $_->custnum == $custnum } @old_cust_pkg;
1936 @old_cust_pkg = grep { $_->custnum != $custnum } @old_cust_pkg;
1938 my $error = order $custnum, $pkgparts, \@remove, \@return;
1940 push @errors, $error
1942 push @$return_cust_pkg, @return;
1945 if (scalar(@errors)) {
1946 $dbh->rollback if $oldAutoCommit;
1947 return join(' / ', @errors);
1950 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1955 my ($self, %options) = @_;
1957 my $otaker = $FS::CurrentUser::CurrentUser->username;
1960 if ( $options{'reason'} =~ /^(\d+)$/ ) {
1964 } elsif ( ref($options{'reason'}) ) {
1966 return 'Enter a new reason (or select an existing one)'
1967 unless $options{'reason'}->{'reason'} !~ /^\s*$/;
1969 my $reason = new FS::reason({
1970 'reason_type' => $options{'reason'}->{'typenum'},
1971 'reason' => $options{'reason'}->{'reason'},
1973 my $error = $reason->insert;
1974 return $error if $error;
1976 $reasonnum = $reason->reasonnum;
1979 return "Unparsable reason: ". $options{'reason'};
1982 my $cust_pkg_reason =
1983 new FS::cust_pkg_reason({ 'pkgnum' => $self->pkgnum,
1984 'reasonnum' => $reasonnum,
1985 'otaker' => $otaker,
1986 'date' => $options{'date'}
1991 $cust_pkg_reason->insert;
1994 =item set_usage USAGE_VALUE_HASHREF
1996 USAGE_VALUE_HASHREF is a hashref of svc_acct usage columns and the amounts
1997 to which they should be set (see L<FS::svc_acct>). Currently seconds,
1998 upbytes, downbytes, and totalbytes are appropriate keys.
2000 All svc_accts which are part of this package have their values reset.
2005 my ($self, $valueref) = @_;
2007 foreach my $cust_svc ($self->cust_svc){
2008 my $svc_x = $cust_svc->svc_x;
2009 $svc_x->set_usage($valueref)
2010 if $svc_x->can("set_usage");
2018 sub order is not OO. Perhaps it should be moved to FS::cust_main and made so?
2020 In sub order, the @pkgparts array (passed by reference) is clobbered.
2022 Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard
2023 method to pass dates to the recur_prog expression, it should do so.
2025 FS::svc_acct, FS::svc_domain, FS::svc_www, FS::svc_ip and FS::svc_forward are
2026 loaded via 'use' at compile time, rather than via 'require' in sub { setup,
2027 suspend, unsuspend, cancel } because they use %FS::UID::callback to load
2028 configuration values. Probably need a subroutine which decides what to do
2029 based on whether or not we've fetched the user yet, rather than a hash. See
2030 FS::UID and the TODO.
2032 Now that things are transactional should the check in the insert method be
2037 L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>,
2038 L<FS::pkg_svc>, schema.html from the base documentation