2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record );
8 use FS::Maketext qw( emt );
9 use FS::Record qw( qsearch qsearchs );
12 use FS::prospect_main;
13 use FS::quotation_pkg;
18 FS::quotation - Object methods for quotation records
24 $record = new FS::quotation \%hash;
25 $record = new FS::quotation { 'column' => 'value' };
27 $error = $record->insert;
29 $error = $new_record->replace($old_record);
31 $error = $record->delete;
33 $error = $record->check;
37 An FS::quotation object represents a quotation. FS::quotation inherits from
38 FS::Record. The following fields are currently supported:
75 Creates a new quotation. To add the quotation to the database, see L<"insert">.
77 Note that this stores the hash reference, not a distinct copy of the hash it
78 points to. You can ask the object for a copy with the I<hash> method.
82 sub table { 'quotation'; }
83 sub notice_name { 'Quotation'; }
84 sub template_conf { 'quotation_'; }
88 Adds this record to the database. If there is an error, returns the error,
89 otherwise returns false.
93 Delete this record from the database.
95 =item replace OLD_RECORD
97 Replaces the OLD_RECORD with this one in the database. If there is an error,
98 returns the error, otherwise returns false.
102 Checks all fields to make sure this is a valid quotation. If there is
103 an error, returns the error, otherwise returns false. Called by the insert
112 $self->ut_numbern('quotationnum')
113 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
114 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
115 || $self->ut_numbern('_date')
116 || $self->ut_enum('disabled', [ '', 'Y' ])
117 || $self->ut_numbern('usernum')
119 return $error if $error;
121 $self->_date(time) unless $self->_date;
123 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
125 return 'prospectnum or custnum must be specified'
126 if ! $self->prospectnum
138 qsearchs('prospect_main', { 'prospectnum' => $self->prospectnum } );
147 qsearchs('cust_main', { 'custnum' => $self->custnum } );
154 sub cust_bill_pkg { #actually quotation_pkg objects
156 qsearch('quotation_pkg', { quotationnum=>$self->quotationnum });
165 $self->_total('setup');
168 =item total_recur [ FREQ ]
174 #=item total_recur [ FREQ ]
175 #my $freq = @_ ? shift : '';
176 $self->_total('recur');
180 my( $self, $method ) = @_;
183 $total += $_->$method() for $self->cust_bill_pkg;
184 sprintf('%.2f', $total);
190 my $opt = shift || {};
191 if ($opt and !ref($opt)) {
192 die ref($self). '->email called with positional parameters';
195 my $conf = $self->conf;
197 my $from = delete $opt->{from};
199 # this is where we set the From: address
200 $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
201 || $conf->config('invoice_from', $self->cust_or_prospect->agentnum );
203 $self->SUPER::email( {
214 $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
217 #my $cust_main = $self->cust_main;
218 #my $name = $cust_main->name;
219 #my $name_short = $cust_main->name_short;
220 #my $invoice_number = $self->invnum;
221 #my $invoice_date = $self->_date_pretty;
226 =item cust_or_prosect
230 sub cust_or_prospect {
232 $self->custnum ? $self->cust_main : $self->prospect_main;
235 =item cust_or_prospect_label_link P
237 HTML links to either the customer or prospect.
239 Returns a list consisting of two elements. The first is a text label for the
240 link, and the second is the URL.
244 sub cust_or_prospect_label_link {
245 my( $self, $p ) = @_;
247 if ( my $custnum = $self->custnum ) {
248 my $display_custnum = $self->cust_main->display_custnum;
249 my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
251 : ';show=quotations';
253 emt("View this customer (#[_1])",$display_custnum) =>
254 "${p}view/cust_main.cgi?custnum=$custnum$target"
256 } elsif ( my $prospectnum = $self->prospectnum ) {
258 emt("View this prospect (#[_1])",$prospectnum) =>
259 "${p}view/prospect_main.html?$prospectnum"
267 #prevent things from falsely showing up as taxes, at least until we support
268 # quoting tax amounts..
273 shift->cust_bill_pkg;
277 my( $self, $total_items ) = @_;
279 if ( $self->total_setup > 0 ) {
280 push @$total_items, {
281 'total_item' => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
282 'total_amount' => $self->total_setup,
286 #could/should add up the different recurring frequencies on lines of their own
287 # but this will cover the 95% cases for now
288 if ( $self->total_recur > 0 ) {
289 push @$total_items, {
290 'total_item' => $self->mt('Total Recurring'),
291 'total_amount' => $self->total_recur,
297 =item enable_previous
301 sub enable_previous { 0 }
303 =item convert_cust_main
305 If this quotation already belongs to a customer, then returns that customer, as
306 an FS::cust_main object.
308 Otherwise, creates a new customer (FS::cust_main object and record, and
309 associated) based on this quotation's prospect, then orders this quotation's
310 packages as real packages for the customer.
312 If there is an error, returns an error message, otherwise, returns the
313 newly-created FS::cust_main object.
317 sub convert_cust_main {
320 my $cust_main = $self->cust_main;
321 return $cust_main if $cust_main; #already converted, don't again
323 my $oldAutoCommit = $FS::UID::AutoCommit;
324 local $FS::UID::AutoCommit = 0;
327 $cust_main = $self->prospect_main->convert_cust_main;
328 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
329 $dbh->rollback if $oldAutoCommit;
333 $self->prospectnum('');
334 $self->custnum( $cust_main->custnum );
335 my $error = $self->replace || $self->order;
337 $dbh->rollback if $oldAutoCommit;
341 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
349 This method is for use with quotations which are already associated with a customer.
351 Orders this quotation's packages as real packages for the customer.
353 If there is an error, returns an error message, otherwise returns false.
360 tie my %cust_pkg, 'Tie::RefHash',
361 map { FS::cust_pkg->new({ pkgpart => $_->pkgpart,
362 quantity => $_->quantity,
366 $self->quotation_pkg ;
368 $self->cust_main->order_pkgs( \%cust_pkg );
378 qsearch('quotation_pkg', { 'quotationnum' => $self->quotationnum } );
383 One-time charges, like FS::cust_main::charge()
387 #super false laziness w/cust_main::charge
390 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
391 my ( $pkg, $comment, $additional );
392 my ( $setuptax, $taxclass ); #internal taxes
393 my ( $taxproduct, $override ); #vendor (CCH) taxes
395 my $cust_pkg_ref = '';
396 my ( $bill_now, $invoice_terms ) = ( 0, '' );
398 if ( ref( $_[0] ) ) {
399 $amount = $_[0]->{amount};
400 $setup_cost = $_[0]->{setup_cost};
401 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
402 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
403 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
404 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
405 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
406 : '$'. sprintf("%.2f",$amount);
407 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
408 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
409 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
410 $additional = $_[0]->{additional} || [];
411 $taxproduct = $_[0]->{taxproductnum};
412 $override = { '' => $_[0]->{tax_override} };
413 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
414 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
415 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
416 $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
422 $pkg = @_ ? shift : 'One-time charge';
423 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
425 $taxclass = @_ ? shift : '';
429 local $SIG{HUP} = 'IGNORE';
430 local $SIG{INT} = 'IGNORE';
431 local $SIG{QUIT} = 'IGNORE';
432 local $SIG{TERM} = 'IGNORE';
433 local $SIG{TSTP} = 'IGNORE';
434 local $SIG{PIPE} = 'IGNORE';
436 my $oldAutoCommit = $FS::UID::AutoCommit;
437 local $FS::UID::AutoCommit = 0;
440 my $part_pkg = new FS::part_pkg ( {
442 'comment' => $comment,
446 'classnum' => ( $classnum ? $classnum : '' ),
447 'setuptax' => $setuptax,
448 'taxclass' => $taxclass,
449 'taxproductnum' => $taxproduct,
450 'setup_cost' => $setup_cost,
453 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
454 ( 0 .. @$additional - 1 )
456 'additional_count' => scalar(@$additional),
457 'setup_fee' => $amount,
460 my $error = $part_pkg->insert( options => \%options,
461 tax_overrides => $override,
464 $dbh->rollback if $oldAutoCommit;
468 my $pkgpart = $part_pkg->pkgpart;
471 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
473 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
474 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
475 $error = $type_pkgs->insert;
477 $dbh->rollback if $oldAutoCommit;
482 #except for DIFF, eveything above is idential to cust_main version
483 #but below is our own thing pretty much (adding a quotation package instead
484 # of ordering a customer package, no "bill now")
486 my $quotation_pkg = new FS::quotation_pkg ( {
487 'quotationnum' => $self->quotationnum,
488 'pkgpart' => $pkgpart,
489 'quantity' => $quantity,
490 #'start_date' => $start_date,
491 #'no_auto' => $no_auto,
492 'locationnum'=> $locationnum,
495 $error = $quotation_pkg->insert;
497 $dbh->rollback if $oldAutoCommit;
499 #} elsif ( $cust_pkg_ref ) {
500 # ${$cust_pkg_ref} = $cust_pkg;
503 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
510 Disables this quotation (sets disabled to Y, which hides the quotation on
511 prospects and customers).
513 If there is an error, returns an error message, otherwise returns false.
519 $self->disabled('Y');
525 Enables this quotation.
527 If there is an error, returns an error message, otherwise returns false.
544 =item search_sql_where HASHREF
546 Class method which returns an SQL WHERE fragment to search for parameters
547 specified in HASHREF. Valid parameters are
553 List reference of start date, end date, as UNIX timestamps.
563 List reference of charged limits (exclusive).
567 List reference of charged limits (exclusive).
571 flag, return open invoices only
575 flag, return net invoices only
583 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
587 sub search_sql_where {
588 my($class, $param) = @_;
590 # warn "$me search_sql_where called with params: \n".
591 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
597 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
598 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
602 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
603 # push @search, "cust_main.refnum = $1";
607 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
608 push @search, "quotation.prospectnum = $1";
612 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
613 push @search, "cust_bill.custnum = $1";
617 if ( $param->{_date} ) {
618 my($beginning, $ending) = @{$param->{_date}};
620 push @search, "quotation._date >= $beginning",
621 "quotation._date < $ending";
625 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
626 push @search, "quotation.quotationnum >= $1";
628 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
629 push @search, "quotation.quotationnum <= $1";
633 # if ( $param->{charged} ) {
634 # my @charged = ref($param->{charged})
635 # ? @{ $param->{charged} }
636 # : ($param->{charged});
638 # push @search, map { s/^charged/cust_bill.charged/; $_; }
642 my $owed_sql = FS::cust_bill->owed_sql;
645 push @search, "quotation._date < ". (time-86400*$param->{'days'})
648 #agent virtualization
649 my $curuser = $FS::CurrentUser::CurrentUser;
650 #false laziness w/search/quotation.html
651 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
652 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
655 join(' AND ', @search );
665 L<FS::Record>, schema.html from the base documentation.