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;
713 =item last_cust_pkg_reason
715 Returns the most recent FS::reason associated with the package.
719 sub last_cust_pkg_reason {
722 'table' => 'cust_pkg_reason',
723 'hashref' => { 'pkgnum' => $self->pkgnum, },
724 'extra_sql'=> "AND date <= ". time,
725 'order_by' => 'ORDER BY date DESC LIMIT 1',
731 Returns the most recent FS::reason associated with the package.
736 my $cust_pkg_reason = shift->last_cust_pkg_reason;
737 $cust_pkg_reason->reason
743 Returns the definition for this billing item, as an FS::part_pkg object (see
750 #exists( $self->{'_pkgpart'} )
752 ? $self->{'_pkgpart'}
753 : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
758 Returns the cancelled package this package was changed from, if any.
764 return '' unless $self->change_pkgnum;
765 qsearchs('cust_pkg', { 'pkgnum' => $self->change_pkgnum } );
770 Calls the I<calc_setup> of the FS::part_pkg object associated with this billing
777 $self->part_pkg->calc_setup($self, @_);
782 Calls the I<calc_recur> of the FS::part_pkg object associated with this billing
789 $self->part_pkg->calc_recur($self, @_);
794 Calls the I<calc_remain> of the FS::part_pkg object associated with this
801 $self->part_pkg->calc_remain($self, @_);
806 Calls the I<calc_cancel> of the FS::part_pkg object associated with this
813 $self->part_pkg->calc_cancel($self, @_);
818 Returns any invoice line items for this package (see L<FS::cust_bill_pkg>).
824 qsearch( 'cust_bill_pkg', { 'pkgnum' => $self->pkgnum } );
827 =item cust_svc [ SVCPART ]
829 Returns the services for this package, as FS::cust_svc objects (see
830 L<FS::cust_svc>). If a svcpart is specified, return only the matching
839 return qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum,
840 'svcpart' => shift, } );
843 #if ( $self->{'_svcnum'} ) {
844 # values %{ $self->{'_svcnum'}->cache };
846 $self->_sort_cust_svc(
847 [ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ]
853 =item overlimit [ SVCPART ]
855 Returns the services for this package which have exceeded their
856 usage limit as FS::cust_svc objects (see L<FS::cust_svc>). If a svcpart
857 is specified, return only the matching services.
863 grep { $_->overlimit } $self->cust_svc;
866 =item h_cust_svc END_TIMESTAMP [ START_TIMESTAMP ]
868 Returns historical services for this package created before END TIMESTAMP and
869 (optionally) not cancelled before START_TIMESTAMP, as FS::h_cust_svc objects
870 (see L<FS::h_cust_svc>).
877 $self->_sort_cust_svc(
878 [ qsearch( 'h_cust_svc',
879 { 'pkgnum' => $self->pkgnum, },
880 FS::h_cust_svc->sql_h_search(@_),
887 my( $self, $arrayref ) = @_;
890 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
892 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $self->pkgpart,
893 'svcpart' => $_->svcpart } );
895 $pkg_svc ? $pkg_svc->primary_svc : '',
896 $pkg_svc ? $pkg_svc->quantity : 0,
903 =item num_cust_svc [ SVCPART ]
905 Returns the number of provisioned services for this package. If a svcpart is
906 specified, counts only the matching services.
912 my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?';
913 $sql .= ' AND svcpart = ?' if @_;
914 my $sth = dbh->prepare($sql) or die dbh->errstr;
915 $sth->execute($self->pkgnum, @_) or die $sth->errstr;
916 $sth->fetchrow_arrayref->[0];
919 =item available_part_svc
921 Returns a list of FS::part_svc objects representing services included in this
922 package but not yet provisioned. Each FS::part_svc object also has an extra
923 field, I<num_avail>, which specifies the number of available services.
927 sub available_part_svc {
929 grep { $_->num_avail > 0 }
931 my $part_svc = $_->part_svc;
932 $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking
933 $_->quantity - $self->num_cust_svc($_->svcpart);
936 $self->part_pkg->pkg_svc;
941 Returns a list of FS::part_svc objects representing provisioned and available
942 services included in this package. Each FS::part_svc object also has the
943 following extra fields:
947 =item num_cust_svc (count)
949 =item num_avail (quantity - count)
951 =item cust_pkg_svc (services) - array reference containing the provisioned services, as cust_svc objects
954 label -> ($cust_svc->label)[1]
963 #XXX some sort of sort order besides numeric by svcpart...
964 my @part_svc = sort { $a->svcpart <=> $b->svcpart } map {
966 my $part_svc = $pkg_svc->part_svc;
967 my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
968 $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #more evil
969 $part_svc->{'Hash'}{'num_avail'} =
970 max( 0, $pkg_svc->quantity - $num_cust_svc );
971 $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
973 } $self->part_pkg->pkg_svc;
976 push @part_svc, map {
978 my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
979 $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #speak no evail
980 $part_svc->{'Hash'}{'num_avail'} = 0; #0-$num_cust_svc ?
981 $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
983 } $self->extra_part_svc;
991 Returns a list of FS::part_svc objects corresponding to services in this
992 package which are still provisioned but not (any longer) available in the
1000 my $pkgnum = $self->pkgnum;
1001 my $pkgpart = $self->pkgpart;
1004 'table' => 'part_svc',
1006 'extra_sql' => "WHERE 0 = ( SELECT COUNT(*) FROM pkg_svc
1007 WHERE pkg_svc.svcpart = part_svc.svcpart
1008 AND pkg_svc.pkgpart = $pkgpart
1011 AND 0 < ( SELECT count(*)
1013 LEFT JOIN cust_pkg using ( pkgnum )
1014 WHERE cust_svc.svcpart = part_svc.svcpart
1015 AND pkgnum = $pkgnum
1022 Returns a short status string for this package, currently:
1026 =item not yet billed
1028 =item one-time charge
1043 my $freq = length($self->freq) ? $self->freq : $self->part_pkg->freq;
1045 return 'cancelled' if $self->get('cancel');
1046 return 'suspended' if $self->susp;
1047 return 'not yet billed' unless $self->setup;
1048 return 'one-time charge' if $freq =~ /^(0|$)/;
1054 Class method that returns the list of possible status strings for pacakges
1055 (see L<the status method|/status>). For example:
1057 @statuses = FS::cust_pkg->statuses();
1061 tie my %statuscolor, 'Tie::IxHash',
1062 'not yet billed' => '000000',
1063 'one-time charge' => '000000',
1064 'active' => '00CC00',
1065 'suspended' => 'FF9900',
1066 'cancelled' => 'FF0000',
1070 my $self = shift; #could be class...
1071 grep { $_ !~ /^(not yet billed)$/ } #this is a dumb status anyway
1072 # mayble split btw one-time vs. recur
1078 Returns a hex triplet color string for this package's status.
1084 $statuscolor{$self->status};
1089 Returns a list of lists, calling the label method for all services
1090 (see L<FS::cust_svc>) of this billing item.
1096 map { [ $_->label ] } $self->cust_svc;
1099 =item h_labels END_TIMESTAMP [ START_TIMESTAMP ]
1101 Like the labels method, but returns historical information on services that
1102 were active as of END_TIMESTAMP and (optionally) not cancelled before
1105 Returns a list of lists, calling the label method for all (historical) services
1106 (see L<FS::h_cust_svc>) of this billing item.
1112 map { [ $_->label(@_) ] } $self->h_cust_svc(@_);
1115 =item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ]
1117 Like h_labels, except returns a simple flat list, and shortens long
1118 (currently >5) lists of identical services to one line that lists the service
1119 label and the number of individual services rather than individual items.
1123 sub h_labels_short {
1127 #tie %labels, 'Tie::IxHash';
1128 push @{ $labels{$_->[0]} }, $_->[1]
1129 foreach $self->h_labels(@_);
1131 foreach my $label ( keys %labels ) {
1132 my @values = @{ $labels{$label} };
1133 my $num = scalar(@values);
1135 push @labels, "$label ($num)";
1137 push @labels, map { "$label: $_" } @values;
1147 Returns the parent customer object (see L<FS::cust_main>).
1153 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
1156 =item seconds_since TIMESTAMP
1158 Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
1159 package have been online since TIMESTAMP, according to the session monitor.
1161 TIMESTAMP is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1162 L<Time::Local> and L<Date::Parse> for conversion functions.
1167 my($self, $since) = @_;
1170 foreach my $cust_svc (
1171 grep { $_->part_svc->svcdb eq 'svc_acct' } $self->cust_svc
1173 $seconds += $cust_svc->seconds_since($since);
1180 =item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
1182 Returns the numbers of seconds all accounts (see L<FS::svc_acct>) in this
1183 package have been online between TIMESTAMP_START (inclusive) and TIMESTAMP_END
1186 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1187 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1193 sub seconds_since_sqlradacct {
1194 my($self, $start, $end) = @_;
1198 foreach my $cust_svc (
1200 my $part_svc = $_->part_svc;
1201 $part_svc->svcdb eq 'svc_acct'
1202 && scalar($part_svc->part_export('sqlradius'));
1205 $seconds += $cust_svc->seconds_since_sqlradacct($start, $end);
1212 =item attribute_since_sqlradacct TIMESTAMP_START TIMESTAMP_END ATTRIBUTE
1214 Returns the sum of the given attribute for all accounts (see L<FS::svc_acct>)
1215 in this package for sessions ending between TIMESTAMP_START (inclusive) and
1219 TIMESTAMP_START and TIMESTAMP_END are specified as UNIX timestamps; see
1220 L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for conversion
1225 sub attribute_since_sqlradacct {
1226 my($self, $start, $end, $attrib) = @_;
1230 foreach my $cust_svc (
1232 my $part_svc = $_->part_svc;
1233 $part_svc->svcdb eq 'svc_acct'
1234 && scalar($part_svc->part_export('sqlradius'));
1237 $sum += $cust_svc->attribute_since_sqlradacct($start, $end, $attrib);
1244 =item transfer DEST_PKGNUM | DEST_CUST_PKG, [ OPTION => VALUE ... ]
1246 Transfers as many services as possible from this package to another package.
1248 The destination package can be specified by pkgnum by passing an FS::cust_pkg
1249 object. The destination package must already exist.
1251 Services are moved only if the destination allows services with the correct
1252 I<svcpart> (not svcdb), unless the B<change_svcpart> option is set true. Use
1253 this option with caution! No provision is made for export differences
1254 between the old and new service definitions. Probably only should be used
1255 when your exports for all service definitions of a given svcdb are identical.
1256 (attempt a transfer without it first, to move all possible svcpart-matching
1259 Any services that can't be moved remain in the original package.
1261 Returns an error, if there is one; otherwise, returns the number of services
1262 that couldn't be moved.
1267 my ($self, $dest_pkgnum, %opt) = @_;
1273 if (ref ($dest_pkgnum) eq 'FS::cust_pkg') {
1274 $dest = $dest_pkgnum;
1275 $dest_pkgnum = $dest->pkgnum;
1277 $dest = qsearchs('cust_pkg', { pkgnum => $dest_pkgnum });
1280 return ('Package does not exist: '.$dest_pkgnum) unless $dest;
1282 foreach my $pkg_svc ( $dest->part_pkg->pkg_svc ) {
1283 $target{$pkg_svc->svcpart} = $pkg_svc->quantity;
1286 foreach my $cust_svc ($dest->cust_svc) {
1287 $target{$cust_svc->svcpart}--;
1290 my %svcpart2svcparts = ();
1291 if ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
1292 warn "change_svcpart option received, creating alternates list\n" if $DEBUG;
1293 foreach my $svcpart ( map { $_->svcpart } $self->cust_svc ) {
1294 next if exists $svcpart2svcparts{$svcpart};
1295 my $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
1296 $svcpart2svcparts{$svcpart} = [
1298 sort { $b->[1] cmp $a->[1] or $a->[2] <=> $b->[2] }
1300 my $pkg_svc = qsearchs( 'pkg_svc', { 'pkgpart' => $dest->pkgpart,
1301 'svcpart' => $_ } );
1303 $pkg_svc ? $pkg_svc->primary_svc : '',
1304 $pkg_svc ? $pkg_svc->quantity : 0,
1308 grep { $_ != $svcpart }
1310 qsearch('part_svc', { 'svcdb' => $part_svc->svcdb } )
1312 warn "alternates for svcpart $svcpart: ".
1313 join(', ', @{$svcpart2svcparts{$svcpart}}). "\n"
1318 foreach my $cust_svc ($self->cust_svc) {
1319 if($target{$cust_svc->svcpart} > 0) {
1320 $target{$cust_svc->svcpart}--;
1321 my $new = new FS::cust_svc { $cust_svc->hash };
1322 $new->pkgnum($dest_pkgnum);
1323 my $error = $new->replace($cust_svc);
1324 return $error if $error;
1325 } elsif ( exists $opt{'change_svcpart'} && $opt{'change_svcpart'} ) {
1327 warn "looking for alternates for svcpart ". $cust_svc->svcpart. "\n";
1328 warn "alternates to consider: ".
1329 join(', ', @{$svcpart2svcparts{$cust_svc->svcpart}}). "\n";
1331 my @alternate = grep {
1332 warn "considering alternate svcpart $_: ".
1333 "$target{$_} available in new package\n"
1336 } @{$svcpart2svcparts{$cust_svc->svcpart}};
1338 warn "alternate(s) found\n" if $DEBUG;
1339 my $change_svcpart = $alternate[0];
1340 $target{$change_svcpart}--;
1341 my $new = new FS::cust_svc { $cust_svc->hash };
1342 $new->svcpart($change_svcpart);
1343 $new->pkgnum($dest_pkgnum);
1344 my $error = $new->replace($cust_svc);
1345 return $error if $error;
1358 This method is deprecated. See the I<depend_jobnum> option to the insert and
1359 order_pkgs methods in FS::cust_main for a better way to defer provisioning.
1366 local $SIG{HUP} = 'IGNORE';
1367 local $SIG{INT} = 'IGNORE';
1368 local $SIG{QUIT} = 'IGNORE';
1369 local $SIG{TERM} = 'IGNORE';
1370 local $SIG{TSTP} = 'IGNORE';
1371 local $SIG{PIPE} = 'IGNORE';
1373 my $oldAutoCommit = $FS::UID::AutoCommit;
1374 local $FS::UID::AutoCommit = 0;
1377 foreach my $cust_svc ( $self->cust_svc ) {
1378 #false laziness w/svc_Common::insert
1379 my $svc_x = $cust_svc->svc_x;
1380 foreach my $part_export ( $cust_svc->part_svc->part_export ) {
1381 my $error = $part_export->export_insert($svc_x);
1383 $dbh->rollback if $oldAutoCommit;
1389 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1396 =head1 CLASS METHODS
1402 Returns an SQL expression identifying recurring packages.
1406 sub recurring_sql { "
1407 '0' != ( select freq from part_pkg
1408 where cust_pkg.pkgpart = part_pkg.pkgpart )
1413 Returns an SQL expression identifying one-time packages.
1418 '0' = ( select freq from part_pkg
1419 where cust_pkg.pkgpart = part_pkg.pkgpart )
1424 Returns an SQL expression identifying active packages.
1429 ". $_[0]->recurring_sql(). "
1430 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1431 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
1436 Returns an SQL expression identifying inactive packages (one-time packages
1437 that are otherwise unsuspended/uncancelled).
1441 sub inactive_sql { "
1442 ". $_[0]->onetime_sql(). "
1443 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1444 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
1450 Returns an SQL expression identifying suspended packages.
1454 sub suspended_sql { susp_sql(@_); }
1456 #$_[0]->recurring_sql(). ' AND '.
1458 ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
1459 AND cust_pkg.susp IS NOT NULL AND cust_pkg.susp != 0
1466 Returns an SQL exprression identifying cancelled packages.
1470 sub cancelled_sql { cancel_sql(@_); }
1472 #$_[0]->recurring_sql(). ' AND '.
1473 "cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0";
1476 =item search_sql HREF
1478 Returns a qsearch hash expression to search for parameters specified in HREF.
1479 Valid parameters are
1483 =item magic - /^(active|inactive|suspended|cancell?ed)$/
1484 =item status - /^(active|inactive|suspended|one-time charge|inactive|cancell?ed)$/
1486 =item pkgpart - list specified how?
1487 =item setup - arrayref of beginning and ending epoch date
1488 =item last_bill - arrayref of beginning and ending epoch date
1489 =item bill - arrayref of beginning and ending epoch date
1490 =item adjourn - arrayref of beginning and ending epoch date
1491 =item susp - arrayref of beginning and ending epoch date
1492 =item expire - arrayref of beginning and ending epoch date
1493 =item cancel - arrayref of beginning and ending epoch date
1494 =item query - /^(pkgnum/APKG_pkgnum)$/
1495 =item cust_fields - a value suited to passing to FS::UI::Web::cust_header
1496 =item CurrentUser - specifies the user for agent virtualization
1502 my ($class, $params) = @_;
1509 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
1518 if ( $params->{'magic'} eq 'active'
1519 || $params->{'status'} eq 'active' ) {
1521 push @where, FS::cust_pkg->active_sql();
1523 } elsif ( $params->{'magic'} eq 'inactive'
1524 || $params->{'status'} eq 'inactive' ) {
1526 push @where, FS::cust_pkg->inactive_sql();
1528 } elsif ( $params->{'magic'} eq 'suspended'
1529 || $params->{'status'} eq 'suspended' ) {
1531 push @where, FS::cust_pkg->suspended_sql();
1533 } elsif ( $params->{'magic'} =~ /^cancell?ed$/
1534 || $params->{'status'} =~ /^cancell?ed$/ ) {
1536 push @where, FS::cust_pkg->cancelled_sql();
1538 } elsif ( $params->{'status'} =~ /^(one-time charge|inactive)$/ ) {
1540 push @where, FS::cust_pkg->inactive_sql();
1545 # parse package class
1548 #false lazinessish w/graph/cust_bill_pkg.cgi
1551 if ( exists($params->{'classnum'})
1552 && $params->{'classnum'} =~ /^(\d*)$/
1556 if ( $classnum ) { #a specific class
1557 push @where, "classnum = $classnum";
1559 #@pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) );
1560 #die "classnum $classnum not found!" unless $pkg_class[0];
1561 #$title .= $pkg_class[0]->classname.' ';
1563 } elsif ( $classnum eq '' ) { #the empty class
1565 push @where, "classnum IS NULL";
1566 #$title .= 'Empty class ';
1567 #@pkg_class = ( '(empty class)' );
1568 } elsif ( $classnum eq '0' ) {
1569 #@pkg_class = qsearch('pkg_class', {} ); # { 'disabled' => '' } );
1570 #push @pkg_class, '(empty class)';
1572 die "illegal classnum";
1581 my $pkgpart = join (' OR pkgpart=',
1582 grep {$_} map { /^(\d+)$/; } ($params->{'pkgpart'}));
1583 push @where, '(pkgpart=' . $pkgpart . ')' if $pkgpart;
1591 #false laziness w/report_cust_pkg.html
1594 'one-time charge' => { 'last_bill'=>1, 'bill'=>1, 'adjourn'=>1, 'susp'=>1, 'expire'=>1, 'cancel'=>1, },
1595 'active' => { 'susp'=>1, 'cancel'=>1 },
1596 'suspended' => { 'cancel' => 1 },
1601 foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
1603 next unless exists($params->{$field});
1605 my($beginning, $ending) = @{$params->{$field}};
1607 next if $beginning == 0 && $ending == 4294967295;
1610 "cust_pkg.$field IS NOT NULL",
1611 "cust_pkg.$field >= $beginning",
1612 "cust_pkg.$field <= $ending";
1614 $orderby ||= "ORDER BY cust_pkg.$field";
1618 $orderby ||= 'ORDER BY bill';
1621 # parse magic, legacy, etc.
1624 if ( $params->{'magic'} &&
1625 $params->{'magic'} =~ /^(active|inactive|suspended|cancell?ed)$/
1628 $orderby = 'ORDER BY pkgnum';
1630 if ( $params->{'pkgpart'} =~ /^(\d+)$/ ) {
1631 push @where, "pkgpart = $1";
1634 } elsif ( $params->{'query'} eq 'pkgnum' ) {
1636 $orderby = 'ORDER BY pkgnum';
1638 } elsif ( $params->{'query'} eq 'APKG_pkgnum' ) {
1640 $orderby = 'ORDER BY pkgnum';
1643 SELECT count(*) FROM pkg_svc
1644 WHERE pkg_svc.pkgpart = cust_pkg.pkgpart
1645 AND pkg_svc.quantity > ( SELECT count(*) FROM cust_svc
1646 WHERE cust_svc.pkgnum = cust_pkg.pkgnum
1647 AND cust_svc.svcpart = pkg_svc.svcpart
1654 # setup queries, links, subs, etc. for the search
1657 # here is the agent virtualization
1658 if ($params->{CurrentUser}) {
1660 qsearchs('access_user', { username => $params->{CurrentUser} });
1663 push @where, $access_user->agentnums_sql;
1668 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
1671 my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
1673 my $addl_from = 'LEFT JOIN cust_main USING ( custnum ) '.
1674 'LEFT JOIN part_pkg USING ( pkgpart ) '.
1675 'LEFT JOIN pkg_class USING ( classnum ) ';
1677 my $count_query = "SELECT COUNT(*) FROM cust_pkg $addl_from $extra_sql";
1680 'table' => 'cust_pkg',
1682 'select' => join(', ',
1684 ( map "part_pkg.$_", qw( pkg freq ) ),
1685 'pkg_class.classname',
1686 'cust_main.custnum as cust_main_custnum',
1687 FS::UI::Web::cust_sql_fields(
1688 $params->{'cust_fields'}
1691 'extra_sql' => "$extra_sql $orderby",
1692 'addl_from' => $addl_from,
1693 'count_query' => $count_query,
1702 =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ] ]
1704 CUSTNUM is a customer (see L<FS::cust_main>)
1706 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
1707 L<FS::part_pkg>) to order for this customer. Duplicates are of course
1710 REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
1711 remove for this customer. The services (see L<FS::cust_svc>) are moved to the
1712 new billing items. An error is returned if this is not possible (see
1713 L<FS::pkg_svc>). An empty arrayref is equivalent to not specifying this
1716 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
1717 newly-created cust_pkg objects.
1722 my ($custnum, $pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
1724 my $conf = new FS::Conf;
1726 # Transactionize this whole mess
1727 local $SIG{HUP} = 'IGNORE';
1728 local $SIG{INT} = 'IGNORE';
1729 local $SIG{QUIT} = 'IGNORE';
1730 local $SIG{TERM} = 'IGNORE';
1731 local $SIG{TSTP} = 'IGNORE';
1732 local $SIG{PIPE} = 'IGNORE';
1734 my $oldAutoCommit = $FS::UID::AutoCommit;
1735 local $FS::UID::AutoCommit = 0;
1739 my $cust_main = qsearchs('cust_main', { custnum => $custnum });
1740 return "Customer not found: $custnum" unless $cust_main;
1742 my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
1745 my $change = scalar(@old_cust_pkg) != 0;
1748 if ( scalar(@old_cust_pkg) == 1 && scalar(@$pkgparts) == 1 ) {
1752 #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( last_bill bill );
1754 #$hash{$_} = $old_cust_pkg[0]->$_() foreach qw( setup );
1755 $hash{'setup'} = $time if $old_cust_pkg[0]->setup;
1757 $hash{'change_date'} = $time;
1758 $hash{"change_$_"} = $old_cust_pkg[0]->$_() foreach qw( pkgnum pkgpart );
1761 # Create the new packages.
1762 foreach my $pkgpart (@$pkgparts) {
1763 my $cust_pkg = new FS::cust_pkg { custnum => $custnum,
1764 pkgpart => $pkgpart,
1767 $error = $cust_pkg->insert( 'change' => $change );
1769 $dbh->rollback if $oldAutoCommit;
1772 push @$return_cust_pkg, $cust_pkg;
1774 # $return_cust_pkg now contains refs to all of the newly
1777 # Transfer services and cancel old packages.
1778 foreach my $old_pkg (@old_cust_pkg) {
1780 foreach my $new_pkg (@$return_cust_pkg) {
1781 $error = $old_pkg->transfer($new_pkg);
1782 if ($error and $error == 0) {
1783 # $old_pkg->transfer failed.
1784 $dbh->rollback if $oldAutoCommit;
1789 if ( $error > 0 && $conf->exists('cust_pkg-change_svcpart') ) {
1790 warn "trying transfer again with change_svcpart option\n" if $DEBUG;
1791 foreach my $new_pkg (@$return_cust_pkg) {
1792 $error = $old_pkg->transfer($new_pkg, 'change_svcpart'=>1 );
1793 if ($error and $error == 0) {
1794 # $old_pkg->transfer failed.
1795 $dbh->rollback if $oldAutoCommit;
1802 # Transfers were successful, but we went through all of the
1803 # new packages and still had services left on the old package.
1804 # We can't cancel the package under the circumstances, so abort.
1805 $dbh->rollback if $oldAutoCommit;
1806 return "Unable to transfer all services from package ".$old_pkg->pkgnum;
1808 $error = $old_pkg->cancel( quiet=>1 );
1814 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1818 =item bulk_change PKGPARTS_ARYREF, REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ]
1820 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
1821 L<FS::part_pkg>) to order for this customer. Duplicates are of course
1824 REMOVE_PKGNUMS is an list of pkgnums specifying the billing items to
1825 replace. The services (see L<FS::cust_svc>) are moved to the
1826 new billing items. An error is returned if this is not possible (see
1829 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
1830 newly-created cust_pkg objects.
1835 my ($pkgparts, $remove_pkgnum, $return_cust_pkg) = @_;
1837 # Transactionize this whole mess
1838 local $SIG{HUP} = 'IGNORE';
1839 local $SIG{INT} = 'IGNORE';
1840 local $SIG{QUIT} = 'IGNORE';
1841 local $SIG{TERM} = 'IGNORE';
1842 local $SIG{TSTP} = 'IGNORE';
1843 local $SIG{PIPE} = 'IGNORE';
1845 my $oldAutoCommit = $FS::UID::AutoCommit;
1846 local $FS::UID::AutoCommit = 0;
1850 my @old_cust_pkg = map { qsearchs('cust_pkg', { pkgnum => $_ }) }
1853 while(scalar(@old_cust_pkg)) {
1855 my $custnum = $old_cust_pkg[0]->custnum;
1856 my (@remove) = map { $_->pkgnum }
1857 grep { $_->custnum == $custnum } @old_cust_pkg;
1858 @old_cust_pkg = grep { $_->custnum != $custnum } @old_cust_pkg;
1860 my $error = order $custnum, $pkgparts, \@remove, \@return;
1862 push @errors, $error
1864 push @$return_cust_pkg, @return;
1867 if (scalar(@errors)) {
1868 $dbh->rollback if $oldAutoCommit;
1869 return join(' / ', @errors);
1872 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1877 my ($self, %options) = @_;
1879 my $otaker = $FS::CurrentUser::CurrentUser->username;
1881 my $cust_pkg_reason =
1882 new FS::cust_pkg_reason({ 'pkgnum' => $self->pkgnum,
1883 'reasonnum' => $options{'reason'},
1884 'otaker' => $otaker,
1885 'date' => $options{'date'}
1889 return $cust_pkg_reason->insert;
1892 =item set_usage USAGE_VALUE_HASHREF
1894 USAGE_VALUE_HASHREF is a hashref of svc_acct usage columns and the amounts
1895 to which they should be set (see L<FS::svc_acct>). Currently seconds,
1896 upbytes, downbytes, and totalbytes are appropriate keys.
1898 All svc_accts which are part of this package have their values reset.
1903 my ($self, $valueref) = @_;
1905 foreach my $cust_svc ($self->cust_svc){
1906 my $svc_x = $cust_svc->svc_x;
1907 $svc_x->set_usage($valueref)
1908 if $svc_x->can("set_usage");
1916 sub order is not OO. Perhaps it should be moved to FS::cust_main and made so?
1918 In sub order, the @pkgparts array (passed by reference) is clobbered.
1920 Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard
1921 method to pass dates to the recur_prog expression, it should do so.
1923 FS::svc_acct, FS::svc_domain, FS::svc_www, FS::svc_ip and FS::svc_forward are
1924 loaded via 'use' at compile time, rather than via 'require' in sub { setup,
1925 suspend, unsuspend, cancel } because they use %FS::UID::callback to load
1926 configuration values. Probably need a subroutine which decides what to do
1927 based on whether or not we've fetched the user yet, rather than a hash. See
1928 FS::UID and the TODO.
1930 Now that things are transactional should the check in the insert method be
1935 L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>,
1936 L<FS::pkg_svc>, schema.html from the base documentation