2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record
8 use FS::UID qw( dbh myconnect );
9 use FS::Maketext qw( emt );
10 use FS::Record qw( qsearch qsearchs );
14 use FS::quotation_pkg;
15 use FS::quotation_pkg_tax;
24 FS::quotation - Object methods for quotation records
30 $record = new FS::quotation \%hash;
31 $record = new FS::quotation { 'column' => 'value' };
33 $error = $record->insert;
35 $error = $new_record->replace($old_record);
37 $error = $record->delete;
39 $error = $record->check;
43 An FS::quotation object represents a quotation. FS::quotation inherits from
44 FS::Record. The following fields are currently supported:
74 projected date when the quotation will be closed
78 projected confidence (expressed as integer) that quotation will close
88 Creates a new quotation. To add the quotation to the database, see L<"insert">.
90 Note that this stores the hash reference, not a distinct copy of the hash it
91 points to. You can ask the object for a copy with the I<hash> method.
95 sub table { 'quotation'; }
96 sub notice_name { 'Quotation'; }
97 sub template_conf { 'quotation_'; }
98 sub has_sections { 1; }
102 Adds this record to the database. If there is an error, returns the error,
103 otherwise returns false.
107 Delete this record from the database.
109 =item replace OLD_RECORD
111 Replaces the OLD_RECORD with this one in the database. If there is an error,
112 returns the error, otherwise returns false.
116 Checks all fields to make sure this is a valid quotation. If there is
117 an error, returns the error, otherwise returns false. Called by the insert
126 $self->ut_numbern('quotationnum')
127 || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
128 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
129 || $self->ut_numbern('_date')
130 || $self->ut_enum('disabled', [ '', 'Y' ])
131 || $self->ut_numbern('usernum')
132 || $self->ut_numbern('close_date')
133 || $self->ut_numbern('confidence')
135 return $error if $error;
137 $self->_date(time) unless $self->_date;
139 $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
141 return 'confidence must be an integer between 1 and 100'
142 if length($self->confidence) && (($self->confidence < 1) || ($self->confidence > 100));
144 return 'prospectnum or custnum must be specified'
145 if ! $self->prospectnum
159 sub cust_bill_pkg { #actually quotation_pkg objects
160 shift->quotation_pkg(@_);
169 sprintf('%.2f', $self->_total('setup') + $self->_total('setup_tax'));
172 =item total_recur [ FREQ ]
178 #=item total_recur [ FREQ ]
179 #my $freq = @_ ? shift : '';
180 sprintf('%.2f', $self->_total('recur') + $self->_total('recur_tax'));
184 my( $self, $method ) = @_;
187 $total += $_->$method() for $self->quotation_pkg;
188 sprintf('%.2f', $total);
194 my $opt = shift || {};
195 if ($opt and !ref($opt)) {
196 die ref($self). '->email called with positional parameters';
199 my $conf = $self->conf;
201 my $from = delete $opt->{from};
203 # this is where we set the From: address
204 $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
205 || $conf->invoice_from_full( $self->cust_or_prospect->agentnum );
206 $self->SUPER::email( {
217 $self->conf->config('quotation_subject') #, $self->cust_main->agentnum)
220 #my $cust_main = $self->cust_main;
221 #my $name = $cust_main->name;
222 #my $name_short = $cust_main->name_short;
223 #my $invoice_number = $self->invnum;
224 #my $invoice_date = $self->_date_pretty;
229 =item cust_or_prosect
233 sub cust_or_prospect {
235 $self->custnum ? $self->cust_main : $self->prospect_main;
238 =item cust_or_prospect_label_link
240 HTML links to either the customer or prospect.
242 Returns a list consisting of two elements. The first is a text label for the
243 link, and the second is the URL.
247 sub cust_or_prospect_label_link {
248 my( $self, $p ) = @_;
250 if ( my $custnum = $self->custnum ) {
251 my $display_custnum = $self->cust_main->display_custnum;
252 my $target = $FS::CurrentUser::CurrentUser->default_customer_view eq 'jumbo'
254 : ';show=quotations';
256 emt("View this customer (#[_1])",$display_custnum) =>
257 "${p}view/cust_main.cgi?custnum=$custnum$target"
259 } elsif ( my $prospectnum = $self->prospectnum ) {
261 emt("View this prospect (#[_1])",$prospectnum) =>
262 "${p}view/prospect_main.html?$prospectnum"
270 sub _items_sections {
273 my $escape = $opt{escape}; # the only one we care about
275 my %show; # package frequency => 1 if there's anything to display
276 my %subtotals = (); # package frequency => subtotal
277 my $disable_total = 0;
278 foreach my $pkg ($self->quotation_pkg) {
280 my $part_pkg = $pkg->part_pkg;
282 my $recur_freq = $part_pkg->freq;
283 $show{$recur_freq} = 1 if $pkg->unitrecur > 0;
284 $show{0} = 1 if $pkg->unitsetup > 0;
285 ($subtotals{0} ||= 0) += $pkg->setup + $pkg->setup_tax;
286 ($subtotals{$recur_freq} ||= 0) += $pkg->recur + $pkg->recur_tax;
288 #this is a shitty hack based on what's in part_pkg/ at the moment
289 # but its good enough for the 99% common case of preventing totals from
290 # displaying for prorate packages
292 if $part_pkg->plan =~ /^(prorate|torrus|agent$)/
293 || $part_pkg->option('recur_method') eq 'prorate'
294 || ( $part_pkg->option('sync_bill_date')
296 && $self->cust_main->billing_pkgs #num_billing_pkgs when we have it
300 my @pkg_freq_order = keys %{ FS::Misc->pkg_freqs };
303 my $no_recurring = 0;
304 foreach my $freq (keys %subtotals) {
306 #next if $subtotals{$freq} == 0;
307 next if !$show{$freq};
310 List::MoreUtils::first_index { $_ eq $freq } @pkg_freq_order;
312 if ( $freq eq '0' ) {
313 if ( scalar(keys(%subtotals)) == 1 ) {
314 # there are no recurring packages
316 $desc = $self->mt('Charges');
318 $desc = $self->mt('Setup Charges');
321 $desc = $self->mt('Recurring Charges') . ' - ' .
322 ucfirst($self->mt(FS::Misc->pkg_freqs->{$freq}))
326 'description' => &$escape($desc),
327 'sort_weight' => $weight,
329 'subtotal' => sprintf('%.2f',$subtotals{$freq}),
333 unless ( $disable_total || $no_recurring ) {
335 $total += $_ for values %subtotals;
337 'description' => 'First payment',
339 'category' => 'Total category', #required but what's it used for?
340 'subtotal' => sprintf('%.2f',$total)
344 return \@sections, [];
347 =item enable_previous
351 sub enable_previous { 0 }
353 =item convert_cust_main
355 If this quotation already belongs to a customer, then returns that customer, as
356 an FS::cust_main object.
358 Otherwise, creates a new customer (FS::cust_main object and record, and
359 associated) based on this quotation's prospect, then orders this quotation's
360 packages as real packages for the customer.
362 If there is an error, returns an error message, otherwise, returns the
363 newly-created FS::cust_main object.
367 sub convert_cust_main {
370 my $cust_main = $self->cust_main;
371 return $cust_main if $cust_main; #already converted, don't again
373 my $oldAutoCommit = $FS::UID::AutoCommit;
374 local $FS::UID::AutoCommit = 0;
377 $cust_main = $self->prospect_main->convert_cust_main;
378 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
379 $dbh->rollback if $oldAutoCommit;
383 $self->prospectnum('');
384 $self->custnum( $cust_main->custnum );
385 my $error = $self->replace || $self->order;
387 $dbh->rollback if $oldAutoCommit;
391 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
397 =item order [ HASHREF ]
399 This method is for use with quotations which are already associated with a customer.
401 Orders this quotation's packages as real packages for the customer.
403 If there is an error, returns an error message, otherwise returns false.
405 If HASHREF is passed, it will be filled with a hash mapping the
406 C<quotationpkgnum> of each quoted package to the C<pkgnum> of the package
413 my $pkgnum_map = shift || {};
414 my $details_map = {};
416 tie my %all_cust_pkg, 'Tie::RefHash';
417 foreach my $quotation_pkg ($self->quotation_pkg) {
418 my $cust_pkg = FS::cust_pkg->new;
419 $pkgnum_map->{ $quotation_pkg->quotationpkgnum } = $cust_pkg;
421 # details will be copied below, after package is ordered
422 $details_map->{ $quotation_pkg->quotationpkgnum } = [
423 map { $_->copy_on_order ? $_->detail : () } $quotation_pkg->quotation_pkg_detail
426 foreach (qw(pkgpart locationnum start_date contract_end quantity waive_setup)) {
427 $cust_pkg->set( $_, $quotation_pkg->get($_) );
430 # can now have two discounts each (setup and recur)
431 foreach my $pkg_discount ($quotation_pkg->quotation_pkg_discount) {
432 my $field = $pkg_discount->setuprecur . '_discountnum';
433 $cust_pkg->set($field, $pkg_discount->discountnum);
436 $all_cust_pkg{$cust_pkg} = []; # no services
439 local $SIG{HUP} = 'IGNORE';
440 local $SIG{INT} = 'IGNORE';
441 local $SIG{QUIT} = 'IGNORE';
442 local $SIG{TERM} = 'IGNORE';
443 local $SIG{TSTP} = 'IGNORE';
444 local $SIG{PIPE} = 'IGNORE';
446 my $oldAutoCommit = $FS::UID::AutoCommit;
447 local $FS::UID::AutoCommit = 0;
450 my $error = $self->cust_main->order_pkgs( \%all_cust_pkg );
453 # copy details (copy_on_order filtering handled above)
454 foreach my $quotationpkgnum (keys %$details_map) {
455 next unless @{$details_map->{$quotationpkgnum}};
456 $error = $pkgnum_map->{$quotationpkgnum}->set_cust_pkg_detail(
458 @{$details_map->{$quotationpkgnum}}
464 foreach my $quotationpkgnum (keys %$pkgnum_map) {
465 # convert the objects to just pkgnums
466 my $cust_pkg = $pkgnum_map->{$quotationpkgnum};
467 $pkgnum_map->{$quotationpkgnum} = $cust_pkg->pkgnum;
471 $dbh->rollback if $oldAutoCommit;
475 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
482 One-time charges, like FS::cust_main::charge()
486 #super false laziness w/cust_main::charge
489 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
490 my ( $pkg, $comment, $additional );
491 my ( $setuptax, $taxclass ); #internal taxes
492 my ( $taxproduct, $override ); #vendor (CCH) taxes
494 my $cust_pkg_ref = '';
495 my ( $bill_now, $invoice_terms ) = ( 0, '' );
497 if ( ref( $_[0] ) ) {
498 $amount = $_[0]->{amount};
499 $setup_cost = $_[0]->{setup_cost};
500 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
501 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
502 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
503 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
504 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
505 : '$'. sprintf("%.2f",$amount);
506 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
507 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
508 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
509 $additional = $_[0]->{additional} || [];
510 $taxproduct = $_[0]->{taxproductnum};
511 $override = { '' => $_[0]->{tax_override} };
512 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
513 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
514 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
515 $locationnum = $_[0]->{locationnum};
521 $pkg = @_ ? shift : 'One-time charge';
522 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
524 $taxclass = @_ ? shift : '';
528 local $SIG{HUP} = 'IGNORE';
529 local $SIG{INT} = 'IGNORE';
530 local $SIG{QUIT} = 'IGNORE';
531 local $SIG{TERM} = 'IGNORE';
532 local $SIG{TSTP} = 'IGNORE';
533 local $SIG{PIPE} = 'IGNORE';
535 my $oldAutoCommit = $FS::UID::AutoCommit;
536 local $FS::UID::AutoCommit = 0;
539 my $part_pkg = new FS::part_pkg ( {
541 'comment' => $comment,
545 'classnum' => ( $classnum ? $classnum : '' ),
546 'setuptax' => $setuptax,
547 'taxclass' => $taxclass,
548 'taxproductnum' => $taxproduct,
549 'setup_cost' => $setup_cost,
552 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
553 ( 0 .. @$additional - 1 )
555 'additional_count' => scalar(@$additional),
556 'setup_fee' => $amount,
559 my $error = $part_pkg->insert( options => \%options,
560 tax_overrides => $override,
563 $dbh->rollback if $oldAutoCommit;
567 my $pkgpart = $part_pkg->pkgpart;
570 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
572 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
573 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
574 $error = $type_pkgs->insert;
576 $dbh->rollback if $oldAutoCommit;
581 #except for DIFF, eveything above is idential to cust_main version
582 #but below is our own thing pretty much (adding a quotation package instead
583 # of ordering a customer package, no "bill now")
585 my $quotation_pkg = new FS::quotation_pkg ( {
586 'quotationnum' => $self->quotationnum,
587 'pkgpart' => $pkgpart,
588 'quantity' => $quantity,
589 #'start_date' => $start_date,
590 #'no_auto' => $no_auto,
591 'locationnum'=> $locationnum,
594 $error = $quotation_pkg->insert;
596 $dbh->rollback if $oldAutoCommit;
598 #} elsif ( $cust_pkg_ref ) {
599 # ${$cust_pkg_ref} = $cust_pkg;
602 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
609 Disables this quotation (sets disabled to Y, which hides the quotation on
610 prospects and customers).
612 If there is an error, returns an error message, otherwise returns false.
618 $self->disabled('Y');
624 Enables this quotation.
626 If there is an error, returns an error message, otherwise returns false.
638 Calculates current prices for all items on this quotation, including
639 discounts and taxes, and updates the quotation_pkg records accordingly.
645 my $conf = FS::Conf->new;
647 my %pkgnum_of; # quotationpkgnum => temporary pkgnum
649 my $me = "[quotation #".$self->quotationnum."]"; # for debug messages
651 my @return_bill = ([]);
654 ###### BEGIN TRANSACTION ######
658 my $temp_dbh = myconnect();
659 local $FS::UID::dbh = $temp_dbh;
660 local $FS::UID::AutoCommit = 0;
662 my $fake_self = FS::quotation->new({ $self->hash });
664 # if this is a prospect, make them into a customer for now
665 # XXX prospects currently can't have service locations
666 my $cust_or_prospect = $self->cust_or_prospect;
668 if ( $cust_or_prospect->isa('FS::prospect_main') ) {
669 $cust_main = $cust_or_prospect->convert_cust_main;
670 die "$cust_main (simulating customer signup)\n" unless ref $cust_main;
671 $fake_self->set('prospectnum', '');
672 $fake_self->set('custnum', $cust_main->custnum);
674 $cust_main = $cust_or_prospect;
678 $error = $fake_self->order(\%pkgnum_of);
679 die "$error (simulating package order)\n" if $error;
681 my @new_pkgs = map { FS::cust_pkg->by_key($_) } values(%pkgnum_of);
683 # simulate the first bill
686 'pkg_list' => \@new_pkgs,
687 'time' => time, # an option to adjust this?
688 'return_bill' => $return_bill[0],
689 'no_usage_reset' => 1,
691 $error = $cust_main->bill(%bill_opt);
692 die "$error (simulating initial billing)\n" if $error;
694 # pick dates for future bills
696 foreach (@new_pkgs) {
697 my $bill = $_->get('bill');
699 push @{ $next_bill_pkgs{$bill} ||= [] }, $_;
703 foreach my $next_bill (keys %next_bill_pkgs) {
704 $bill_opt{'time'} = $next_bill;
705 $bill_opt{'return_bill'} = $return_bill[$i] = [];
706 $bill_opt{'pkg_list'} = $next_bill_pkgs{$next_bill};
707 $error = $cust_main->bill(%bill_opt);
708 die "$error (simulating recurring billing cycle $i)\n" if $error;
715 ###### END TRANSACTION ######
716 my %quotationpkgnum_of = reverse %pkgnum_of;
719 warn "pkgnums:\n".Dumper(\%pkgnum_of);
720 warn Dumper(\@return_bill);
723 # Careful: none of the foreign keys in here are correct outside the sandbox.
724 # We have a translation table for pkgnums; all others are total lies.
726 my %quotation_pkg; # quotationpkgnum => quotation_pkg
727 foreach my $qp ($self->quotation_pkg) {
728 $quotation_pkg{$qp->quotationpkgnum} = $qp;
729 $qp->set($_, 0) foreach qw(unitsetup unitrecur);
730 $qp->set('freq', '');
731 # flush old tax records
732 foreach ($qp->quotation_pkg_tax) {
734 return "$error (flushing tax records for pkgpart ".$qp->part_pkg->pkgpart.")"
739 my %quotation_pkg_tax; # quotationpkgnum => tax name => quotation_pkg_tax obj
740 my %quotation_pkg_discount; # quotationpkgnum => setuprecur => quotation_pkg_discount obj
742 for (my $i = 0; $i < scalar(@return_bill); $i++) {
743 my $this_bill = $return_bill[$i]->[0];
745 warn "$me billing cycle $i produced no invoice\n";
751 foreach my $cust_bill_pkg (@{ $this_bill->get('cust_bill_pkg') }) {
752 my $pkgnum = $cust_bill_pkg->pkgnum;
753 $cust_bill_pkg{ $cust_bill_pkg->billpkgnum } = $cust_bill_pkg;
755 # taxes/fees; come back to it
756 push @nonpkg_lines, $cust_bill_pkg;
759 my $quotationpkgnum = $quotationpkgnum_of{$pkgnum};
760 my $qp = $quotation_pkg{$quotationpkgnum};
762 # XXX supplemental packages could do this (they have separate pkgnums)
763 # handle that special case at some point
764 warn "$me simulated bill returned a package not on the quotation (pkgpart ".$cust_bill_pkg->pkgpart.")\n";
768 # then this is the first (setup) invoice
769 $qp->set('start_date', $cust_bill_pkg->sdate);
770 $qp->set('unitsetup', $qp->unitsetup + $cust_bill_pkg->unitsetup);
771 # pkgpart_override is a possibility
773 # recurring invoice (should be only one of these per package, though
774 # it may have multiple lineitems with the same pkgnum)
775 $qp->set('unitrecur', $qp->unitrecur + $cust_bill_pkg->unitrecur);
779 if ( $cust_bill_pkg->get('discounts') ) {
780 # discount records are generated as (setup, recur).
781 # well, not always, sometimes it's just (recur), but fixing this
782 # is horribly invasive.
783 my $discount = $cust_bill_pkg->get('discounts')->[0];
786 # find the quotation_pkg_discount record for this billing pass...
787 my $setuprecur = $i ? 'recur' : 'setup';
788 my $qpd = $quotation_pkg_discount{$quotationpkgnum}{$setuprecur}
789 ||= qsearchs('quotation_pkg_discount', {
790 'quotationpkgnum' => $quotationpkgnum,
791 'setuprecur' => $setuprecur,
794 if (!$qpd) { #can't happen
795 warn "$me simulated bill returned a $setuprecur discount but no discount is in effect.\n";
798 $qpd->set('amount', $discount->amount);
801 } # end of discount stuff
806 foreach my $cust_bill_pkg (@nonpkg_lines) {
808 my $itemdesc = $cust_bill_pkg->itemdesc;
810 if ($cust_bill_pkg->feepart) {
811 warn "$me simulated bill included a non-package fee (feepart ".
812 $cust_bill_pkg->feepart.")\n";
815 my $links = $cust_bill_pkg->get('cust_bill_pkg_tax_location') ||
816 $cust_bill_pkg->get('cust_bill_pkg_tax_rate_location') ||
818 # breadth-first unrolled recursion:
819 # take each tax link and any tax-on-tax descendants, and merge them
820 # into a single quotation_pkg_tax record for each pkgnum/taxname
822 while (my $tax_link = shift @$links) {
823 my $target = $cust_bill_pkg{ $tax_link->taxable_billpkgnum }
824 or die "$me unable to resolve tax link\n";
825 if ($target->pkgnum) {
826 my $quotationpkgnum = $quotationpkgnum_of{$target->pkgnum};
827 # create this if there isn't one yet
828 my $qpt = $quotation_pkg_tax{$quotationpkgnum}{$itemdesc} ||=
829 FS::quotation_pkg_tax->new({
830 quotationpkgnum => $quotationpkgnum,
831 itemdesc => $itemdesc,
835 if ( $i == 0 ) { # first invoice
836 $qpt->set('setup_amount', $qpt->setup_amount + $tax_link->amount);
837 } else { # subsequent invoices
838 # this isn't perfectly accurate, but that's why it's an estimate
839 $qpt->set('recur_amount', $qpt->recur_amount + $tax_link->amount);
840 $qpt->set('setup_amount', sprintf('%.2f', $qpt->setup_amount - $tax_link->amount));
841 $qpt->set('setup_amount', 0) if $qpt->setup_amount < 0;
843 } elsif ($target->feepart) {
844 # do nothing; we already warned for the fee itself
846 # tax on tax: the tax target is another tax item.
847 # since this is an estimate, I'm just going to assign it to the
848 # first of the underlying packages. (RT#5243 is why we can't have
850 my $sublinks = $target->cust_bill_pkg_tax_rate_location;
851 if ($sublinks and $sublinks->[0]) {
852 $tax_link->set('taxable_billpkgnum', $sublinks->[0]->taxable_billpkgnum);
853 push @$links, $tax_link; #try again
855 warn "$me unable to assign tax on tax; ignoring\n";
858 } # while my $tax_link
860 } # foreach my $cust_bill_pkg
862 foreach my $quotation_pkg (values %quotation_pkg) {
863 $error = $quotation_pkg->replace;
864 return "$error (recording estimate for ".$quotation_pkg->part_pkg->pkg.")"
867 foreach (values %quotation_pkg_discount) {
868 # { setup => one, recur => another }
869 foreach my $quotation_pkg_discount (values %$_) {
870 $error = $quotation_pkg_discount->replace;
871 return "$error (recording estimated discount)"
875 foreach my $quotation_pkg_tax (map { values %$_ } values %quotation_pkg_tax) {
876 $error = $quotation_pkg_tax->insert;
877 return "$error (recording estimated tax for ".$quotation_pkg_tax->itemdesc.")"
890 =item search_sql_where HASHREF
892 Class method which returns an SQL WHERE fragment to search for parameters
893 specified in HASHREF. Valid parameters are
899 List reference of start date, end date, as UNIX timestamps.
909 List reference of charged limits (exclusive).
913 List reference of charged limits (exclusive).
917 flag, return open invoices only
921 flag, return net invoices only
929 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
933 sub search_sql_where {
934 my($class, $param) = @_;
936 # warn "$me search_sql_where called with params: \n".
937 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
943 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
944 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
948 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
949 # push @search, "cust_main.refnum = $1";
953 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
954 push @search, "quotation.prospectnum = $1";
958 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
959 push @search, "cust_bill.custnum = $1";
963 if ( $param->{_date} ) {
964 my($beginning, $ending) = @{$param->{_date}};
966 push @search, "quotation._date >= $beginning",
967 "quotation._date < $ending";
971 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
972 push @search, "quotation.quotationnum >= $1";
974 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
975 push @search, "quotation.quotationnum <= $1";
979 # if ( $param->{charged} ) {
980 # my @charged = ref($param->{charged})
981 # ? @{ $param->{charged} }
982 # : ($param->{charged});
984 # push @search, map { s/^charged/cust_bill.charged/; $_; }
988 my $owed_sql = FS::cust_bill->owed_sql;
991 push @search, "quotation._date < ". (time-86400*$param->{'days'})
994 #agent virtualization
995 my $curuser = $FS::CurrentUser::CurrentUser;
996 #false laziness w/search/quotation.html
997 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
998 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
1001 join(' AND ', @search );
1007 Return line item hashes for each package on this quotation.
1012 my ($self, %options) = @_;
1013 my $escape = $options{'escape_function'};
1014 my $locale = $self->cust_or_prospect->locale;
1016 my $preref = $options{'preref_callback'};
1018 my $section = $options{'section'};
1019 my $freq = $section->{'category'};
1020 my @pkgs = $self->quotation_pkg;
1022 die "_items_pkg called without section->{'category'}"
1023 unless defined $freq;
1025 my %tax_item; # taxname => hashref, will be aggregated AT DISPLAY TIME
1026 # like we should have done in the first place
1028 foreach my $quotation_pkg (@pkgs) {
1029 my $part_pkg = $quotation_pkg->part_pkg;
1030 my @details = $quotation_pkg->details;
1033 'pkgnum' => $quotation_pkg->quotationpkgnum,
1034 'description' => $quotation_pkg->desc($locale),
1035 'ext_description' => \@details,
1036 'quantity' => $quotation_pkg->quantity,
1040 $setuprecur = 'setup';
1041 if ($part_pkg->freq ne '0') {
1042 # indicate that it's a setup fee on a recur package (cust_bill does
1044 $this_item->{'description'} .= ' Setup';
1047 # recur for this frequency
1048 next if $freq ne $part_pkg->freq;
1049 $setuprecur = 'recur';
1052 $this_item->{'unit_amount'} = sprintf('%.2f',
1053 $quotation_pkg->get('unit'.$setuprecur));
1054 $this_item->{'amount'} = sprintf('%.2f', $this_item->{'unit_amount'}
1055 * $quotation_pkg->quantity);
1056 next if $this_item->{'amount'} == 0;
1059 $this_item->{'preref_html'} = &$preref($quotation_pkg);
1062 push @items, $this_item;
1063 my $discount = $quotation_pkg->_item_discount(setuprecur => $setuprecur);
1065 $_ = &{$escape}($_) foreach @{ $discount->{ext_description} };
1066 push @items, $discount;
1069 # each quotation_pkg_tax has two amounts: the amount charged on the
1070 # setup invoice, and the amount on the recurring invoice.
1071 foreach my $qpt ($quotation_pkg->quotation_pkg_tax) {
1072 my $this_tax = $tax_item{$qpt->itemdesc} ||= {
1074 'description' => $qpt->itemdesc,
1075 'ext_description' => [],
1078 $this_tax->{'amount'} += $qpt->get($setuprecur.'_amount');
1080 } # foreach $quotation_pkg
1082 foreach my $taxname ( sort { $a cmp $b } keys (%tax_item) ) {
1083 my $this_tax = $tax_item{$taxname};
1084 $this_tax->{'amount'} = sprintf('%.2f', $this_tax->{'amount'});
1085 next if $this_tax->{'amount'} == 0;
1086 push @items, $this_tax;
1102 L<FS::Record>, schema.html from the base documentation.