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;
23 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
25 # because they load configuration by setting FS::UID::callback (see TODO)
31 # for sending cancel emails in sub cancel
34 @ISA = qw( FS::cust_main_Mixin FS::option_Common FS::Record );
38 $disable_agentcheck = 0;
42 my ( $hashref, $cache ) = @_;
43 #if ( $hashref->{'pkgpart'} ) {
44 if ( $hashref->{'pkg'} ) {
45 # #@{ $self->{'_pkgnum'} } = ();
46 # my $subcache = $cache->subcache('pkgpart', 'part_pkg');
47 # $self->{'_pkgpart'} = $subcache;
48 # #push @{ $self->{'_pkgnum'} },
49 # FS::part_pkg->new_or_cached($hashref, $subcache);
50 $self->{'_pkgpart'} = FS::part_pkg->new($hashref);
52 if ( exists $hashref->{'svcnum'} ) {
53 #@{ $self->{'_pkgnum'} } = ();
54 my $subcache = $cache->subcache('svcnum', 'cust_svc', $hashref->{pkgnum});
55 $self->{'_svcnum'} = $subcache;
56 #push @{ $self->{'_pkgnum'} },
57 FS::cust_svc->new_or_cached($hashref, $subcache) if $hashref->{svcnum};
63 FS::cust_pkg - Object methods for cust_pkg objects
69 $record = new FS::cust_pkg \%hash;
70 $record = new FS::cust_pkg { 'column' => 'value' };
72 $error = $record->insert;
74 $error = $new_record->replace($old_record);
76 $error = $record->delete;
78 $error = $record->check;
80 $error = $record->cancel;
82 $error = $record->suspend;
84 $error = $record->unsuspend;
86 $part_pkg = $record->part_pkg;
88 @labels = $record->labels;
90 $seconds = $record->seconds_since($timestamp);
92 $error = FS::cust_pkg::order( $custnum, \@pkgparts );
93 $error = FS::cust_pkg::order( $custnum, \@pkgparts, \@remove_pkgnums ] );
97 An FS::cust_pkg object represents a customer billing item. FS::cust_pkg
98 inherits from FS::Record. The following fields are currently supported:
102 =item pkgnum - primary key (assigned automatically for new billing items)
104 =item custnum - Customer (see L<FS::cust_main>)
106 =item pkgpart - Billing item definition (see L<FS::part_pkg>)
110 =item bill - date (next bill date)
112 =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, adjourn, 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'; }
144 sub cust_linked { $_[0]->cust_main_custnum; }
145 sub cust_unlinked_msg {
147 "WARNING: can't find cust_main.custnum ". $self->custnum.
148 ' (cust_pkg.pkgnum '. $self->pkgnum. ')';
151 =item insert [ OPTION => VALUE ... ]
153 Adds this billing item to the database ("Orders" the item). If there is an
154 error, returns the error, otherwise returns false.
156 If the additional field I<promo_code> is defined instead of I<pkgpart>, it
157 will be used to look up the package definition and agent restrictions will be
160 The following options are available: I<change>
162 I<change>, if set true, supresses any referral credit to a referring customer.
167 my( $self, %options ) = @_;
169 local $SIG{HUP} = 'IGNORE';
170 local $SIG{INT} = 'IGNORE';
171 local $SIG{QUIT} = 'IGNORE';
172 local $SIG{TERM} = 'IGNORE';
173 local $SIG{TSTP} = 'IGNORE';
174 local $SIG{PIPE} = 'IGNORE';
176 my $oldAutoCommit = $FS::UID::AutoCommit;
177 local $FS::UID::AutoCommit = 0;
180 my $error = $self->SUPER::insert($options{options} ? %{$options{options}} : ());
182 $dbh->rollback if $oldAutoCommit;
186 #if ( $self->reg_code ) {
187 # my $reg_code = qsearchs('reg_code', { 'code' => $self->reg_code } );
188 # $error = $reg_code->delete;
190 # $dbh->rollback if $oldAutoCommit;
195 my $conf = new FS::Conf;
196 my $cust_main = $self->cust_main;
197 my $part_pkg = $self->part_pkg;
198 if ( $conf->exists('referral_credit')
199 && $cust_main->referral_custnum
200 && ! $options{'change'}
201 && $part_pkg->freq !~ /^0\D?$/
204 my $referring_cust_main = $cust_main->referring_cust_main;
205 if ( $referring_cust_main->status ne 'cancelled' ) {
207 if ( $part_pkg->freq !~ /^\d+$/ ) {
208 warn 'WARNING: Not crediting customer '. $cust_main->referral_custnum.
209 ' for package '. $self->pkgnum.
210 ' ( customer '. $self->custnum. ')'.
211 ' - One-time referral credits not (yet) available for '.
212 ' packages with '. $part_pkg->freq_pretty. ' frequency';
215 my $amount = sprintf( "%.2f", $part_pkg->base_recur / $part_pkg->freq );
217 $referring_cust_main->
219 'Referral credit for '.$cust_main->name,
220 'reason_type' => $conf->config('referral_credit_type')
223 $dbh->rollback if $oldAutoCommit;
224 return "Error crediting customer ". $cust_main->referral_custnum.
225 " for referral: $error";
233 if ($conf->config('welcome_letter') && $self->cust_main->num_pkgs == 1) {
234 my $queue = new FS::queue {
235 'job' => 'FS::cust_main::queueable_print',
237 $error = $queue->insert(
238 'custnum' => $self->custnum,
239 'template' => 'welcome_letter',
243 warn "can't send welcome letter: $error";
248 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
255 This method now works but you probably shouldn't use it.
257 You don't want to delete billing items, because there would then be no record
258 the customer ever purchased the item. Instead, see the cancel method.
263 # return "Can't delete cust_pkg records!";
266 =item replace OLD_RECORD
268 Replaces the OLD_RECORD with this one in the database. If there is an error,
269 returns the error, otherwise returns false.
271 Currently, custnum, setup, bill, adjourn, susp, expire, and cancel may be changed.
273 Changing pkgpart may have disasterous effects. See the order subroutine.
275 setup and bill are normally updated by calling the bill method of a customer
276 object (see L<FS::cust_main>).
278 suspend is normally updated by the suspend and unsuspend methods.
280 cancel is normally updated by the cancel method (and also the order subroutine
288 my( $new, $old, %options ) = @_;
290 # We absolutely have to have an old vs. new record to make this work.
291 if (!defined($old)) {
292 $old = qsearchs( 'cust_pkg', { 'pkgnum' => $new->pkgnum } );
294 #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
295 return "Can't change otaker!" if $old->otaker ne $new->otaker;
298 #return "Can't change setup once it exists!"
299 # if $old->getfield('setup') &&
300 # $old->getfield('setup') != $new->getfield('setup');
302 #some logic for bill, susp, cancel?
304 local($disable_agentcheck) = 1 if $old->pkgpart == $new->pkgpart;
306 local $SIG{HUP} = 'IGNORE';
307 local $SIG{INT} = 'IGNORE';
308 local $SIG{QUIT} = 'IGNORE';
309 local $SIG{TERM} = 'IGNORE';
310 local $SIG{TSTP} = 'IGNORE';
311 local $SIG{PIPE} = 'IGNORE';
313 my $oldAutoCommit = $FS::UID::AutoCommit;
314 local $FS::UID::AutoCommit = 0;
317 foreach my $method ( qw(adjourn expire) ) { # How many reasons?
318 if ($options{'reason'} && $new->$method && $old->$method ne $new->$method) {
319 my $error = $new->insert_reason( 'reason' => $options{'reason'},
320 'date' => $new->$method,
323 dbh->rollback if $oldAutoCommit;
324 return "Error inserting cust_pkg_reason: $error";
329 #save off and freeze RADIUS attributes for any associated svc_acct records
331 if ( $old->part_pkg->is_prepaid || $new->part_pkg->is_prepaid ) {
333 #also check for specific exports?
334 # to avoid spurious modify export events
335 @svc_acct = map { $_->svc_x }
336 grep { $_->part_svc->svcdb eq 'svc_acct' }
339 $_->snapshot foreach @svc_acct;
343 my $error = $new->SUPER::replace($old,
344 $options{options} ? ${options{options}} : ()
347 $dbh->rollback if $oldAutoCommit;
351 #for prepaid packages,
352 #trigger export of new RADIUS Expiration attribute when cust_pkg.bill changes
353 foreach my $old_svc_acct ( @svc_acct ) {
354 my $new_svc_acct = new FS::svc_acct { $old_svc_acct->hash };
355 my $s_error = $new_svc_acct->replace($old_svc_acct);
357 $dbh->rollback if $oldAutoCommit;
362 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
369 Checks all fields to make sure this is a valid billing item. If there is an
370 error, returns the error, otherwise returns false. Called by the insert and
379 $self->ut_numbern('pkgnum')
380 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
381 || $self->ut_numbern('pkgpart')
382 || $self->ut_numbern('setup')
383 || $self->ut_numbern('bill')
384 || $self->ut_numbern('susp')
385 || $self->ut_numbern('cancel')
386 || $self->ut_numbern('adjourn')
387 || $self->ut_numbern('expire')
389 return $error if $error;
391 if ( $self->reg_code ) {
393 unless ( grep { $self->pkgpart == $_->pkgpart }
394 map { $_->reg_code_pkg }
395 qsearchs( 'reg_code', { 'code' => $self->reg_code,
396 'agentnum' => $self->cust_main->agentnum })
398 return "Unknown registration code";
401 } elsif ( $self->promo_code ) {
404 qsearchs('part_pkg', {
405 'pkgpart' => $self->pkgpart,
406 'promo_code' => { op=>'ILIKE', value=>$self->promo_code },
408 return 'Unknown promotional code' unless $promo_part_pkg;
412 unless ( $disable_agentcheck ) {
414 qsearchs( 'agent', { 'agentnum' => $self->cust_main->agentnum } );
415 my $pkgpart_href = $agent->pkgpart_hashref;
416 return "agent ". $agent->agentnum.
417 " can't purchase pkgpart ". $self->pkgpart
418 unless $pkgpart_href->{ $self->pkgpart };
421 $error = $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' );
422 return $error if $error;
426 $self->otaker(getotaker) unless $self->otaker;
427 $self->otaker =~ /^(\w{1,32})$/ or return "Illegal otaker";
430 if ( $self->dbdef_table->column('manual_flag') ) {
431 $self->manual_flag('') if $self->manual_flag eq ' ';
432 $self->manual_flag =~ /^([01]?)$/
433 or return "Illegal manual_flag ". $self->manual_flag;
434 $self->manual_flag($1);
440 =item cancel [ OPTION => VALUE ... ]
442 Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
443 in this package, then cancels the package itself (sets the cancel field to
446 Available options are: I<quiet>
448 I<quiet> can be set true to supress email cancellation notices.
450 If there is an error, returns the error, otherwise returns false.
455 my( $self, %options ) = @_;
458 local $SIG{HUP} = 'IGNORE';
459 local $SIG{INT} = 'IGNORE';
460 local $SIG{QUIT} = 'IGNORE';
461 local $SIG{TERM} = 'IGNORE';
462 local $SIG{TSTP} = 'IGNORE';
463 local $SIG{PIPE} = 'IGNORE';
465 my $oldAutoCommit = $FS::UID::AutoCommit;
466 local $FS::UID::AutoCommit = 0;
469 if ($options{'reason'}) {
470 $error = $self->insert_reason( 'reason' => $options{'reason'} );
472 dbh->rollback if $oldAutoCommit;
473 return "Error inserting cust_pkg_reason: $error";
478 foreach my $cust_svc (
481 sort { $a->[1] <=> $b->[1] }
482 map { [ $_, $_->svc_x->table_info->{'cancel_weight'} ]; }
483 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
486 my $error = $cust_svc->cancel;
489 $dbh->rollback if $oldAutoCommit;
490 return "Error cancelling cust_svc: $error";
494 # Add a credit for remaining service
495 my $remaining_value = $self->calc_remain();
496 if ( $remaining_value > 0 ) {
497 my $conf = new FS::Conf;
498 my $error = $self->cust_main->credit(
500 'Credit for unused time on '. $self->part_pkg->pkg,
501 'reason_type' => $conf->config('cancel_credit_type'),
504 $dbh->rollback if $oldAutoCommit;
505 return "Error crediting customer \$$remaining_value for unused time on".
506 $self->part_pkg->pkg. ": $error";
510 unless ( $self->getfield('cancel') ) {
511 my %hash = $self->hash;
512 $hash{'cancel'} = time;
513 my $new = new FS::cust_pkg ( \%hash );
514 $error = $new->replace( $self, options => { $self->options } );
516 $dbh->rollback if $oldAutoCommit;
521 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
523 my $conf = new FS::Conf;
524 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list;
525 if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) {
526 my $conf = new FS::Conf;
527 my $error = send_email(
528 'from' => $conf->config('invoice_from'),
529 'to' => \@invoicing_list,
530 'subject' => ( $conf->config('cancelsubject') || 'Cancellation Notice' ),
531 'body' => [ map "$_\n", $conf->config('cancelmessage') ],
533 #should this do something on errors?
542 Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
543 package, then suspends the package itself (sets the susp field to now).
545 If there is an error, returns the error, otherwise returns false.
550 my( $self, %options ) = @_;
553 local $SIG{HUP} = 'IGNORE';
554 local $SIG{INT} = 'IGNORE';
555 local $SIG{QUIT} = 'IGNORE';
556 local $SIG{TERM} = 'IGNORE';
557 local $SIG{TSTP} = 'IGNORE';
558 local $SIG{PIPE} = 'IGNORE';
560 my $oldAutoCommit = $FS::UID::AutoCommit;
561 local $FS::UID::AutoCommit = 0;
564 if ($options{'reason'}) {
565 $error = $self->insert_reason( 'reason' => $options{'reason'} );
567 dbh->rollback if $oldAutoCommit;
568 return "Error inserting cust_pkg_reason: $error";
572 foreach my $cust_svc (
573 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
575 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
577 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
578 $dbh->rollback if $oldAutoCommit;
579 return "Illegal svcdb value in part_svc!";
582 require "FS/$svcdb.pm";
584 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
586 $error = $svc->suspend;
588 $dbh->rollback if $oldAutoCommit;
595 unless ( $self->getfield('susp') ) {
596 my %hash = $self->hash;
597 $hash{'susp'} = time;
598 my $new = new FS::cust_pkg ( \%hash );
599 $error = $new->replace( $self, options => { $self->options } );
601 $dbh->rollback if $oldAutoCommit;
606 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
611 =item unsuspend [ OPTION => VALUE ... ]
613 Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
614 package, then unsuspends the package itself (clears the susp field and the
615 adjourn field if it is in the past).
617 Available options are: I<adjust_next_bill>.
619 I<adjust_next_bill> can be set true to adjust the next bill date forward by
620 the amount of time the account was inactive. This was set true by default
621 since 1.4.2 and 1.5.0pre6; however, starting with 1.7.0 this needs to be
622 explicitly requested. Price plans for which this makes sense (anniversary-date
623 based than prorate or subscription) could have an option to enable this
626 If there is an error, returns the error, otherwise returns false.
631 my( $self, %opt ) = @_;
634 local $SIG{HUP} = 'IGNORE';
635 local $SIG{INT} = 'IGNORE';
636 local $SIG{QUIT} = 'IGNORE';
637 local $SIG{TERM} = 'IGNORE';
638 local $SIG{TSTP} = 'IGNORE';
639 local $SIG{PIPE} = 'IGNORE';
641 my $oldAutoCommit = $FS::UID::AutoCommit;
642 local $FS::UID::AutoCommit = 0;
645 foreach my $cust_svc (
646 qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
648 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
650 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
651 $dbh->rollback if $oldAutoCommit;
652 return "Illegal svcdb value in part_svc!";
655 require "FS/$svcdb.pm";
657 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
659 $error = $svc->unsuspend;
661 $dbh->rollback if $oldAutoCommit;
668 unless ( ! $self->getfield('susp') ) {
669 my %hash = $self->hash;
670 my $inactive = time - $hash{'susp'};
672 my $conf = new FS::Conf;
674 $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive
675 if ( $opt{'adjust_next_bill'}
676 || $conf->config('unsuspend-always_adjust_next_bill_date') )
677 && $inactive > 0 && ( $hash{'bill'} || $hash{'setup'} );
680 $hash{'adjourn'} = '' if $hash{'adjourn'} < time;
681 my $new = new FS::cust_pkg ( \%hash );
682 $error = $new->replace( $self, options => { $self->options } );
684 $dbh->rollback if $oldAutoCommit;
689 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
696 Returns the last bill date, or if there is no last bill date, the setup date.
697 Useful for billing metered services.
703 if ( $self->dbdef_table->column('last_bill') ) {
704 return $self->setfield('last_bill', $_[0]) if @_;
705 return $self->getfield('last_bill') if $self->getfield('last_bill');
707 my $cust_bill_pkg = qsearchs('cust_bill_pkg', { 'pkgnum' => $self->pkgnum,
708 'edate' => $self->bill, } );
709 $cust_bill_pkg ? $cust_bill_pkg->sdate : $self->setup || 0;
714 Returns the most recent FS::reason associated with the package.
720 my $cust_pkg_reason = qsearchs( {
721 'table' => 'cust_pkg_reason',
722 'hashref' => { 'pkgnum' => $self->pkgnum, },
723 'extra_sql'=> 'ORDER BY date DESC LIMIT 1',
725 qsearchs ( 'reason', { 'reasonnum' => $cust_pkg_reason->reasonnum } )
731 Returns the definition for this billing item, as an FS::part_pkg object (see
738 #exists( $self->{'_pkgpart'} )
740 ? $self->{'_pkgpart'}
741 : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
746 Returns the cancelled package this package was changed from, if any.
752 return '' unless $self->change_pkgnum;
753 qsearchs('cust_pkg', { 'pkgnum' => $self->change_pkgnum } );
758 Calls the I<calc_setup> of the FS::part_pkg object associated with this billing
765 $self->part_pkg->calc_setup($self, @_);
770 Calls the I<calc_recur> of the FS::part_pkg object associated with this billing
777 $self->part_pkg->calc_recur($self, @_);
782 Calls the I<calc_remain> of the FS::part_pkg object associated with this
789 $self->part_pkg->calc_remain($self, @_);
794 Calls the I<calc_cancel> of the FS::part_pkg object associated with this
801 $self->part_pkg->calc_cancel($self, @_);
806 Returns any invoice line items for this package (see L<FS::cust_bill_pkg>).
812 qsearch( 'cust_bill_pkg', { 'pkgnum' => $self->pkgnum } );
815 =item cust_svc [ SVCPART ]
817 Returns the services for this package, as FS::cust_svc objects (see
818 L<FS::cust_svc>). If a svcpart is specified, return only the matching
827 return qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum,
828 'svcpart' => shift, } );
831 #if ( $self->{'_svcnum'} ) {
832 # values %{ $self->{'_svcnum'}->cache };
834 $self->_sort_cust_svc(
835 [ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ]
841 =item overlimit [ SVCPART ]
843 Returns the services for this package which have exceeded their
844 usage limit as FS::cust_svc objects (see L<FS::cust_svc>). If a svcpart
845 is specified, return only the matching services.
851 grep { $_->overlimit } $self->cust_svc;
854 =item h_cust_svc END_TIMESTAMP [ START_TIMESTAMP ]
856 Returns historical services for this package created before END TIMESTAMP and
857 (optionally) not cancelled before START_TIMESTAMP, as FS::h_cust_svc objects
858 (see L<FS::h_cust_svc>).
865 $self->_sort_cust_svc(
866 [ qsearch( 'h_cust_svc',
867 { 'pkgnum' => $self->pkgnum, },
868 FS::h_cust_svc->sql_h_search(@_),
875 my( $self, $arrayref ) = @_;
878 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
880 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
881 'svcpart' => $_->svcpart } );
883 $pkg_svc ? $pkg_svc->primary_svc : '',
884 $pkg_svc ? $pkg_svc->quantity : 0,
891 =item num_cust_svc [ SVCPART ]
893 Returns the number of provisioned services for this package. If a svcpart is
894 specified, counts only the matching services.
900 my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?';
901 $sql .= ' AND svcpart = ?' if @_;
902 my $sth = dbh->prepare($sql) or die dbh->errstr;
903 $sth->execute($self->pkgnum, @_) or die $sth->errstr;
904 $sth->fetchrow_arrayref->[0];
907 =item available_part_svc
909 Returns a list of FS::part_svc objects representing services included in this
910 package but not yet provisioned. Each FS::part_svc object also has an extra
911 field, I<num_avail>, which specifies the number of available services.
915 sub available_part_svc {
917 grep { $_->num_avail > 0 }
919 my $part_svc = $_->part_svc;
920 $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking
921 $_->quantity - $self->num_cust_svc($_->svcpart);
924 $self->part_pkg->pkg_svc;
929 Returns a list of FS::part_svc objects representing provisioned and available
930 services included in this package. Each FS::part_svc object also has the
931 following extra fields:
935 =item num_cust_svc (count)
937 =item num_avail (quantity - count)
939 =item cust_pkg_svc (services) - array reference containing the provisioned services, as cust_svc objects
942 label -> ($cust_svc->label)[1]
951 #XXX some sort of sort order besides numeric by svcpart...
952 my @part_svc = sort { $a->svcpart <=> $b->svcpart } map {
954 my $part_svc = $pkg_svc->part_svc;
955 my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
956 $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #more evil
957 $part_svc->{'Hash'}{'num_avail'} =
958 max( 0, $pkg_svc->quantity - $num_cust_svc );
959 $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
961 } $self->part_pkg->pkg_svc;
964 push @part_svc, map {
966 my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
967 $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #speak no evail
968 $part_svc->{'Hash'}{'num_avail'} = 0; #0-$num_cust_svc ?
969 $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
971 } $self->extra_part_svc;
979 Returns a list of FS::part_svc objects corresponding to services in this
980 package which are still provisioned but not (any longer) available in the
988 my $pkgnum = $self->pkgnum;
989 my $pkgpart = $self->pkgpart;
992 'table' => 'part_svc',
994 'extra_sql' => "WHERE 0 = ( SELECT COUNT(*) FROM pkg_svc
995 WHERE pkg_svc.svcpart = part_svc.svcpart
996 AND pkg_svc.pkgpart = $pkgpart
999 AND 0 < ( SELECT count(*)
1001 LEFT JOIN cust_pkg using ( pkgnum )
1002 WHERE cust_svc.svcpart = part_svc.svcpart
1003 AND pkgnum = $pkgnum
1010 Returns a short status string for this package, currently:
1014 =item not yet billed
1016 =item one-time charge
1031 my $freq = length($self->freq) ? $self->freq : $self->part_pkg->freq;
1033 return 'cancelled' if $self->get('cancel');
1034 return 'suspended' if $self->susp;
1035 return 'not yet billed' unless $self->setup;
1036 return 'one-time charge' if $freq =~ /^(0|$)/;
1042 Class method that returns the list of possible status strings for pacakges
1043 (see L<the status method|/status>). For example:
1045 @statuses = FS::cust_pkg->statuses();
1049 tie my %statuscolor, 'Tie::IxHash',
1050 'not yet billed' => '000000',
1051 'one-time charge' => '000000',
1052 'active' => '00CC00',
1053 'suspended' => 'FF9900',
1054 'cancelled' => 'FF0000',
1058 my $self = shift; #could be class...
1059 grep { $_ !~ /^(not yet billed)$/ } #this is a dumb status anyway
1060 # mayble split btw one-time vs. recur
1066 Returns a hex triplet color string for this package's status.
1072 $statuscolor{$self->status};
1077 Returns a list of lists, calling the label method for all services
1078 (see L<FS::cust_svc>) of this billing item.
1084 map { [ $_->label ] } $self->cust_svc;
1087 =item h_labels END_TIMESTAMP [ START_TIMESTAMP ]
1089 Like the labels method, but returns historical information on services that
1090 were active as of END_TIMESTAMP and (optionally) not cancelled before
1093 Returns a list of lists, calling the label method for all (historical) services
1094 (see L<FS::h_cust_svc>) of this billing item.
1100 map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
1103 =item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
1105 Like h_labels, except returns a simple flat list, and shortens long
1106 (currently >5) lists of identical services to one line that lists the service
1107 label and the number of individual services rather than individual items.
1111 sub h_labels_short {
1115 #tie %labels, 'Tie::IxHash';
1116 push @{ $labels{$_->[0]} }, $_->[1]
1117 foreach $self->h_labels(@_);
1119 foreach my $label ( keys %labels ) {
1120 my @values = @{ $labels{$label} };
1121 my $num = scalar(@values);
1123 push @labels, "$label ($num)";
1125 push @labels, map { "$label: $_" } @values;
1135 Returns the parent customer object (see L<FS::cust_main>).
1141 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
1144 =item seconds_since TIMESTAMP
1146 Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
1147 package have been online since TIMESTAMP, according to the session monitor.
1149 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1150 L<Time::Local> and L<Date::Parse> for conversion functions.
1155 my($self, $since) = @_;
1158 foreach my $cust_svc (
1159 grep { $_->part_svc->svcdb eq 'svc_acct' } $self->cust_svc
1161 $seconds += $cust_svc->seconds_since($since);
1168 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1170 Returns the numbers of seconds all accounts (see L<FS::svc_acct>) in this
1171 package have been online between TIMESTAMP_START (inclusive) and TIMESTAMP_END
1174 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1175 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1181 sub seconds_since_sqlradacct {
1182 my($self, $start, $end) = @_;
1186 foreach my $cust_svc (
1188 my $part_svc = $_->part_svc;
1189 $part_svc->svcdb eq 'svc_acct'
1190 && scalar($part_svc->part_export('sqlradius'));
1193 $seconds += $cust_svc->seconds_since_sqlradacct($start, $end);
1200 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1202 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1203 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1207 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1208 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1213 sub attribute_since_sqlradacct {
1214 my($self, $start, $end, $attrib) = @_;
1218 foreach my $cust_svc (
1220 my $part_svc = $_->part_svc;
1221 $part_svc->svcdb eq 'svc_acct'
1222 && scalar($part_svc->part_export('sqlradius'));
1225 $sum += $cust_svc->attribute_since_sqlradacct($start, $end, $attrib);
1232 =item transfer DEST_PKGNUM | DEST_CUST_PKG, [ OPTION => VALUE ... ]
1234 Transfers as many services as possible from this package to another package.
1236 The destination package can be specified by pkgnum by passing an FS::cust_pkg
1237 object. The destination package must already exist.
1239 Services are moved only if the destination allows services with the correct
1240 I<svcpart> (not svcdb), unless the B<change_svcpart> option is set true. Use
1241 this option with caution! No provision is made for export differences
1242 between the old and new service definitions. Probably only should be used
1243 when your exports for all service definitions of a given svcdb are identical.
1244 (attempt a transfer without it first, to move all possible svcpart-matching
1247 Any services that can't be moved remain in the original package.
1249 Returns an error, if there is one; otherwise, returns the number of services
1250 that couldn't be moved.
1255 my ($self, $dest_pkgnum, %opt) = @_;
1261 if (ref ($dest_pkgnum) eq 'FS::cust_pkg') {
1262 $dest = $dest_pkgnum;
1263 $dest_pkgnum = $dest->pkgnum;
1265 $dest = qsearchs('cust_pkg', { pkgnum => $dest_pkgnum });
1268 return ('Package does not exist: '.$dest_pkgnum) unless $dest;
1270 foreach my $pkg_svc ( $dest->part_pkg->pkg_svc ) {
1271 $target{$pkg_svc->svcpart} = $pkg_svc->quantity;
1274 foreach my $cust_svc ($dest->cust_svc) {
1275 $target{$cust_svc->svcpart}--;
1278 my %svcpart2svcparts = ();
1279 if ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
1280 warn "change_svcpart option received, creating alternates list\n" if $DEBUG;
1281 foreach my $svcpart ( map { $_->svcpart } $self->cust_svc ) {
1282 next if exists $svcpart2svcparts{$svcpart};
1283 my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
1284 $svcpart2svcparts{$svcpart} = [
1286 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
1288 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $dest->pkgpart,
1289 'svcpart' => $_ } );
1291 $pkg_svc ? $pkg_svc->primary_svc : '',
1292 $pkg_svc ? $pkg_svc->quantity : 0,
1296 grep { $_ != $svcpart }
1298 qsearch('part_svc', { 'svcdb' => $part_svc->svcdb } )
1300 warn "alternates for svcpart $svcpart: ".
1301 join(', ', @{$svcpart2svcparts{$svcpart}}). "\n"
1306 foreach my $cust_svc ($self->cust_svc) {
1307 if($target{$cust_svc->svcpart} > 0) {
1308 $target{$cust_svc->svcpart}--;
1309 my $new = new FS::cust_svc { $cust_svc->hash };
1310 $new->pkgnum($dest_pkgnum);
1311 my $error = $new->replace($cust_svc);
1312 return $error if $error;
1313 } elsif ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
1315 warn "looking for alternates for svcpart ". $cust_svc->svcpart. "\n";
1316 warn "alternates to consider: ".
1317 join(', ', @{$svcpart2svcparts{$cust_svc->svcpart}}). "\n";
1319 my @alternate = grep {
1320 warn "considering alternate svcpart $_: ".
1321 "$target{$_} available in new package\n"
1324 } @{$svcpart2svcparts{$cust_svc->svcpart}};
1326 warn "alternate(s) found\n" if $DEBUG;
1327 my $change_svcpart = $alternate[0];
1328 $target{$change_svcpart}--;
1329 my $new = new FS::cust_svc { $cust_svc->hash };
1330 $new->svcpart($change_svcpart);
1331 $new->pkgnum($dest_pkgnum);
1332 my $error = $new->replace($cust_svc);
1333 return $error if $error;
1346 This method is deprecated. See the I<depend_jobnum> option to the insert and
1347 order_pkgs methods in FS::cust_main for a better way to defer provisioning.
1354 local $SIG{HUP} = 'IGNORE';
1355 local $SIG{INT} = 'IGNORE';
1356 local $SIG{QUIT} = 'IGNORE';
1357 local $SIG{TERM} = 'IGNORE';
1358 local $SIG{TSTP} = 'IGNORE';
1359 local $SIG{PIPE} = 'IGNORE';
1361 my $oldAutoCommit = $FS::UID::AutoCommit;
1362 local $FS::UID::AutoCommit = 0;
1365 foreach my $cust_svc ( $self->cust_svc ) {
1366 #false laziness w/svc_Common::insert
1367 my $svc_x = $cust_svc->svc_x;
1368 foreach my $part_export ( $cust_svc->part_svc->part_export ) {
1369 my $error = $part_export->export_insert($svc_x);
1371 $dbh->rollback if $oldAutoCommit;
1377 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1384 =head1 CLASS METHODS
1390 Returns an SQL expression identifying recurring packages.
1394 sub recurring_sql { "
1395 '0' != ( select freq from part_pkg
1396 where cust_pkg.pkgpart = part_pkg.pkgpart )
1401 Returns an SQL expression identifying one-time packages.
1406 '0' = ( select freq from part_pkg
1407 where cust_pkg.pkgpart = part_pkg.pkgpart )
1412 Returns an SQL expression identifying active packages.
1417 ". $_[0]->recurring_sql(). "
1418 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1419 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
1424 Returns an SQL expression identifying inactive packages (one-time packages
1425 that are otherwise unsuspended/uncancelled).
1429 sub inactive_sql { "
1430 ". $_[0]->onetime_sql(). "
1431 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1432 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
1438 Returns an SQL expression identifying suspended packages.
1442 sub suspended_sql { susp_sql(@_); }
1444 #$_[0]->recurring_sql(). ' AND '.
1446 ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1447 AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0
1454 Returns an SQL exprression identifying cancelled packages.
1458 sub cancelled_sql { cancel_sql(@_); }
1460 #$_[0]->recurring_sql(). ' AND '.
1461 "cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0";
1464 =item search_sql HREF
1466 Returns a qsearch hash expression to search for parameters specified in HREF.
1467 Valid parameters are
1471 =item magic - /^(active|inactive|suspended|cancell?ed)$/
1472 =item status - /^(active|inactive|suspended|one-time charge|inactive|cancell?ed)$/
1474 =item pkgpart - list specified how?
1475 =item setup - arrayref of beginning and ending epoch date
1476 =item last_bill - arrayref of beginning and ending epoch date
1477 =item bill - arrayref of beginning and ending epoch date
1478 =item adjourn - arrayref of beginning and ending epoch date
1479 =item susp - arrayref of beginning and ending epoch date
1480 =item expire - arrayref of beginning and ending epoch date
1481 =item cancel - arrayref of beginning and ending epoch date
1482 =item query - /^(pkgnum/APKG_pkgnum)$/
1483 =item cust_fields - a value suited to passing to FS::UI::Web::cust_header
1484 =item CurrentUser - specifies the user for agent virtualization
1490 my ($class, $params) = @_;
1497 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
1506 if ( $params->{'magic'} eq 'active'
1507 || $params->{'status'} eq 'active' ) {
1509 push @where, FS::cust_pkg->active_sql();
1511 } elsif ( $params->{'magic'} eq 'inactive'
1512 || $params->{'status'} eq 'inactive' ) {
1514 push @where, FS::cust_pkg->inactive_sql();
1516 } elsif ( $params->{'magic'} eq 'suspended'
1517 || $params->{'status'} eq 'suspended' ) {
1519 push @where, FS::cust_pkg->suspended_sql();
1521 } elsif ( $params->{'magic'} =~ /^cancell?ed$/
1522 || $params->{'status'} =~ /^cancell?ed$/ ) {
1524 push @where, FS::cust_pkg->cancelled_sql();
1526 } elsif ( $params->{'status'} =~ /^(one-time charge|inactive)$/ ) {
1528 push @where, FS::cust_pkg->inactive_sql();
1533 # parse package class
1536 #false lazinessish w/graph/cust_bill_pkg.cgi
1539 if ( exists($params->{'classnum'})
1540 && $params->{'classnum'} =~ /^(\d*)$/
1544 if ( $classnum ) { #a specific class
1545 push @where, "classnum = $classnum";
1547 #@pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) );
1548 #die "classnum $classnum not found!" unless $pkg_class[0];
1549 #$title .= $pkg_class[0]->classname.' ';
1551 } elsif ( $classnum eq '' ) { #the empty class
1553 push @where, "classnum IS NULL";
1554 #$title .= 'Empty class ';
1555 #@pkg_class = ( '(empty class)' );
1556 } elsif ( $classnum eq '0' ) {
1557 #@pkg_class = qsearch('pkg_class', {} ); # { 'disabled' => '' } );
1558 #push @pkg_class, '(empty class)';
1560 die "illegal classnum";
1569 my $pkgpart = join (' OR pkgpart=',
1570 grep {$_} map { /^(\d+)$/; } ($params->{'pkgpart'}));
1571 push @where, '(pkgpart=' . $pkgpart . ')' if $pkgpart;
1579 #false laziness w/report_cust_pkg.html
1582 'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
1583 'active' => { 'susp'=>1, 'cancel'=>1 },
1584 'suspended' => { 'cancel' => 1 },
1589 foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
1591 next unless exists($params->{$field});
1593 my($beginning, $ending) = @{$params->{$field}};
1595 next if $beginning == 0 && $ending == 4294967295;
1598 "cust_pkg.$field IS NOT NULL",
1599 "cust_pkg.$field >= $beginning",
1600 "cust_pkg.$field <= $ending";
1602 $orderby ||= "ORDER BY cust_pkg.$field";
1606 $orderby ||= 'ORDER BY bill';
1609 # parse magic, legacy, etc.
1612 if ( $params->{'magic'} &&
1613 $params->{'magic'} =~ /^(active|inactive|suspended|cancell?ed)$/
1616 $orderby = 'ORDER BY pkgnum';
1618 if ( $params->{'pkgpart'} =~ /^(\d+)$/ ) {
1619 push @where, "pkgpart = $1";
1622 } elsif ( $params->{'query'} eq 'pkgnum' ) {
1624 $orderby = 'ORDER BY pkgnum';
1626 } elsif ( $params->{'query'} eq 'APKG_pkgnum' ) {
1628 $orderby = 'ORDER BY pkgnum';
1631 SELECT count(*) FROM pkg_svc
1632 WHERE pkg_svc.pkgpart = cust_pkg.pkgpart
1633 AND pkg_svc.quantity > ( SELECT count(*) FROM cust_svc
1634 WHERE cust_svc.pkgnum = cust_pkg.pkgnum
1635 AND cust_svc.svcpart = pkg_svc.svcpart
1642 # setup queries, links, subs, etc. for the search
1645 # here is the agent virtualization
1646 if ($params->{CurrentUser}) {
1648 qsearchs('access_user', { username => $params->{CurrentUser} });
1651 push @where, $access_user->agentnums_sql;
1656 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
1659 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
1661 my $addl_from = 'LEFT JOIN cust_main USING ( custnum ) '.
1662 'LEFT JOIN part_pkg USING ( pkgpart ) '.
1663 'LEFT JOIN pkg_class USING ( classnum ) ';
1665 my $count_query = "SELECT COUNT(*) FROM cust_pkg $addl_from $extra_sql";
1668 'table' => 'cust_pkg',
1670 'select' => join(', ',
1672 ( map "part_pkg.$_", qw( pkg freq ) ),
1673 'pkg_class.classname',
1674 'cust_main.custnum as cust_main_custnum',
1675 FS::UI::Web::cust_sql_fields(
1676 $params->{'cust_fields'}
1679 'extra_sql' => "$extra_sql $orderby",
1680 'addl_from' => $addl_from,
1681 'count_query' => $count_query,
1690 =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ] ]
1692 CUSTNUM is a customer (see L<FS::cust_main>)
1694 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
1695 L<FS::part_pkg>) to order for this customer. Duplicates are of course
1698 REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
1699 remove for this customer. The services (see L<FS::cust_svc>) are moved to the
1700 new billing items. An error is returned if this is not possible (see
1701 L<FS::pkg_svc>). An empty arrayref is equivalent to not specifying this
1704 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
1705 newly-created cust_pkg objects.
1710 my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
1712 my $conf = new FS::Conf;
1714 # Transactionize this whole mess
1715 local $SIG{HUP} = 'IGNORE';
1716 local $SIG{INT} = 'IGNORE';
1717 local $SIG{QUIT} = 'IGNORE';
1718 local $SIG{TERM} = 'IGNORE';
1719 local $SIG{TSTP} = 'IGNORE';
1720 local $SIG{PIPE} = 'IGNORE';
1722 my $oldAutoCommit = $FS::UID::AutoCommit;
1723 local $FS::UID::AutoCommit = 0;
1727 my $cust_main = qsearchs('cust_main', { custnum => $custnum });
1728 return "Customer not found: $custnum" unless $cust_main;
1730 my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
1733 my $change = scalar(@old_cust_pkg) != 0;
1736 if ( scalar(@old_cust_pkg) == 1 && scalar(@$pkgparts) == 1 ) {
1740 #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( last_bill bill );
1742 #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( setup );
1743 $hash{'setup'} = $time if $old_cust_pkg[0]->setup;
1745 $hash{'change_date'} = $time;
1746 $hash{"change_$_"} = $old_cust_pkg[0]->$_() foreach qw( pkgnum pkgpart );
1749 # Create the new packages.
1750 foreach my $pkgpart (@$pkgparts) {
1751 my $cust_pkg = new FS::cust_pkg { custnum => $custnum,
1752 pkgpart => $pkgpart,
1755 $error = $cust_pkg->insert( 'change' => $change );
1757 $dbh->rollback if $oldAutoCommit;
1760 push @$return_cust_pkg, $cust_pkg;
1762 # $return_cust_pkg now contains refs to all of the newly
1765 # Transfer services and cancel old packages.
1766 foreach my $old_pkg (@old_cust_pkg) {
1768 foreach my $new_pkg (@$return_cust_pkg) {
1769 $error = $old_pkg->transfer($new_pkg);
1770 if ($error and $error == 0) {
1771 # $old_pkg->transfer failed.
1772 $dbh->rollback if $oldAutoCommit;
1777 if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
1778 warn "trying transfer again with change_svcpart option\n" if $DEBUG;
1779 foreach my $new_pkg (@$return_cust_pkg) {
1780 $error = $old_pkg->transfer($new_pkg, 'change_svcpart'=>1 );
1781 if ($error and $error == 0) {
1782 # $old_pkg->transfer failed.
1783 $dbh->rollback if $oldAutoCommit;
1790 # Transfers were successful, but we went through all of the
1791 # new packages and still had services left on the old package.
1792 # We can't cancel the package under the circumstances, so abort.
1793 $dbh->rollback if $oldAutoCommit;
1794 return "Unable to transfer all services from package ".$old_pkg->pkgnum;
1796 $error = $old_pkg->cancel( quiet=>1 );
1802 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1806 =item bulk_change PKGPARTS_ARYREF, REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ]
1808 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
1809 L<FS::part_pkg>) to order for this customer. Duplicates are of course
1812 REMOVE_PKGNUMS is an list of pkgnums specifying the billing items to
1813 replace. The services (see L<FS::cust_svc>) are moved to the
1814 new billing items. An error is returned if this is not possible (see
1817 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
1818 newly-created cust_pkg objects.
1823 my ($pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
1825 # Transactionize this whole mess
1826 local $SIG{HUP} = 'IGNORE';
1827 local $SIG{INT} = 'IGNORE';
1828 local $SIG{QUIT} = 'IGNORE';
1829 local $SIG{TERM} = 'IGNORE';
1830 local $SIG{TSTP} = 'IGNORE';
1831 local $SIG{PIPE} = 'IGNORE';
1833 my $oldAutoCommit = $FS::UID::AutoCommit;
1834 local $FS::UID::AutoCommit = 0;
1838 my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
1841 while(scalar(@old_cust_pkg)) {
1843 my $custnum = $old_cust_pkg[0]->custnum;
1844 my (@remove) = map { $_->pkgnum }
1845 grep { $_->custnum == $custnum } @old_cust_pkg;
1846 @old_cust_pkg = grep { $_->custnum != $custnum } @old_cust_pkg;
1848 my $error = order $custnum, $pkgparts, \@remove, \@return;
1850 push @errors, $error
1852 push @$return_cust_pkg, @return;
1855 if (scalar(@errors)) {
1856 $dbh->rollback if $oldAutoCommit;
1857 return join(' / ', @errors);
1860 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1865 my ($self, %options) = @_;
1867 my $otaker = $FS::CurrentUser::CurrentUser->username;
1869 my $cust_pkg_reason =
1870 new FS::cust_pkg_reason({ 'pkgnum' => $self->pkgnum,
1871 'reasonnum' => $options{'reason'},
1872 'otaker' => $otaker,
1873 'date' => $options{'date'}
1877 return $cust_pkg_reason->insert;
1880 =item set_usage USAGE_VALUE_HASHREF
1882 USAGE_VALUE_HASHREF is a hashref of svc_acct usage columns and the amounts
1883 to which they should be set (see L<FS::svc_acct>). Currently seconds,
1884 upbytes, downbytes, and totalbytes are appropriate keys.
1886 All svc_accts which are part of this package have their values reset.
1891 my ($self, $valueref) = @_;
1893 foreach my $cust_svc ($self->cust_svc){
1894 my $svc_x = $cust_svc->svc_x;
1895 $svc_x->set_usage($valueref)
1896 if $svc_x->can("set_usage");
1904 sub order is not OO. Perhaps it should be moved to FS::cust_main and made so?
1906 In sub order, the @pkgparts array (passed by reference) is clobbered.
1908 Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard
1909 method to pass dates to the recur_prog expression, it should do so.
1911 FS::svc_acct, FS::svc_domain, FS::svc_www, FS::svc_ip and FS::svc_forward are
1912 loaded via 'use' at compile time, rather than via 'require' in sub { setup,
1913 suspend, unsuspend, cancel } because they use %FS::UID::callback to load
1914 configuration values. Probably need a subroutine which decides what to do
1915 based on whether or not we've fetched the user yet, rather than a hash. See
1916 FS::UID and the TODO.
1918 Now that things are transactional should the check in the insert method be
1923 L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>,
1924 L<FS::pkg_svc>, schema.html from the base documentation