2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record
9 use FS::Maketext qw( emt );
10 use FS::Record qw( qsearchs );
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
140 sub cust_bill_pkg { #actually quotation_pkg objects
141 shift->quotation_pkg(@_);
150 $self->_total('setup');
153 =item total_recur [ FREQ ]
159 #=item total_recur [ FREQ ]
160 #my $freq = @_ ? shift : '';
161 $self->_total('recur');
165 my( $self, $method ) = @_;
168 $total += $_->$method() for $self->cust_bill_pkg;
169 sprintf('%.2f', $total);
175 my $opt = shift || {};
176 if ($opt and !ref($opt)) {
177 die ref($self). '->email called with positional parameters';
180 my $conf = $self->conf;
182 my $from = delete $opt->{from};
184 # this is where we set the From: address
185 $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
186 || $conf->config('invoice_from', $self->cust_or_prospect->agentnum );
188 $self->SUPER::email( {
199 $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
202 #my $cust_main = $self->cust_main;
203 #my $name = $cust_main->name;
204 #my $name_short = $cust_main->name_short;
205 #my $invoice_number = $self->invnum;
206 #my $invoice_date = $self->_date_pretty;
211 =item cust_or_prosect
215 sub cust_or_prospect {
217 $self->custnum ? $self->cust_main : $self->prospect_main;
220 =item cust_or_prospect_label_link P
222 HTML links to either the customer or prospect.
224 Returns a list consisting of two elements. The first is a text label for the
225 link, and the second is the URL.
229 sub cust_or_prospect_label_link {
230 my( $self, $p ) = @_;
232 if ( my $custnum = $self->custnum ) {
233 my $display_custnum = $self->cust_main->display_custnum;
234 my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
236 : ';show=quotations';
238 emt("View this customer (#[_1])",$display_custnum) =>
239 "${p}view/cust_main.cgi?custnum=$custnum$target"
241 } elsif ( my $prospectnum = $self->prospectnum ) {
243 emt("View this prospect (#[_1])",$prospectnum) =>
244 "${p}view/prospect_main.html?$prospectnum"
252 #prevent things from falsely showing up as taxes, at least until we support
253 # quoting tax amounts..
258 shift->cust_bill_pkg;
262 my( $self, $total_items ) = @_;
264 if ( $self->total_setup > 0 ) {
265 push @$total_items, {
266 'total_item' => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
267 'total_amount' => $self->total_setup,
271 #could/should add up the different recurring frequencies on lines of their own
272 # but this will cover the 95% cases for now
273 if ( $self->total_recur > 0 ) {
274 push @$total_items, {
275 'total_item' => $self->mt('Total Recurring'),
276 'total_amount' => $self->total_recur,
282 =item enable_previous
286 sub enable_previous { 0 }
288 =item convert_cust_main
290 If this quotation already belongs to a customer, then returns that customer, as
291 an FS::cust_main object.
293 Otherwise, creates a new customer (FS::cust_main object and record, and
294 associated) based on this quotation's prospect, then orders this quotation's
295 packages as real packages for the customer.
297 If there is an error, returns an error message, otherwise, returns the
298 newly-created FS::cust_main object.
302 sub convert_cust_main {
305 my $cust_main = $self->cust_main;
306 return $cust_main if $cust_main; #already converted, don't again
308 my $oldAutoCommit = $FS::UID::AutoCommit;
309 local $FS::UID::AutoCommit = 0;
312 $cust_main = $self->prospect_main->convert_cust_main;
313 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
314 $dbh->rollback if $oldAutoCommit;
318 $self->prospectnum('');
319 $self->custnum( $cust_main->custnum );
320 my $error = $self->replace || $self->order;
322 $dbh->rollback if $oldAutoCommit;
326 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
334 This method is for use with quotations which are already associated with a customer.
336 Orders this quotation's packages as real packages for the customer.
338 If there is an error, returns an error message, otherwise returns false.
345 tie my %cust_pkg, 'Tie::RefHash',
346 map { FS::cust_pkg->new({ pkgpart => $_->pkgpart,
347 quantity => $_->quantity,
351 $self->quotation_pkg ;
353 $self->cust_main->order_pkgs( \%cust_pkg );
359 One-time charges, like FS::cust_main::charge()
363 #super false laziness w/cust_main::charge
366 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
367 my ( $pkg, $comment, $additional );
368 my ( $setuptax, $taxclass ); #internal taxes
369 my ( $taxproduct, $override ); #vendor (CCH) taxes
371 my $cust_pkg_ref = '';
372 my ( $bill_now, $invoice_terms ) = ( 0, '' );
374 if ( ref( $_[0] ) ) {
375 $amount = $_[0]->{amount};
376 $setup_cost = $_[0]->{setup_cost};
377 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
378 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
379 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
380 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
381 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
382 : '$'. sprintf("%.2f",$amount);
383 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
384 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
385 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
386 $additional = $_[0]->{additional} || [];
387 $taxproduct = $_[0]->{taxproductnum};
388 $override = { '' => $_[0]->{tax_override} };
389 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
390 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
391 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
392 $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
398 $pkg = @_ ? shift : 'One-time charge';
399 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
401 $taxclass = @_ ? shift : '';
405 local $SIG{HUP} = 'IGNORE';
406 local $SIG{INT} = 'IGNORE';
407 local $SIG{QUIT} = 'IGNORE';
408 local $SIG{TERM} = 'IGNORE';
409 local $SIG{TSTP} = 'IGNORE';
410 local $SIG{PIPE} = 'IGNORE';
412 my $oldAutoCommit = $FS::UID::AutoCommit;
413 local $FS::UID::AutoCommit = 0;
416 my $part_pkg = new FS::part_pkg ( {
418 'comment' => $comment,
422 'classnum' => ( $classnum ? $classnum : '' ),
423 'setuptax' => $setuptax,
424 'taxclass' => $taxclass,
425 'taxproductnum' => $taxproduct,
426 'setup_cost' => $setup_cost,
429 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
430 ( 0 .. @$additional - 1 )
432 'additional_count' => scalar(@$additional),
433 'setup_fee' => $amount,
436 my $error = $part_pkg->insert( options => \%options,
437 tax_overrides => $override,
440 $dbh->rollback if $oldAutoCommit;
444 my $pkgpart = $part_pkg->pkgpart;
447 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
449 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
450 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
451 $error = $type_pkgs->insert;
453 $dbh->rollback if $oldAutoCommit;
458 #except for DIFF, eveything above is idential to cust_main version
459 #but below is our own thing pretty much (adding a quotation package instead
460 # of ordering a customer package, no "bill now")
462 my $quotation_pkg = new FS::quotation_pkg ( {
463 'quotationnum' => $self->quotationnum,
464 'pkgpart' => $pkgpart,
465 'quantity' => $quantity,
466 #'start_date' => $start_date,
467 #'no_auto' => $no_auto,
468 'locationnum'=> $locationnum,
471 $error = $quotation_pkg->insert;
473 $dbh->rollback if $oldAutoCommit;
475 #} elsif ( $cust_pkg_ref ) {
476 # ${$cust_pkg_ref} = $cust_pkg;
479 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
486 Disables this quotation (sets disabled to Y, which hides the quotation on
487 prospects and customers).
489 If there is an error, returns an error message, otherwise returns false.
495 $self->disabled('Y');
501 Enables this quotation.
503 If there is an error, returns an error message, otherwise returns false.
520 =item search_sql_where HASHREF
522 Class method which returns an SQL WHERE fragment to search for parameters
523 specified in HASHREF. Valid parameters are
529 List reference of start date, end date, as UNIX timestamps.
539 List reference of charged limits (exclusive).
543 List reference of charged limits (exclusive).
547 flag, return open invoices only
551 flag, return net invoices only
559 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
563 sub search_sql_where {
564 my($class, $param) = @_;
566 # warn "$me search_sql_where called with params: \n".
567 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
573 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
574 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
578 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
579 # push @search, "cust_main.refnum = $1";
583 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
584 push @search, "quotation.prospectnum = $1";
588 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
589 push @search, "cust_bill.custnum = $1";
593 if ( $param->{_date} ) {
594 my($beginning, $ending) = @{$param->{_date}};
596 push @search, "quotation._date >= $beginning",
597 "quotation._date < $ending";
601 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
602 push @search, "quotation.quotationnum >= $1";
604 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
605 push @search, "quotation.quotationnum <= $1";
609 # if ( $param->{charged} ) {
610 # my @charged = ref($param->{charged})
611 # ? @{ $param->{charged} }
612 # : ($param->{charged});
614 # push @search, map { s/^charged/cust_bill.charged/; $_; }
618 my $owed_sql = FS::cust_bill->owed_sql;
621 push @search, "quotation._date < ". (time-86400*$param->{'days'})
624 #agent virtualization
625 my $curuser = $FS::CurrentUser::CurrentUser;
626 #false laziness w/search/quotation.html
627 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
628 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
631 join(' AND ', @search );
641 L<FS::Record>, schema.html from the base documentation.