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.
130 Note: setup, bill, adjourn, susp, expire and cancel are specified as UNIX timestamps;
131 see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for
132 conversion functions.
140 Create a new billing item. To add the item to the database, see L<"insert">.
144 sub table { 'cust_pkg'; }
145 sub cust_linked { $_[0]->cust_main_custnum; }
146 sub cust_unlinked_msg {
148 "WARNING: can't find cust_main.custnum ". $self->custnum.
149 ' (cust_pkg.pkgnum '. $self->pkgnum. ')';
152 =item insert [ OPTION => VALUE ... ]
154 Adds this billing item to the database ("Orders" the item). If there is an
155 error, returns the error, otherwise returns false.
157 If the additional field I<promo_code> is defined instead of I<pkgpart>, it
158 will be used to look up the package definition and agent restrictions will be
161 The following options are available: I<change>
163 I<change>, if set true, supresses any referral credit to a referring customer.
168 my( $self, %options ) = @_;
170 local $SIG{HUP} = 'IGNORE';
171 local $SIG{INT} = 'IGNORE';
172 local $SIG{QUIT} = 'IGNORE';
173 local $SIG{TERM} = 'IGNORE';
174 local $SIG{TSTP} = 'IGNORE';
175 local $SIG{PIPE} = 'IGNORE';
177 my $oldAutoCommit = $FS::UID::AutoCommit;
178 local $FS::UID::AutoCommit = 0;
181 my $error = $self->SUPER::insert($options{options} ? %{$options{options}} : ());
183 $dbh->rollback if $oldAutoCommit;
187 #if ( $self->reg_code ) {
188 # my $reg_code = qsearchs('reg_code', { 'code' => $self->reg_code } );
189 # $error = $reg_code->delete;
191 # $dbh->rollback if $oldAutoCommit;
196 my $conf = new FS::Conf;
197 my $cust_main = $self->cust_main;
198 my $part_pkg = $self->part_pkg;
199 if ( $conf->exists('referral_credit')
200 && $cust_main->referral_custnum
201 && ! $options{'change'}
202 && $part_pkg->freq !~ /^0\D?$/
205 my $referring_cust_main = $cust_main->referring_cust_main;
206 if ( $referring_cust_main->status ne 'cancelled' ) {
208 if ( $part_pkg->freq !~ /^\d+$/ ) {
209 warn 'WARNING: Not crediting customer '. $cust_main->referral_custnum.
210 ' for package '. $self->pkgnum.
211 ' ( customer '. $self->custnum. ')'.
212 ' - One-time referral credits not (yet) available for '.
213 ' packages with '. $part_pkg->freq_pretty. ' frequency';
216 my $amount = sprintf( "%.2f", $part_pkg->base_recur / $part_pkg->freq );
218 $referring_cust_main->
220 'Referral credit for '.$cust_main->name,
221 'reason_type' => $conf->config('referral_credit_type')
224 $dbh->rollback if $oldAutoCommit;
225 return "Error crediting customer ". $cust_main->referral_custnum.
226 " for referral: $error";
234 if ($conf->config('welcome_letter') && $self->cust_main->num_pkgs == 1) {
235 my $queue = new FS::queue {
236 'job' => 'FS::cust_main::queueable_print',
238 $error = $queue->insert(
239 'custnum' => $self->custnum,
240 'template' => 'welcome_letter',
244 warn "can't send welcome letter: $error";
249 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
256 This method now works but you probably shouldn't use it.
258 You don't want to delete billing items, because there would then be no record
259 the customer ever purchased the item. Instead, see the cancel method.
264 # return "Can't delete cust_pkg records!";
267 =item replace OLD_RECORD
269 Replaces the OLD_RECORD with this one in the database. If there is an error,
270 returns the error, otherwise returns false.
272 Currently, custnum, setup, bill, adjourn, susp, expire, and cancel may be changed.
274 Changing pkgpart may have disasterous effects. See the order subroutine.
276 setup and bill are normally updated by calling the bill method of a customer
277 object (see L<FS::cust_main>).
279 suspend is normally updated by the suspend and unsuspend methods.
281 cancel is normally updated by the cancel method (and also the order subroutine
289 my( $new, $old, %options ) = @_;
291 # We absolutely have to have an old vs. new record to make this work.
292 if (!defined($old)) {
293 $old = qsearchs( 'cust_pkg', { 'pkgnum' => $new->pkgnum } );
295 #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
296 return "Can't change otaker!" if $old->otaker ne $new->otaker;
299 #return "Can't change setup once it exists!"
300 # if $old->getfield('setup') &&
301 # $old->getfield('setup') != $new->getfield('setup');
303 #some logic for bill, susp, cancel?
305 local($disable_agentcheck) = 1 if $old->pkgpart == $new->pkgpart;
307 local $SIG{HUP} = 'IGNORE';
308 local $SIG{INT} = 'IGNORE';
309 local $SIG{QUIT} = 'IGNORE';
310 local $SIG{TERM} = 'IGNORE';
311 local $SIG{TSTP} = 'IGNORE';
312 local $SIG{PIPE} = 'IGNORE';
314 my $oldAutoCommit = $FS::UID::AutoCommit;
315 local $FS::UID::AutoCommit = 0;
318 foreach my $method ( qw(adjourn expire) ) { # How many reasons?
319 if ($options{'reason'} && $new->$method && $old->$method ne $new->$method) {
320 my $error = $new->insert_reason( 'reason' => $options{'reason'},
321 'date' => $new->$method,
324 dbh->rollback if $oldAutoCommit;
325 return "Error inserting cust_pkg_reason: $error";
330 #save off and freeze RADIUS attributes for any associated svc_acct records
332 if ( $old->part_pkg->is_prepaid || $new->part_pkg->is_prepaid ) {
334 #also check for specific exports?
335 # to avoid spurious modify export events
336 @svc_acct = map { $_->svc_x }
337 grep { $_->part_svc->svcdb eq 'svc_acct' }
340 $_->snapshot foreach @svc_acct;
344 my $error = $new->SUPER::replace($old,
345 $options{options} ? ${options{options}} : ()
348 $dbh->rollback if $oldAutoCommit;
352 #for prepaid packages,
353 #trigger export of new RADIUS Expiration attribute when cust_pkg.bill changes
354 foreach my $old_svc_acct ( @svc_acct ) {
355 my $new_svc_acct = new FS::svc_acct { $old_svc_acct->hash };
356 my $s_error = $new_svc_acct->replace($old_svc_acct);
358 $dbh->rollback if $oldAutoCommit;
363 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
370 Checks all fields to make sure this is a valid billing item. If there is an
371 error, returns the error, otherwise returns false. Called by the insert and
380 $self->ut_numbern('pkgnum')
381 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
382 || $self->ut_numbern('pkgpart')
383 || $self->ut_numbern('setup')
384 || $self->ut_numbern('bill')
385 || $self->ut_numbern('susp')
386 || $self->ut_numbern('cancel')
387 || $self->ut_numbern('adjourn')
388 || $self->ut_numbern('expire')
390 return $error if $error;
392 if ( $self->reg_code ) {
394 unless ( grep { $self->pkgpart == $_->pkgpart }
395 map { $_->reg_code_pkg }
396 qsearchs( 'reg_code', { 'code' => $self->reg_code,
397 'agentnum' => $self->cust_main->agentnum })
399 return "Unknown registration code";
402 } elsif ( $self->promo_code ) {
405 qsearchs('part_pkg', {
406 'pkgpart' => $self->pkgpart,
407 'promo_code' => { op=>'ILIKE', value=>$self->promo_code },
409 return 'Unknown promotional code' unless $promo_part_pkg;
413 unless ( $disable_agentcheck ) {
415 qsearchs( 'agent', { 'agentnum' => $self->cust_main->agentnum } );
416 my $pkgpart_href = $agent->pkgpart_hashref;
417 return "agent ". $agent->agentnum.
418 " can't purchase pkgpart ". $self->pkgpart
419 unless $pkgpart_href->{ $self->pkgpart };
422 $error = $self->ut_foreign_key('pkgpart', 'part_pkg', 'pkgpart' );
423 return $error if $error;
427 $self->otaker(getotaker) unless $self->otaker;
428 $self->otaker =~ /^(\w{1,32})$/ or return "Illegal otaker";
431 if ( $self->dbdef_table->column('manual_flag') ) {
432 $self->manual_flag('') if $self->manual_flag eq ' ';
433 $self->manual_flag =~ /^([01]?)$/
434 or return "Illegal manual_flag ". $self->manual_flag;
435 $self->manual_flag($1);
441 =item cancel [ OPTION => VALUE ... ]
443 Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
444 in this package, then cancels the package itself (sets the cancel field to
447 Available options are: I<quiet>
449 I<quiet> can be set true to supress email cancellation notices.
451 If there is an error, returns the error, otherwise returns false.
456 my( $self, %options ) = @_;
459 local $SIG{HUP} = 'IGNORE';
460 local $SIG{INT} = 'IGNORE';
461 local $SIG{QUIT} = 'IGNORE';
462 local $SIG{TERM} = 'IGNORE';
463 local $SIG{TSTP} = 'IGNORE';
464 local $SIG{PIPE} = 'IGNORE';
466 my $oldAutoCommit = $FS::UID::AutoCommit;
467 local $FS::UID::AutoCommit = 0;
470 if ($options{'reason'}) {
471 $error = $self->insert_reason( 'reason' => $options{'reason'} );
473 dbh->rollback if $oldAutoCommit;
474 return "Error inserting cust_pkg_reason: $error";
479 foreach my $cust_svc (
482 sort { $a->[1] <=> $b->[1] }
483 map { [ $_, $_->svc_x->table_info->{'cancel_weight'} ]; }
484 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
487 my $error = $cust_svc->cancel;
490 $dbh->rollback if $oldAutoCommit;
491 return "Error cancelling cust_svc: $error";
495 # Add a credit for remaining service
496 my $remaining_value = $self->calc_remain();
497 if ( $remaining_value > 0 ) {
498 my $conf = new FS::Conf;
499 my $error = $self->cust_main->credit(
501 'Credit for unused time on '. $self->part_pkg->pkg,
502 'reason_type' => $conf->config('cancel_credit_type'),
505 $dbh->rollback if $oldAutoCommit;
506 return "Error crediting customer \$$remaining_value for unused time on".
507 $self->part_pkg->pkg. ": $error";
511 unless ( $self->getfield('cancel') ) {
512 my %hash = $self->hash;
513 $hash{'cancel'} = time;
514 my $new = new FS::cust_pkg ( \%hash );
515 $error = $new->replace( $self, options => { $self->options } );
517 $dbh->rollback if $oldAutoCommit;
522 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
524 my $conf = new FS::Conf;
525 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list;
526 if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) {
527 my $conf = new FS::Conf;
528 my $error = send_email(
529 'from' => $conf->config('invoice_from'),
530 'to' => \@invoicing_list,
531 'subject' => ( $conf->config('cancelsubject') || 'Cancellation Notice' ),
532 'body' => [ map "$_\n", $conf->config('cancelmessage') ],
534 #should this do something on errors?
543 Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
544 package, then suspends the package itself (sets the susp field to now).
546 If there is an error, returns the error, otherwise returns false.
551 my( $self, %options ) = @_;
554 local $SIG{HUP} = 'IGNORE';
555 local $SIG{INT} = 'IGNORE';
556 local $SIG{QUIT} = 'IGNORE';
557 local $SIG{TERM} = 'IGNORE';
558 local $SIG{TSTP} = 'IGNORE';
559 local $SIG{PIPE} = 'IGNORE';
561 my $oldAutoCommit = $FS::UID::AutoCommit;
562 local $FS::UID::AutoCommit = 0;
565 if ($options{'reason'}) {
566 $error = $self->insert_reason( 'reason' => $options{'reason'} );
568 dbh->rollback if $oldAutoCommit;
569 return "Error inserting cust_pkg_reason: $error";
573 foreach my $cust_svc (
574 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
576 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
578 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
579 $dbh->rollback if $oldAutoCommit;
580 return "Illegal svcdb value in part_svc!";
583 require "FS/$svcdb.pm";
585 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
587 $error = $svc->suspend;
589 $dbh->rollback if $oldAutoCommit;
596 unless ( $self->getfield('susp') ) {
597 my %hash = $self->hash;
598 $hash{'susp'} = time;
599 my $new = new FS::cust_pkg ( \%hash );
600 $error = $new->replace( $self, options => { $self->options } );
602 $dbh->rollback if $oldAutoCommit;
607 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
612 =item unsuspend [ OPTION => VALUE ... ]
614 Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
615 package, then unsuspends the package itself (clears the susp field and the
616 adjourn field if it is in the past).
618 Available options are: I<adjust_next_bill>.
620 I<adjust_next_bill> can be set true to adjust the next bill date forward by
621 the amount of time the account was inactive. This was set true by default
622 since 1.4.2 and 1.5.0pre6; however, starting with 1.7.0 this needs to be
623 explicitly requested. Price plans for which this makes sense (anniversary-date
624 based than prorate or subscription) could have an option to enable this
627 If there is an error, returns the error, otherwise returns false.
632 my( $self, %opt ) = @_;
635 local $SIG{HUP} = 'IGNORE';
636 local $SIG{INT} = 'IGNORE';
637 local $SIG{QUIT} = 'IGNORE';
638 local $SIG{TERM} = 'IGNORE';
639 local $SIG{TSTP} = 'IGNORE';
640 local $SIG{PIPE} = 'IGNORE';
642 my $oldAutoCommit = $FS::UID::AutoCommit;
643 local $FS::UID::AutoCommit = 0;
646 foreach my $cust_svc (
647 qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
649 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
651 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
652 $dbh->rollback if $oldAutoCommit;
653 return "Illegal svcdb value in part_svc!";
656 require "FS/$svcdb.pm";
658 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
660 $error = $svc->unsuspend;
662 $dbh->rollback if $oldAutoCommit;
669 unless ( ! $self->getfield('susp') ) {
670 my %hash = $self->hash;
671 my $inactive = time - $hash{'susp'};
673 my $conf = new FS::Conf;
675 $hash{'bill'} = ( $hash{'bill'} || $hash{'setup'} ) + $inactive
676 if ( $opt{'adjust_next_bill'}
677 || $conf->config('unsuspend-always_adjust_next_bill_date') )
678 && $inactive > 0 && ( $hash{'bill'} || $hash{'setup'} );
681 $hash{'adjourn'} = '' if $hash{'adjourn'} < time;
682 my $new = new FS::cust_pkg ( \%hash );
683 $error = $new->replace( $self, options => { $self->options } );
685 $dbh->rollback if $oldAutoCommit;
690 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
697 Returns the last bill date, or if there is no last bill date, the setup date.
698 Useful for billing metered services.
704 if ( $self->dbdef_table->column('last_bill') ) {
705 return $self->setfield('last_bill', $_[0]) if @_;
706 return $self->getfield('last_bill') if $self->getfield('last_bill');
708 my $cust_bill_pkg = qsearchs('cust_bill_pkg', { 'pkgnum' => $self->pkgnum,
709 'edate' => $self->bill, } );
710 $cust_bill_pkg ? $cust_bill_pkg->sdate : $self->setup || 0;
715 Returns the most recent FS::reason associated with the package.
721 my $cust_pkg_reason = qsearchs( {
722 'table' => 'cust_pkg_reason',
723 'hashref' => { 'pkgnum' => $self->pkgnum, },
724 'extra_sql'=> 'ORDER BY date DESC LIMIT 1',
726 qsearchs ( 'reason', { 'reasonnum' => $cust_pkg_reason->reasonnum } )
732 Returns the definition for this billing item, as an FS::part_pkg object (see
739 #exists( $self->{'_pkgpart'} )
741 ? $self->{'_pkgpart'}
742 : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
747 Returns the cancelled package this package was changed from, if any.
753 return '' unless $self->change_pkgnum;
754 qsearchs('cust_pkg', { 'pkgnum' => $self->change_pkgnum } );
759 Calls the I<calc_setup> of the FS::part_pkg object associated with this billing
766 $self->part_pkg->calc_setup($self, @_);
771 Calls the I<calc_recur> of the FS::part_pkg object associated with this billing
778 $self->part_pkg->calc_recur($self, @_);
783 Calls the I<calc_remain> of the FS::part_pkg object associated with this
790 $self->part_pkg->calc_remain($self, @_);
795 Calls the I<calc_cancel> of the FS::part_pkg object associated with this
802 $self->part_pkg->calc_cancel($self, @_);
807 Returns any invoice line items for this package (see L<FS::cust_bill_pkg>).
813 qsearch( 'cust_bill_pkg', { 'pkgnum' => $self->pkgnum } );
816 =item cust_svc [ SVCPART ]
818 Returns the services for this package, as FS::cust_svc objects (see
819 L<FS::cust_svc>). If a svcpart is specified, return only the matching
828 return qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum,
829 'svcpart' => shift, } );
832 #if ( $self->{'_svcnum'} ) {
833 # values %{ $self->{'_svcnum'}->cache };
835 $self->_sort_cust_svc(
836 [ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ]
842 =item overlimit [ SVCPART ]
844 Returns the services for this package which have exceeded their
845 usage limit as FS::cust_svc objects (see L<FS::cust_svc>). If a svcpart
846 is specified, return only the matching services.
852 grep { $_->overlimit } $self->cust_svc;
855 =item h_cust_svc END_TIMESTAMP [ START_TIMESTAMP ]
857 Returns historical services for this package created before END TIMESTAMP and
858 (optionally) not cancelled before START_TIMESTAMP, as FS::h_cust_svc objects
859 (see L<FS::h_cust_svc>).
866 $self->_sort_cust_svc(
867 [ qsearch( 'h_cust_svc',
868 { 'pkgnum' => $self->pkgnum, },
869 FS::h_cust_svc->sql_h_search(@_),
876 my( $self, $arrayref ) = @_;
879 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
881 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
882 'svcpart' => $_->svcpart } );
884 $pkg_svc ? $pkg_svc->primary_svc : '',
885 $pkg_svc ? $pkg_svc->quantity : 0,
892 =item num_cust_svc [ SVCPART ]
894 Returns the number of provisioned services for this package. If a svcpart is
895 specified, counts only the matching services.
901 my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?';
902 $sql .= ' AND svcpart = ?' if @_;
903 my $sth = dbh->prepare($sql) or die dbh->errstr;
904 $sth->execute($self->pkgnum, @_) or die $sth->errstr;
905 $sth->fetchrow_arrayref->[0];
908 =item available_part_svc
910 Returns a list of FS::part_svc objects representing services included in this
911 package but not yet provisioned. Each FS::part_svc object also has an extra
912 field, I<num_avail>, which specifies the number of available services.
916 sub available_part_svc {
918 grep { $_->num_avail > 0 }
920 my $part_svc = $_->part_svc;
921 $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking
922 $_->quantity - $self->num_cust_svc($_->svcpart);
925 $self->part_pkg->pkg_svc;
930 Returns a list of FS::part_svc objects representing provisioned and available
931 services included in this package. Each FS::part_svc object also has the
932 following extra fields:
936 =item num_cust_svc (count)
938 =item num_avail (quantity - count)
940 =item cust_pkg_svc (services) - array reference containing the provisioned services, as cust_svc objects
943 label -> ($cust_svc->label)[1]
952 #XXX some sort of sort order besides numeric by svcpart...
953 my @part_svc = sort { $a->svcpart <=> $b->svcpart } map {
955 my $part_svc = $pkg_svc->part_svc;
956 my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
957 $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #more evil
958 $part_svc->{'Hash'}{'num_avail'} =
959 max( 0, $pkg_svc->quantity - $num_cust_svc );
960 $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
962 } $self->part_pkg->pkg_svc;
965 push @part_svc, map {
967 my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
968 $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #speak no evail
969 $part_svc->{'Hash'}{'num_avail'} = 0; #0-$num_cust_svc ?
970 $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
972 } $self->extra_part_svc;
980 Returns a list of FS::part_svc objects corresponding to services in this
981 package which are still provisioned but not (any longer) available in the
989 my $pkgnum = $self->pkgnum;
990 my $pkgpart = $self->pkgpart;
993 'table' => 'part_svc',
995 'extra_sql' => "WHERE 0 = ( SELECT COUNT(*) FROM pkg_svc
996 WHERE pkg_svc.svcpart = part_svc.svcpart
997 AND pkg_svc.pkgpart = $pkgpart
1000 AND 0 < ( SELECT count(*)
1002 LEFT JOIN cust_pkg using ( pkgnum )
1003 WHERE cust_svc.svcpart = part_svc.svcpart
1004 AND pkgnum = $pkgnum
1011 Returns a short status string for this package, currently:
1015 =item not yet billed
1017 =item one-time charge
1032 my $freq = length($self->freq) ? $self->freq : $self->part_pkg->freq;
1034 return 'cancelled' if $self->get('cancel');
1035 return 'suspended' if $self->susp;
1036 return 'not yet billed' unless $self->setup;
1037 return 'one-time charge' if $freq =~ /^(0|$)/;
1043 Class method that returns the list of possible status strings for pacakges
1044 (see L<the status method|/status>). For example:
1046 @statuses = FS::cust_pkg->statuses();
1050 tie my %statuscolor, 'Tie::IxHash',
1051 'not yet billed' => '000000',
1052 'one-time charge' => '000000',
1053 'active' => '00CC00',
1054 'suspended' => 'FF9900',
1055 'cancelled' => 'FF0000',
1059 my $self = shift; #could be class...
1060 grep { $_ !~ /^(not yet billed)$/ } #this is a dumb status anyway
1061 # mayble split btw one-time vs. recur
1067 Returns a hex triplet color string for this package's status.
1073 $statuscolor{$self->status};
1078 Returns a list of lists, calling the label method for all services
1079 (see L<FS::cust_svc>) of this billing item.
1085 map { [ $_->label ] } $self->cust_svc;
1088 =item h_labels END_TIMESTAMP [ START_TIMESTAMP ]
1090 Like the labels method, but returns historical information on services that
1091 were active as of END_TIMESTAMP and (optionally) not cancelled before
1094 Returns a list of lists, calling the label method for all (historical) services
1095 (see L<FS::h_cust_svc>) of this billing item.
1101 map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
1104 =item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
1106 Like h_labels, except returns a simple flat list, and shortens long
1107 (currently >5) lists of identical services to one line that lists the service
1108 label and the number of individual services rather than individual items.
1112 sub h_labels_short {
1116 #tie %labels, 'Tie::IxHash';
1117 push @{ $labels{$_->[0]} }, $_->[1]
1118 foreach $self->h_labels(@_);
1120 foreach my $label ( keys %labels ) {
1121 my @values = @{ $labels{$label} };
1122 my $num = scalar(@values);
1124 push @labels, "$label ($num)";
1126 push @labels, map { "$label: $_" } @values;
1136 Returns the parent customer object (see L<FS::cust_main>).
1142 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
1145 =item seconds_since TIMESTAMP
1147 Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
1148 package have been online since TIMESTAMP, according to the session monitor.
1150 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1151 L<Time::Local> and L<Date::Parse> for conversion functions.
1156 my($self, $since) = @_;
1159 foreach my $cust_svc (
1160 grep { $_->part_svc->svcdb eq 'svc_acct' } $self->cust_svc
1162 $seconds += $cust_svc->seconds_since($since);
1169 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1171 Returns the numbers of seconds all accounts (see L<FS::svc_acct>) in this
1172 package have been online between TIMESTAMP_START (inclusive) and TIMESTAMP_END
1175 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1176 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1182 sub seconds_since_sqlradacct {
1183 my($self, $start, $end) = @_;
1187 foreach my $cust_svc (
1189 my $part_svc = $_->part_svc;
1190 $part_svc->svcdb eq 'svc_acct'
1191 && scalar($part_svc->part_export('sqlradius'));
1194 $seconds += $cust_svc->seconds_since_sqlradacct($start, $end);
1201 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1203 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1204 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1208 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1209 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1214 sub attribute_since_sqlradacct {
1215 my($self, $start, $end, $attrib) = @_;
1219 foreach my $cust_svc (
1221 my $part_svc = $_->part_svc;
1222 $part_svc->svcdb eq 'svc_acct'
1223 && scalar($part_svc->part_export('sqlradius'));
1226 $sum += $cust_svc->attribute_since_sqlradacct($start, $end, $attrib);
1233 =item transfer DEST_PKGNUM | DEST_CUST_PKG, [ OPTION => VALUE ... ]
1235 Transfers as many services as possible from this package to another package.
1237 The destination package can be specified by pkgnum by passing an FS::cust_pkg
1238 object. The destination package must already exist.
1240 Services are moved only if the destination allows services with the correct
1241 I<svcpart> (not svcdb), unless the B<change_svcpart> option is set true. Use
1242 this option with caution! No provision is made for export differences
1243 between the old and new service definitions. Probably only should be used
1244 when your exports for all service definitions of a given svcdb are identical.
1245 (attempt a transfer without it first, to move all possible svcpart-matching
1248 Any services that can't be moved remain in the original package.
1250 Returns an error, if there is one; otherwise, returns the number of services
1251 that couldn't be moved.
1256 my ($self, $dest_pkgnum, %opt) = @_;
1262 if (ref ($dest_pkgnum) eq 'FS::cust_pkg') {
1263 $dest = $dest_pkgnum;
1264 $dest_pkgnum = $dest->pkgnum;
1266 $dest = qsearchs('cust_pkg', { pkgnum => $dest_pkgnum });
1269 return ('Package does not exist: '.$dest_pkgnum) unless $dest;
1271 foreach my $pkg_svc ( $dest->part_pkg->pkg_svc ) {
1272 $target{$pkg_svc->svcpart} = $pkg_svc->quantity;
1275 foreach my $cust_svc ($dest->cust_svc) {
1276 $target{$cust_svc->svcpart}--;
1279 my %svcpart2svcparts = ();
1280 if ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
1281 warn "change_svcpart option received, creating alternates list\n" if $DEBUG;
1282 foreach my $svcpart ( map { $_->svcpart } $self->cust_svc ) {
1283 next if exists $svcpart2svcparts{$svcpart};
1284 my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
1285 $svcpart2svcparts{$svcpart} = [
1287 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
1289 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $dest->pkgpart,
1290 'svcpart' => $_ } );
1292 $pkg_svc ? $pkg_svc->primary_svc : '',
1293 $pkg_svc ? $pkg_svc->quantity : 0,
1297 grep { $_ != $svcpart }
1299 qsearch('part_svc', { 'svcdb' => $part_svc->svcdb } )
1301 warn "alternates for svcpart $svcpart: ".
1302 join(', ', @{$svcpart2svcparts{$svcpart}}). "\n"
1307 foreach my $cust_svc ($self->cust_svc) {
1308 if($target{$cust_svc->svcpart} > 0) {
1309 $target{$cust_svc->svcpart}--;
1310 my $new = new FS::cust_svc { $cust_svc->hash };
1311 $new->pkgnum($dest_pkgnum);
1312 my $error = $new->replace($cust_svc);
1313 return $error if $error;
1314 } elsif ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
1316 warn "looking for alternates for svcpart ". $cust_svc->svcpart. "\n";
1317 warn "alternates to consider: ".
1318 join(', ', @{$svcpart2svcparts{$cust_svc->svcpart}}). "\n";
1320 my @alternate = grep {
1321 warn "considering alternate svcpart $_: ".
1322 "$target{$_} available in new package\n"
1325 } @{$svcpart2svcparts{$cust_svc->svcpart}};
1327 warn "alternate(s) found\n" if $DEBUG;
1328 my $change_svcpart = $alternate[0];
1329 $target{$change_svcpart}--;
1330 my $new = new FS::cust_svc { $cust_svc->hash };
1331 $new->svcpart($change_svcpart);
1332 $new->pkgnum($dest_pkgnum);
1333 my $error = $new->replace($cust_svc);
1334 return $error if $error;
1347 This method is deprecated. See the I<depend_jobnum> option to the insert and
1348 order_pkgs methods in FS::cust_main for a better way to defer provisioning.
1355 local $SIG{HUP} = 'IGNORE';
1356 local $SIG{INT} = 'IGNORE';
1357 local $SIG{QUIT} = 'IGNORE';
1358 local $SIG{TERM} = 'IGNORE';
1359 local $SIG{TSTP} = 'IGNORE';
1360 local $SIG{PIPE} = 'IGNORE';
1362 my $oldAutoCommit = $FS::UID::AutoCommit;
1363 local $FS::UID::AutoCommit = 0;
1366 foreach my $cust_svc ( $self->cust_svc ) {
1367 #false laziness w/svc_Common::insert
1368 my $svc_x = $cust_svc->svc_x;
1369 foreach my $part_export ( $cust_svc->part_svc->part_export ) {
1370 my $error = $part_export->export_insert($svc_x);
1372 $dbh->rollback if $oldAutoCommit;
1378 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1385 =head1 CLASS METHODS
1391 Returns an SQL expression identifying recurring packages.
1395 sub recurring_sql { "
1396 '0' != ( select freq from part_pkg
1397 where cust_pkg.pkgpart = part_pkg.pkgpart )
1402 Returns an SQL expression identifying one-time packages.
1407 '0' = ( select freq from part_pkg
1408 where cust_pkg.pkgpart = part_pkg.pkgpart )
1413 Returns an SQL expression identifying active packages.
1418 ". $_[0]->recurring_sql(). "
1419 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1420 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
1425 Returns an SQL expression identifying inactive packages (one-time packages
1426 that are otherwise unsuspended/uncancelled).
1430 sub inactive_sql { "
1431 ". $_[0]->onetime_sql(). "
1432 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1433 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
1439 Returns an SQL expression identifying suspended packages.
1443 sub suspended_sql { susp_sql(@_); }
1445 #$_[0]->recurring_sql(). ' AND '.
1447 ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1448 AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0
1455 Returns an SQL exprression identifying cancelled packages.
1459 sub cancelled_sql { cancel_sql(@_); }
1461 #$_[0]->recurring_sql(). ' AND '.
1462 "cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0";
1465 =item search_sql HREF
1467 Returns a qsearch hash expression to search for parameters specified in HREF.
1468 Valid parameters are
1472 =item magic - /^(active|inactive|suspended|cancell?ed)$/
1473 =item status - /^(active|inactive|suspended|one-time charge|inactive|cancell?ed)$/
1475 =item pkgpart - list specified how?
1476 =item setup - arrayref of beginning and ending epoch date
1477 =item last_bill - arrayref of beginning and ending epoch date
1478 =item bill - arrayref of beginning and ending epoch date
1479 =item adjourn - arrayref of beginning and ending epoch date
1480 =item susp - arrayref of beginning and ending epoch date
1481 =item expire - arrayref of beginning and ending epoch date
1482 =item cancel - arrayref of beginning and ending epoch date
1483 =item query - /^(pkgnum/APKG_pkgnum)$/
1484 =item cust_fields - a value suited to passing to FS::UI::Web::cust_header
1485 =item CurrentUser - specifies the user for agent virtualization
1491 my ($class, $params) = @_;
1498 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
1507 if ( $params->{'magic'} eq 'active'
1508 || $params->{'status'} eq 'active' ) {
1510 push @where, FS::cust_pkg->active_sql();
1512 } elsif ( $params->{'magic'} eq 'inactive'
1513 || $params->{'status'} eq 'inactive' ) {
1515 push @where, FS::cust_pkg->inactive_sql();
1517 } elsif ( $params->{'magic'} eq 'suspended'
1518 || $params->{'status'} eq 'suspended' ) {
1520 push @where, FS::cust_pkg->suspended_sql();
1522 } elsif ( $params->{'magic'} =~ /^cancell?ed$/
1523 || $params->{'status'} =~ /^cancell?ed$/ ) {
1525 push @where, FS::cust_pkg->cancelled_sql();
1527 } elsif ( $params->{'status'} =~ /^(one-time charge|inactive)$/ ) {
1529 push @where, FS::cust_pkg->inactive_sql();
1534 # parse package class
1537 #false lazinessish w/graph/cust_bill_pkg.cgi
1540 if ( exists($params->{'classnum'})
1541 && $params->{'classnum'} =~ /^(\d*)$/
1545 if ( $classnum ) { #a specific class
1546 push @where, "classnum = $classnum";
1548 #@pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) );
1549 #die "classnum $classnum not found!" unless $pkg_class[0];
1550 #$title .= $pkg_class[0]->classname.' ';
1552 } elsif ( $classnum eq '' ) { #the empty class
1554 push @where, "classnum IS NULL";
1555 #$title .= 'Empty class ';
1556 #@pkg_class = ( '(empty class)' );
1557 } elsif ( $classnum eq '0' ) {
1558 #@pkg_class = qsearch('pkg_class', {} ); # { 'disabled' => '' } );
1559 #push @pkg_class, '(empty class)';
1561 die "illegal classnum";
1570 my $pkgpart = join (' OR pkgpart=',
1571 grep {$_} map { /^(\d+)$/; } ($params->{'pkgpart'}));
1572 push @where, '(pkgpart=' . $pkgpart . ')' if $pkgpart;
1580 #false laziness w/report_cust_pkg.html
1583 'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
1584 'active' => { 'susp'=>1, 'cancel'=>1 },
1585 'suspended' => { 'cancel' => 1 },
1590 foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
1592 next unless exists($params->{$field});
1594 my($beginning, $ending) = @{$params->{$field}};
1596 next if $beginning == 0 && $ending == 4294967295;
1599 "cust_pkg.$field IS NOT NULL",
1600 "cust_pkg.$field >= $beginning",
1601 "cust_pkg.$field <= $ending";
1603 $orderby ||= "ORDER BY cust_pkg.$field";
1607 $orderby ||= 'ORDER BY bill';
1610 # parse magic, legacy, etc.
1613 if ( $params->{'magic'} &&
1614 $params->{'magic'} =~ /^(active|inactive|suspended|cancell?ed)$/
1617 $orderby = 'ORDER BY pkgnum';
1619 if ( $params->{'pkgpart'} =~ /^(\d+)$/ ) {
1620 push @where, "pkgpart = $1";
1623 } elsif ( $params->{'query'} eq 'pkgnum' ) {
1625 $orderby = 'ORDER BY pkgnum';
1627 } elsif ( $params->{'query'} eq 'APKG_pkgnum' ) {
1629 $orderby = 'ORDER BY pkgnum';
1632 SELECT count(*) FROM pkg_svc
1633 WHERE pkg_svc.pkgpart = cust_pkg.pkgpart
1634 AND pkg_svc.quantity > ( SELECT count(*) FROM cust_svc
1635 WHERE cust_svc.pkgnum = cust_pkg.pkgnum
1636 AND cust_svc.svcpart = pkg_svc.svcpart
1643 # setup queries, links, subs, etc. for the search
1646 # here is the agent virtualization
1647 if ($params->{CurrentUser}) {
1649 qsearchs('access_user', { username => $params->{CurrentUser} });
1652 push @where, $access_user->agentnums_sql;
1657 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
1660 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
1662 my $addl_from = 'LEFT JOIN cust_main USING ( custnum ) '.
1663 'LEFT JOIN part_pkg USING ( pkgpart ) '.
1664 'LEFT JOIN pkg_class USING ( classnum ) ';
1666 my $count_query = "SELECT COUNT(*) FROM cust_pkg $addl_from $extra_sql";
1669 'table' => 'cust_pkg',
1671 'select' => join(', ',
1673 ( map "part_pkg.$_", qw( pkg freq ) ),
1674 'pkg_class.classname',
1675 'cust_main.custnum as cust_main_custnum',
1676 FS::UI::Web::cust_sql_fields(
1677 $params->{'cust_fields'}
1680 'extra_sql' => "$extra_sql $orderby",
1681 'addl_from' => $addl_from,
1682 'count_query' => $count_query,
1691 =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ] ]
1693 CUSTNUM is a customer (see L<FS::cust_main>)
1695 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
1696 L<FS::part_pkg>) to order for this customer. Duplicates are of course
1699 REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
1700 remove for this customer. The services (see L<FS::cust_svc>) are moved to the
1701 new billing items. An error is returned if this is not possible (see
1702 L<FS::pkg_svc>). An empty arrayref is equivalent to not specifying this
1705 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
1706 newly-created cust_pkg objects.
1711 my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
1713 my $conf = new FS::Conf;
1715 # Transactionize this whole mess
1716 local $SIG{HUP} = 'IGNORE';
1717 local $SIG{INT} = 'IGNORE';
1718 local $SIG{QUIT} = 'IGNORE';
1719 local $SIG{TERM} = 'IGNORE';
1720 local $SIG{TSTP} = 'IGNORE';
1721 local $SIG{PIPE} = 'IGNORE';
1723 my $oldAutoCommit = $FS::UID::AutoCommit;
1724 local $FS::UID::AutoCommit = 0;
1728 my $cust_main = qsearchs('cust_main', { custnum => $custnum });
1729 return "Customer not found: $custnum" unless $cust_main;
1731 my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
1734 my $change = scalar(@old_cust_pkg) != 0;
1737 if ( scalar(@old_cust_pkg) == 1 && scalar(@$pkgparts) == 1 ) {
1741 #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( last_bill bill );
1743 #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( setup );
1744 $hash{'setup'} = $time if $old_cust_pkg[0]->setup;
1746 $hash{'change_date'} = $time;
1747 $hash{"change_$_"} = $old_cust_pkg[0]->$_() foreach qw( pkgnum pkgpart );
1750 # Create the new packages.
1751 foreach my $pkgpart (@$pkgparts) {
1752 my $cust_pkg = new FS::cust_pkg { custnum => $custnum,
1753 pkgpart => $pkgpart,
1756 $error = $cust_pkg->insert( 'change' => $change );
1758 $dbh->rollback if $oldAutoCommit;
1761 push @$return_cust_pkg, $cust_pkg;
1763 # $return_cust_pkg now contains refs to all of the newly
1766 # Transfer services and cancel old packages.
1767 foreach my $old_pkg (@old_cust_pkg) {
1769 foreach my $new_pkg (@$return_cust_pkg) {
1770 $error = $old_pkg->transfer($new_pkg);
1771 if ($error and $error == 0) {
1772 # $old_pkg->transfer failed.
1773 $dbh->rollback if $oldAutoCommit;
1778 if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
1779 warn "trying transfer again with change_svcpart option\n" if $DEBUG;
1780 foreach my $new_pkg (@$return_cust_pkg) {
1781 $error = $old_pkg->transfer($new_pkg, 'change_svcpart'=>1 );
1782 if ($error and $error == 0) {
1783 # $old_pkg->transfer failed.
1784 $dbh->rollback if $oldAutoCommit;
1791 # Transfers were successful, but we went through all of the
1792 # new packages and still had services left on the old package.
1793 # We can't cancel the package under the circumstances, so abort.
1794 $dbh->rollback if $oldAutoCommit;
1795 return "Unable to transfer all services from package ".$old_pkg->pkgnum;
1797 $error = $old_pkg->cancel( quiet=>1 );
1803 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1807 =item bulk_change PKGPARTS_ARYREF, REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ]
1809 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
1810 L<FS::part_pkg>) to order for this customer. Duplicates are of course
1813 REMOVE_PKGNUMS is an list of pkgnums specifying the billing items to
1814 replace. The services (see L<FS::cust_svc>) are moved to the
1815 new billing items. An error is returned if this is not possible (see
1818 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
1819 newly-created cust_pkg objects.
1824 my ($pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
1826 # Transactionize this whole mess
1827 local $SIG{HUP} = 'IGNORE';
1828 local $SIG{INT} = 'IGNORE';
1829 local $SIG{QUIT} = 'IGNORE';
1830 local $SIG{TERM} = 'IGNORE';
1831 local $SIG{TSTP} = 'IGNORE';
1832 local $SIG{PIPE} = 'IGNORE';
1834 my $oldAutoCommit = $FS::UID::AutoCommit;
1835 local $FS::UID::AutoCommit = 0;
1839 my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
1842 while(scalar(@old_cust_pkg)) {
1844 my $custnum = $old_cust_pkg[0]->custnum;
1845 my (@remove) = map { $_->pkgnum }
1846 grep { $_->custnum == $custnum } @old_cust_pkg;
1847 @old_cust_pkg = grep { $_->custnum != $custnum } @old_cust_pkg;
1849 my $error = order $custnum, $pkgparts, \@remove, \@return;
1851 push @errors, $error
1853 push @$return_cust_pkg, @return;
1856 if (scalar(@errors)) {
1857 $dbh->rollback if $oldAutoCommit;
1858 return join(' / ', @errors);
1861 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1866 my ($self, %options) = @_;
1868 my $otaker = $FS::CurrentUser::CurrentUser->username;
1870 my $cust_pkg_reason =
1871 new FS::cust_pkg_reason({ 'pkgnum' => $self->pkgnum,
1872 'reasonnum' => $options{'reason'},
1873 'otaker' => $otaker,
1874 'date' => $options{'date'}
1878 return $cust_pkg_reason->insert;
1881 =item set_usage USAGE_VALUE_HASHREF
1883 USAGE_VALUE_HASHREF is a hashref of svc_acct usage columns and the amounts
1884 to which they should be set (see L<FS::svc_acct>). Currently seconds,
1885 upbytes, downbytes, and totalbytes are appropriate keys.
1887 All svc_accts which are part of this package have their values reset.
1892 my ($self, $valueref) = @_;
1894 foreach my $cust_svc ($self->cust_svc){
1895 my $svc_x = $cust_svc->svc_x;
1896 $svc_x->set_usage($valueref)
1897 if $svc_x->can("set_usage");
1905 sub order is not OO. Perhaps it should be moved to FS::cust_main and made so?
1907 In sub order, the @pkgparts array (passed by reference) is clobbered.
1909 Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard
1910 method to pass dates to the recur_prog expression, it should do so.
1912 FS::svc_acct, FS::svc_domain, FS::svc_www, FS::svc_ip and FS::svc_forward are
1913 loaded via 'use' at compile time, rather than via 'require' in sub { setup,
1914 suspend, unsuspend, cancel } because they use %FS::UID::callback to load
1915 configuration values. Probably need a subroutine which decides what to do
1916 based on whether or not we've fetched the user yet, rather than a hash. See
1917 FS::UID and the TODO.
1919 Now that things are transactional should the check in the insert method be
1924 L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>,
1925 L<FS::pkg_svc>, schema.html from the base documentation