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->invoice_from_full( $self->cust_or_prospect->agentnum );
202 $self->SUPER::email( {
213 $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
216 #my $cust_main = $self->cust_main;
217 #my $name = $cust_main->name;
218 #my $name_short = $cust_main->name_short;
219 #my $invoice_number = $self->invnum;
220 #my $invoice_date = $self->_date_pretty;
225 =item cust_or_prosect
229 sub cust_or_prospect {
231 $self->custnum ? $self->cust_main : $self->prospect_main;
234 =item cust_or_prospect_label_link P
236 HTML links to either the customer or prospect.
238 Returns a list consisting of two elements. The first is a text label for the
239 link, and the second is the URL.
243 sub cust_or_prospect_label_link {
244 my( $self, $p ) = @_;
246 if ( my $custnum = $self->custnum ) {
247 my $display_custnum = $self->cust_main->display_custnum;
248 my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
250 : ';show=quotations';
252 emt("View this customer (#[_1])",$display_custnum) =>
253 "${p}view/cust_main.cgi?custnum=$custnum$target"
255 } elsif ( my $prospectnum = $self->prospectnum ) {
257 emt("View this prospect (#[_1])",$prospectnum) =>
258 "${p}view/prospect_main.html?$prospectnum"
266 #prevent things from falsely showing up as taxes, at least until we support
267 # quoting tax amounts..
272 shift->cust_bill_pkg;
276 my( $self, $total_items ) = @_;
278 if ( $self->total_setup > 0 ) {
279 push @$total_items, {
280 'total_item' => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
281 'total_amount' => $self->total_setup,
285 #could/should add up the different recurring frequencies on lines of their own
286 # but this will cover the 95% cases for now
287 if ( $self->total_recur > 0 ) {
288 push @$total_items, {
289 'total_item' => $self->mt('Total Recurring'),
290 'total_amount' => $self->total_recur,
296 =item enable_previous
300 sub enable_previous { 0 }
302 =item convert_cust_main
304 If this quotation already belongs to a customer, then returns that customer, as
305 an FS::cust_main object.
307 Otherwise, creates a new customer (FS::cust_main object and record, and
308 associated) based on this quotation's prospect, then orders this quotation's
309 packages as real packages for the customer.
311 If there is an error, returns an error message, otherwise, returns the
312 newly-created FS::cust_main object.
316 sub convert_cust_main {
319 my $cust_main = $self->cust_main;
320 return $cust_main if $cust_main; #already converted, don't again
322 my $oldAutoCommit = $FS::UID::AutoCommit;
323 local $FS::UID::AutoCommit = 0;
326 $cust_main = $self->prospect_main->convert_cust_main;
327 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
328 $dbh->rollback if $oldAutoCommit;
332 $self->prospectnum('');
333 $self->custnum( $cust_main->custnum );
334 my $error = $self->replace || $self->order;
336 $dbh->rollback if $oldAutoCommit;
340 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
348 This method is for use with quotations which are already associated with a customer.
350 Orders this quotation's packages as real packages for the customer.
352 If there is an error, returns an error message, otherwise returns false.
359 tie my %all_cust_pkg, 'Tie::RefHash';
360 foreach my $quotation_pkg ($self->quotation_pkg) {
361 my $cust_pkg = FS::cust_pkg->new;
362 foreach (qw(pkgpart locationnum start_date contract_end quantity waive_setup)) {
363 $cust_pkg->set( $_, $quotation_pkg->get($_) );
366 # currently only one discount each
367 my ($pkg_discount) = $quotation_pkg->quotation_pkg_discount;
368 if ( $pkg_discount ) {
369 $cust_pkg->set('discountnum', $pkg_discount->discountnum);
372 $all_cust_pkg{$cust_pkg} = []; # no services
375 $self->cust_main->order_pkgs( \%all_cust_pkg );
385 qsearch('quotation_pkg', { 'quotationnum' => $self->quotationnum } );
390 One-time charges, like FS::cust_main::charge()
394 #super false laziness w/cust_main::charge
397 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
398 my ( $pkg, $comment, $additional );
399 my ( $setuptax, $taxclass ); #internal taxes
400 my ( $taxproduct, $override ); #vendor (CCH) taxes
402 my $cust_pkg_ref = '';
403 my ( $bill_now, $invoice_terms ) = ( 0, '' );
405 if ( ref( $_[0] ) ) {
406 $amount = $_[0]->{amount};
407 $setup_cost = $_[0]->{setup_cost};
408 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
409 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
410 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
411 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
412 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
413 : '$'. sprintf("%.2f",$amount);
414 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
415 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
416 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
417 $additional = $_[0]->{additional} || [];
418 $taxproduct = $_[0]->{taxproductnum};
419 $override = { '' => $_[0]->{tax_override} };
420 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
421 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
422 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
423 $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
429 $pkg = @_ ? shift : 'One-time charge';
430 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
432 $taxclass = @_ ? shift : '';
436 local $SIG{HUP} = 'IGNORE';
437 local $SIG{INT} = 'IGNORE';
438 local $SIG{QUIT} = 'IGNORE';
439 local $SIG{TERM} = 'IGNORE';
440 local $SIG{TSTP} = 'IGNORE';
441 local $SIG{PIPE} = 'IGNORE';
443 my $oldAutoCommit = $FS::UID::AutoCommit;
444 local $FS::UID::AutoCommit = 0;
447 my $part_pkg = new FS::part_pkg ( {
449 'comment' => $comment,
453 'classnum' => ( $classnum ? $classnum : '' ),
454 'setuptax' => $setuptax,
455 'taxclass' => $taxclass,
456 'taxproductnum' => $taxproduct,
457 'setup_cost' => $setup_cost,
460 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
461 ( 0 .. @$additional - 1 )
463 'additional_count' => scalar(@$additional),
464 'setup_fee' => $amount,
467 my $error = $part_pkg->insert( options => \%options,
468 tax_overrides => $override,
471 $dbh->rollback if $oldAutoCommit;
475 my $pkgpart = $part_pkg->pkgpart;
478 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
480 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
481 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
482 $error = $type_pkgs->insert;
484 $dbh->rollback if $oldAutoCommit;
489 #except for DIFF, eveything above is idential to cust_main version
490 #but below is our own thing pretty much (adding a quotation package instead
491 # of ordering a customer package, no "bill now")
493 my $quotation_pkg = new FS::quotation_pkg ( {
494 'quotationnum' => $self->quotationnum,
495 'pkgpart' => $pkgpart,
496 'quantity' => $quantity,
497 #'start_date' => $start_date,
498 #'no_auto' => $no_auto,
499 'locationnum'=> $locationnum,
502 $error = $quotation_pkg->insert;
504 $dbh->rollback if $oldAutoCommit;
506 #} elsif ( $cust_pkg_ref ) {
507 # ${$cust_pkg_ref} = $cust_pkg;
510 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
517 Disables this quotation (sets disabled to Y, which hides the quotation on
518 prospects and customers).
520 If there is an error, returns an error message, otherwise returns false.
526 $self->disabled('Y');
532 Enables this quotation.
534 If there is an error, returns an error message, otherwise returns false.
551 =item search_sql_where HASHREF
553 Class method which returns an SQL WHERE fragment to search for parameters
554 specified in HASHREF. Valid parameters are
560 List reference of start date, end date, as UNIX timestamps.
570 List reference of charged limits (exclusive).
574 List reference of charged limits (exclusive).
578 flag, return open invoices only
582 flag, return net invoices only
590 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
594 sub search_sql_where {
595 my($class, $param) = @_;
597 # warn "$me search_sql_where called with params: \n".
598 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
604 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
605 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
609 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
610 # push @search, "cust_main.refnum = $1";
614 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
615 push @search, "quotation.prospectnum = $1";
619 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
620 push @search, "cust_bill.custnum = $1";
624 if ( $param->{_date} ) {
625 my($beginning, $ending) = @{$param->{_date}};
627 push @search, "quotation._date >= $beginning",
628 "quotation._date < $ending";
632 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
633 push @search, "quotation.quotationnum >= $1";
635 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
636 push @search, "quotation.quotationnum <= $1";
640 # if ( $param->{charged} ) {
641 # my @charged = ref($param->{charged})
642 # ? @{ $param->{charged} }
643 # : ($param->{charged});
645 # push @search, map { s/^charged/cust_bill.charged/; $_; }
649 my $owed_sql = FS::cust_bill->owed_sql;
652 push @search, "quotation._date < ". (time-86400*$param->{'days'})
655 #agent virtualization
656 my $curuser = $FS::CurrentUser::CurrentUser;
657 #false laziness w/search/quotation.html
658 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
659 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
662 join(' AND ', @search );
668 Return line item hashes for each package on this quotation. Differs from the
669 base L<FS::Template_Mixin> version in that it recalculates each quoted package
670 first, and doesn't implement the "condensed" option.
675 my ($self, %options) = @_;
676 my @quotation_pkg = $self->quotation_pkg;
677 foreach (@quotation_pkg) {
678 my $error = $_->estimate;
679 die "error calculating estimate for pkgpart " . $_->pkgpart.": $error\n"
683 # run it through the Template_Mixin engine
684 return $self->_items_cust_bill_pkg(\@quotation_pkg, %options);
693 L<FS::Record>, schema.html from the base documentation.