5 use FS::UID qw( getotaker dbh );
6 use FS::Record qw( qsearch qsearchs );
13 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
15 # because they load configuraion by setting FS::UID::callback (see TODO)
21 @ISA = qw( FS::Record );
25 FS::cust_pkg - Object methods for cust_pkg objects
31 $record = new FS::cust_pkg \%hash;
32 $record = new FS::cust_pkg { 'column' => 'value' };
34 $error = $record->insert;
36 $error = $new_record->replace($old_record);
38 $error = $record->delete;
40 $error = $record->check;
42 $error = $record->cancel;
44 $error = $record->suspend;
46 $error = $record->unsuspend;
48 $part_pkg = $record->part_pkg;
50 @labels = $record->labels;
52 $error = FS::cust_pkg::order( $custnum, \@pkgparts );
53 $error = FS::cust_pkg::order( $custnum, \@pkgparts, \@remove_pkgnums ] );
57 An FS::cust_pkg object represents a customer billing item. FS::cust_pkg
58 inherits from FS::Record. The following fields are currently supported:
62 =item pkgnum - primary key (assigned automatically for new billing items)
64 =item custnum - Customer (see L<FS::cust_main>)
66 =item pkgpart - Billing item definition (see L<FS::part_pkg>)
78 =item otaker - order taker (assigned automatically if null, see L<FS::UID>)
80 =item manual_flag - If this field is set to 1, disables the automatic
81 unsuspensiond of this package when using the B<unsuspendauto> config file.
85 Note: setup, bill, susp, expire and cancel are specified as UNIX timestamps;
86 see L<perlfunc/"time">. Also see L<Time::Local> and L<Date::Parse> for
95 Create a new billing item. To add the item to the database, see L<"insert">.
99 sub table { 'cust_pkg'; }
103 Adds this billing item to the database ("Orders" the item). If there is an
104 error, returns the error, otherwise returns false.
111 # custnum might not have have been defined in sub check (for one-shot new
112 # customers), so check it here instead
113 # (is this still necessary with transactions?)
115 my $error = $self->ut_number('custnum');
116 return $error if $error;
118 return "Unknown customer ". $self->custnum unless $self->cust_main;
120 $self->SUPER::insert;
126 Currently unimplemented. You don't want to delete billing items, because there
127 would then be no record the customer ever purchased the item. Instead, see
133 return "Can't delete cust_pkg records!";
136 =item replace OLD_RECORD
138 Replaces the OLD_RECORD with this one in the database. If there is an error,
139 returns the error, otherwise returns false.
141 Currently, custnum, setup, bill, susp, expire, and cancel may be changed.
143 Changing pkgpart may have disasterous effects. See the order subroutine.
145 setup and bill are normally updated by calling the bill method of a customer
146 object (see L<FS::cust_main>).
148 suspend is normally updated by the suspend and unsuspend methods.
150 cancel is normally updated by the cancel method (and also the order subroutine
156 my( $new, $old ) = ( shift, shift );
158 #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
159 return "Can't change otaker!" if $old->otaker ne $new->otaker;
160 return "Can't change setup once it exists!"
161 if $old->getfield('setup') &&
162 $old->getfield('setup') != $new->getfield('setup');
163 #some logic for bill, susp, cancel?
165 $new->SUPER::replace($old);
170 Checks all fields to make sure this is a valid billing item. If there is an
171 error, returns the error, otherwise returns false. Called by the insert and
180 $self->ut_numbern('pkgnum')
181 || $self->ut_numbern('custnum')
182 || $self->ut_number('pkgpart')
183 || $self->ut_numbern('setup')
184 || $self->ut_numbern('bill')
185 || $self->ut_numbern('susp')
186 || $self->ut_numbern('cancel')
188 return $error if $error;
190 if ( $self->custnum ) {
191 return "Unknown customer ". $self->custnum unless $self->cust_main;
194 return "Unknown pkgpart"
195 unless qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
197 $self->otaker(getotaker) unless $self->otaker;
198 $self->otaker =~ /^(\w{0,16})$/ or return "Illegal otaker";
201 if ( $self->dbdef_table->column('manual_flag') ) {
202 $self->manual_flag =~ /^([01]?)$/ or return "Illegal manual_flag";
203 $self->manual_flag($1);
211 Cancels and removes all services (see L<FS::cust_svc> and L<FS::part_svc>)
212 in this package, then cancels the package itself (sets the cancel field to
215 If there is an error, returns the error, otherwise returns false.
223 local $SIG{HUP} = 'IGNORE';
224 local $SIG{INT} = 'IGNORE';
225 local $SIG{QUIT} = 'IGNORE';
226 local $SIG{TERM} = 'IGNORE';
227 local $SIG{TSTP} = 'IGNORE';
228 local $SIG{PIPE} = 'IGNORE';
230 my $oldAutoCommit = $FS::UID::AutoCommit;
231 local $FS::UID::AutoCommit = 0;
234 foreach my $cust_svc (
235 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
237 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
239 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
240 $dbh->rollback if $oldAutoCommit;
241 return "Illegal svcdb value in part_svc!";
244 require "FS/$svcdb.pm";
246 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
248 $error = $svc->cancel;
250 $dbh->rollback if $oldAutoCommit;
251 return "Error cancelling service: $error"
253 $error = $svc->delete;
255 $dbh->rollback if $oldAutoCommit;
256 return "Error deleting service: $error";
260 $error = $cust_svc->delete;
262 $dbh->rollback if $oldAutoCommit;
263 return "Error deleting cust_svc: $error";
268 unless ( $self->getfield('cancel') ) {
269 my %hash = $self->hash;
270 $hash{'cancel'} = time;
271 my $new = new FS::cust_pkg ( \%hash );
272 $error = $new->replace($self);
274 $dbh->rollback if $oldAutoCommit;
279 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
286 Suspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
287 package, then suspends the package itself (sets the susp field to now).
289 If there is an error, returns the error, otherwise returns false.
297 local $SIG{HUP} = 'IGNORE';
298 local $SIG{INT} = 'IGNORE';
299 local $SIG{QUIT} = 'IGNORE';
300 local $SIG{TERM} = 'IGNORE';
301 local $SIG{TSTP} = 'IGNORE';
302 local $SIG{PIPE} = 'IGNORE';
304 my $oldAutoCommit = $FS::UID::AutoCommit;
305 local $FS::UID::AutoCommit = 0;
308 foreach my $cust_svc (
309 qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
311 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
313 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
314 $dbh->rollback if $oldAutoCommit;
315 return "Illegal svcdb value in part_svc!";
318 require "FS/$svcdb.pm";
320 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
322 $error = $svc->suspend;
324 $dbh->rollback if $oldAutoCommit;
331 unless ( $self->getfield('susp') ) {
332 my %hash = $self->hash;
333 $hash{'susp'} = time;
334 my $new = new FS::cust_pkg ( \%hash );
335 $error = $new->replace($self);
337 $dbh->rollback if $oldAutoCommit;
342 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
349 Unsuspends all services (see L<FS::cust_svc> and L<FS::part_svc>) in this
350 package, then unsuspends the package itself (clears the susp field).
352 If there is an error, returns the error, otherwise returns false.
360 local $SIG{HUP} = 'IGNORE';
361 local $SIG{INT} = 'IGNORE';
362 local $SIG{QUIT} = 'IGNORE';
363 local $SIG{TERM} = 'IGNORE';
364 local $SIG{TSTP} = 'IGNORE';
365 local $SIG{PIPE} = 'IGNORE';
367 my $oldAutoCommit = $FS::UID::AutoCommit;
368 local $FS::UID::AutoCommit = 0;
371 foreach my $cust_svc (
372 qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } )
374 my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $cust_svc->svcpart } );
376 $part_svc->svcdb =~ /^([\w\-]+)$/ or do {
377 $dbh->rollback if $oldAutoCommit;
378 return "Illegal svcdb value in part_svc!";
381 require "FS/$svcdb.pm";
383 my $svc = qsearchs( $svcdb, { 'svcnum' => $cust_svc->svcnum } );
385 $error = $svc->unsuspend;
387 $dbh->rollback if $oldAutoCommit;
394 unless ( ! $self->getfield('susp') ) {
395 my %hash = $self->hash;
397 my $new = new FS::cust_pkg ( \%hash );
398 $error = $new->replace($self);
400 $dbh->rollback if $oldAutoCommit;
405 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
412 Returns the definition for this billing item, as an FS::part_pkg object (see
419 qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
424 Returns a list of lists, calling the label method for all services
425 (see L<FS::cust_svc>) of this billing item.
431 map { [ $_->label ] } qsearch ( 'cust_svc', { 'pkgnum' => $self->pkgnum } );
436 Returns the parent customer object (see L<FS::cust_main>).
442 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
451 =item order CUSTNUM, PKGPARTS_ARYREF, [ REMOVE_PKGNUMS_ARYREF [ RETURN_CUST_PKG_ARRAYREF ] ]
453 CUSTNUM is a customer (see L<FS::cust_main>)
455 PKGPARTS is a list of pkgparts specifying the the billing item definitions (see
456 L<FS::part_pkg>) to order for this customer. Duplicates are of course
459 REMOVE_PKGNUMS is an optional list of pkgnums specifying the billing items to
460 remove for this customer. The services (see L<FS::cust_svc>) are moved to the
461 new billing items. An error is returned if this is not possible (see
462 L<FS::pkg_svc>). An empty arrayref is equivalent to not specifying this
465 RETURN_CUST_PKG_ARRAYREF, if specified, will be filled in with the
466 newly-created cust_pkg objects.
471 my($custnum, $pkgparts, $remove_pkgnums, $return_cust_pkg) = @_;
472 $remove_pkgnums = [] unless defined($remove_pkgnums);
474 my $oldAutoCommit = $FS::UID::AutoCommit;
475 local $FS::UID::AutoCommit = 0;
479 # $part_pkg{$pkgpart} is true iff $custnum may purchase $pkgpart
481 my($cust_main)=qsearchs('cust_main',{'custnum'=>$custnum});
482 my($agent)=qsearchs('agent',{'agentnum'=> $cust_main->agentnum });
483 my %part_pkg = %{ $agent->pkgpart_hashref };
487 # for those packages being removed:
488 #@{ $svcnum{$svcpart} } goes from a svcpart to a list of FS::Record
489 # objects (table eq 'cust_svc')
491 foreach $pkgnum ( @{$remove_pkgnums} ) {
493 foreach $cust_svc (qsearch('cust_svc',{'pkgnum'=>$pkgnum})) {
494 push @{ $svcnum{$cust_svc->getfield('svcpart')} }, $cust_svc;
500 # for those packages the customer is purchasing:
501 # @{$pkgparts} is a list of said packages, by pkgpart
502 # @cust_svc is a corresponding list of lists of FS::Record objects
504 foreach $pkgpart ( @{$pkgparts} ) {
505 unless ( $part_pkg{$pkgpart} ) {
506 $dbh->rollback if $oldAutoCommit;
507 return "Customer not permitted to purchase pkgpart $pkgpart!";
511 ( $svcnum{$_} && @{ $svcnum{$_} } ) ? shift @{ $svcnum{$_} } : ();
512 } map { $_->svcpart } qsearch('pkg_svc', { 'pkgpart' => $pkgpart })
516 #check for leftover services
517 foreach (keys %svcnum) {
518 next unless @{ $svcnum{$_} };
519 $dbh->rollback if $oldAutoCommit;
520 return "Leftover services, svcpart $_: svcnum ".
521 join(', ', map { $_->svcnum } @{ $svcnum{$_} } );
524 #no leftover services, let's make changes.
526 local $SIG{HUP} = 'IGNORE';
527 local $SIG{INT} = 'IGNORE';
528 local $SIG{QUIT} = 'IGNORE';
529 local $SIG{TERM} = 'IGNORE';
530 local $SIG{TSTP} = 'IGNORE';
531 local $SIG{PIPE} = 'IGNORE';
533 #first cancel old packages
535 foreach $pkgnum ( @{$remove_pkgnums} ) {
536 my($old) = qsearchs('cust_pkg',{'pkgnum'=>$pkgnum});
538 $dbh->rollback if $oldAutoCommit;
539 return "Package $pkgnum not found to remove!";
541 my(%hash) = $old->hash;
542 $hash{'cancel'}=time;
543 my($new) = new FS::cust_pkg ( \%hash );
544 my($error)=$new->replace($old);
546 $dbh->rollback if $oldAutoCommit;
547 return "Couldn't update package $pkgnum: $error";
551 #now add new packages, changing cust_svc records if necessary
553 while ($pkgpart=shift @{$pkgparts} ) {
555 my $new = new FS::cust_pkg {
556 'custnum' => $custnum,
557 'pkgpart' => $pkgpart,
559 my $error = $new->insert;
561 $dbh->rollback if $oldAutoCommit;
562 return "Couldn't insert new cust_pkg record: $error";
564 push @{$return_cust_pkg}, $new if $return_cust_pkg;
565 my $pkgnum = $new->pkgnum;
567 foreach my $cust_svc ( @{ shift @cust_svc } ) {
568 my(%hash) = $cust_svc->hash;
569 $hash{'pkgnum'}=$pkgnum;
570 my($new) = new FS::cust_svc ( \%hash );
571 my($error)=$new->replace($cust_svc);
573 $dbh->rollback if $oldAutoCommit;
574 return "Couldn't link old service to new package: $error";
579 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
588 $Id: cust_pkg.pm,v 1.10 2001-10-15 12:16:42 ivan Exp $
592 sub order is not OO. Perhaps it should be moved to FS::cust_main and made so?
594 In sub order, the @pkgparts array (passed by reference) is clobbered.
596 Also in sub order, no money is adjusted. Once FS::part_pkg defines a standard
597 method to pass dates to the recur_prog expression, it should do so.
599 FS::svc_acct, FS::svc_acct_sm, and FS::svc_domain are loaded via 'use' at
600 compile time, rather than via 'require' in sub { setup, suspend, unsuspend,
601 cancel } because they use %FS::UID::callback to load configuration values.
602 Probably need a subroutine which decides what to do based on whether or not
603 we've fetched the user yet, rather than a hash. See FS::UID and the TODO.
605 Now that things are transactional should the check in the insert method be
610 L<FS::Record>, L<FS::cust_main>, L<FS::part_pkg>, L<FS::cust_svc>,
611 L<FS::pkg_svc>, schema.html from the base documentation