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 'prospectnum or custnum must be specified'
142 if ! $self->prospectnum
156 sub cust_bill_pkg { #actually quotation_pkg objects
157 shift->quotation_pkg(@_);
166 sprintf('%.2f', $self->_total('setup') + $self->_total('setup_tax'));
169 =item total_recur [ FREQ ]
175 #=item total_recur [ FREQ ]
176 #my $freq = @_ ? shift : '';
177 sprintf('%.2f', $self->_total('recur') + $self->_total('recur_tax'));
181 my( $self, $method ) = @_;
184 $total += $_->$method() for $self->quotation_pkg;
185 sprintf('%.2f', $total);
191 my $opt = shift || {};
192 if ($opt and !ref($opt)) {
193 die ref($self). '->email called with positional parameters';
196 my $conf = $self->conf;
198 my $from = delete $opt->{from};
200 # this is where we set the From: address
201 $from ||= $conf->config('quotation_from', $self->cust_or_prospect->agentnum )
202 || $conf->invoice_from_full( $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
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 sub _items_sections {
270 my $escape = $opt{escape}; # the only one we care about
272 my %show; # package frequency => 1 if there's anything to display
273 my %subtotals = (); # package frequency => subtotal
274 my $disable_total = 0;
275 foreach my $pkg ($self->quotation_pkg) {
277 my $part_pkg = $pkg->part_pkg;
279 my $recur_freq = $part_pkg->freq;
280 $show{$recur_freq} = 1 if $pkg->unitrecur > 0;
281 $show{0} = 1 if $pkg->unitsetup > 0;
282 ($subtotals{0} ||= 0) += $pkg->setup + $pkg->setup_tax;
283 ($subtotals{$recur_freq} ||= 0) += $pkg->recur + $pkg->recur_tax;
285 #this is a shitty hack based on what's in part_pkg/ at the moment
286 # but its good enough for the 99% common case of preventing totals from
287 # displaying for prorate packages
289 if $part_pkg->plan =~ /^(prorate|torrus|agent$)/
290 || $part_pkg->option('recur_method') eq 'prorate'
291 || ( $part_pkg->option('sync_bill_date')
293 && $self->cust_main->billing_pkgs #num_billing_pkgs when we have it
297 my @pkg_freq_order = keys %{ FS::Misc->pkg_freqs };
300 my $no_recurring = 0;
301 foreach my $freq (keys %subtotals) {
303 #next if $subtotals{$freq} == 0;
304 next if !$show{$freq};
307 List::MoreUtils::first_index { $_ eq $freq } @pkg_freq_order;
309 if ( $freq eq '0' ) {
310 if ( scalar(keys(%subtotals)) == 1 ) {
311 # there are no recurring packages
313 $desc = $self->mt('Charges');
315 $desc = $self->mt('Setup Charges');
318 $desc = $self->mt('Recurring Charges') . ' - ' .
319 ucfirst($self->mt(FS::Misc->pkg_freqs->{$freq}))
323 'description' => &$escape($desc),
324 'sort_weight' => $weight,
326 'subtotal' => sprintf('%.2f',$subtotals{$freq}),
330 unless ( $disable_total || $no_recurring ) {
332 $total += $_ for values %subtotals;
334 'description' => 'First payment',
336 'category' => 'Total category', #required but what's it used for?
337 'subtotal' => sprintf('%.2f',$total)
341 return \@sections, [];
344 =item enable_previous
348 sub enable_previous { 0 }
350 =item convert_cust_main
352 If this quotation already belongs to a customer, then returns that customer, as
353 an FS::cust_main object.
355 Otherwise, creates a new customer (FS::cust_main object and record, and
356 associated) based on this quotation's prospect, then orders this quotation's
357 packages as real packages for the customer.
359 If there is an error, returns an error message, otherwise, returns the
360 newly-created FS::cust_main object.
364 sub convert_cust_main {
367 my $cust_main = $self->cust_main;
368 return $cust_main if $cust_main; #already converted, don't again
370 my $oldAutoCommit = $FS::UID::AutoCommit;
371 local $FS::UID::AutoCommit = 0;
374 $cust_main = $self->prospect_main->convert_cust_main;
375 unless ( ref($cust_main) ) { # eq 'FS::cust_main' ) {
376 $dbh->rollback if $oldAutoCommit;
380 $self->prospectnum('');
381 $self->custnum( $cust_main->custnum );
382 my $error = $self->replace || $self->order;
384 $dbh->rollback if $oldAutoCommit;
388 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
394 =item order [ HASHREF ]
396 This method is for use with quotations which are already associated with a customer.
398 Orders this quotation's packages as real packages for the customer.
400 If there is an error, returns an error message, otherwise returns false.
402 If HASHREF is passed, it will be filled with a hash mapping the
403 C<quotationpkgnum> of each quoted package to the C<pkgnum> of the package
410 my $pkgnum_map = shift || {};
411 my $details_map = {};
413 tie my %all_cust_pkg, 'Tie::RefHash';
414 foreach my $quotation_pkg ($self->quotation_pkg) {
415 my $cust_pkg = FS::cust_pkg->new;
416 $pkgnum_map->{ $quotation_pkg->quotationpkgnum } = $cust_pkg;
418 # details will be copied below, after package is ordered
419 $details_map->{ $quotation_pkg->quotationpkgnum } = [
420 map { $_->copy_on_order ? $_->detail : () } $quotation_pkg->quotation_pkg_detail
423 foreach (qw(pkgpart locationnum start_date contract_end quantity waive_setup)) {
424 $cust_pkg->set( $_, $quotation_pkg->get($_) );
427 # can now have two discounts each (setup and recur)
428 foreach my $pkg_discount ($quotation_pkg->quotation_pkg_discount) {
429 my $field = $pkg_discount->setuprecur . '_discountnum';
430 $cust_pkg->set($field, $pkg_discount->discountnum);
433 $all_cust_pkg{$cust_pkg} = []; # no services
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 $error = $self->cust_main->order_pkgs( \%all_cust_pkg );
450 # copy details (copy_on_order filtering handled above)
451 foreach my $quotationpkgnum (keys %$details_map) {
452 next unless @{$details_map->{$quotationpkgnum}};
453 $error = $pkgnum_map->{$quotationpkgnum}->set_cust_pkg_detail(
455 @{$details_map->{$quotationpkgnum}}
461 foreach my $quotationpkgnum (keys %$pkgnum_map) {
462 # convert the objects to just pkgnums
463 my $cust_pkg = $pkgnum_map->{$quotationpkgnum};
464 $pkgnum_map->{$quotationpkgnum} = $cust_pkg->pkgnum;
468 $dbh->rollback if $oldAutoCommit;
472 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
479 One-time charges, like FS::cust_main::charge()
483 #super false laziness w/cust_main::charge
486 my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
487 my ( $pkg, $comment, $additional );
488 my ( $setuptax, $taxclass ); #internal taxes
489 my ( $taxproduct, $override ); #vendor (CCH) taxes
491 my $cust_pkg_ref = '';
492 my ( $bill_now, $invoice_terms ) = ( 0, '' );
494 if ( ref( $_[0] ) ) {
495 $amount = $_[0]->{amount};
496 $setup_cost = $_[0]->{setup_cost};
497 $quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
498 $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
499 $no_auto = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
500 $pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
501 $comment = exists($_[0]->{comment}) ? $_[0]->{comment}
502 : '$'. sprintf("%.2f",$amount);
503 $setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
504 $taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
505 $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
506 $additional = $_[0]->{additional} || [];
507 $taxproduct = $_[0]->{taxproductnum};
508 $override = { '' => $_[0]->{tax_override} };
509 $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
510 $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
511 $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
512 $locationnum = $_[0]->{locationnum};
518 $pkg = @_ ? shift : 'One-time charge';
519 $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
521 $taxclass = @_ ? shift : '';
525 local $SIG{HUP} = 'IGNORE';
526 local $SIG{INT} = 'IGNORE';
527 local $SIG{QUIT} = 'IGNORE';
528 local $SIG{TERM} = 'IGNORE';
529 local $SIG{TSTP} = 'IGNORE';
530 local $SIG{PIPE} = 'IGNORE';
532 my $oldAutoCommit = $FS::UID::AutoCommit;
533 local $FS::UID::AutoCommit = 0;
536 my $part_pkg = new FS::part_pkg ( {
538 'comment' => $comment,
542 'classnum' => ( $classnum ? $classnum : '' ),
543 'setuptax' => $setuptax,
544 'taxclass' => $taxclass,
545 'taxproductnum' => $taxproduct,
546 'setup_cost' => $setup_cost,
549 my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
550 ( 0 .. @$additional - 1 )
552 'additional_count' => scalar(@$additional),
553 'setup_fee' => $amount,
556 my $error = $part_pkg->insert( options => \%options,
557 tax_overrides => $override,
560 $dbh->rollback if $oldAutoCommit;
564 my $pkgpart = $part_pkg->pkgpart;
567 my %type_pkgs = ( 'typenum' => $self->cust_or_prospect->agent->typenum, 'pkgpart' => $pkgpart );
569 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
570 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
571 $error = $type_pkgs->insert;
573 $dbh->rollback if $oldAutoCommit;
578 #except for DIFF, eveything above is idential to cust_main version
579 #but below is our own thing pretty much (adding a quotation package instead
580 # of ordering a customer package, no "bill now")
582 my $quotation_pkg = new FS::quotation_pkg ( {
583 'quotationnum' => $self->quotationnum,
584 'pkgpart' => $pkgpart,
585 'quantity' => $quantity,
586 #'start_date' => $start_date,
587 #'no_auto' => $no_auto,
588 'locationnum'=> $locationnum,
591 $error = $quotation_pkg->insert;
593 $dbh->rollback if $oldAutoCommit;
595 #} elsif ( $cust_pkg_ref ) {
596 # ${$cust_pkg_ref} = $cust_pkg;
599 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
606 Disables this quotation (sets disabled to Y, which hides the quotation on
607 prospects and customers).
609 If there is an error, returns an error message, otherwise returns false.
615 $self->disabled('Y');
621 Enables this quotation.
623 If there is an error, returns an error message, otherwise returns false.
635 Calculates current prices for all items on this quotation, including
636 discounts and taxes, and updates the quotation_pkg records accordingly.
642 my $conf = FS::Conf->new;
644 my %pkgnum_of; # quotationpkgnum => temporary pkgnum
646 my $me = "[quotation #".$self->quotationnum."]"; # for debug messages
648 my @return_bill = ([]);
651 ###### BEGIN TRANSACTION ######
655 my $temp_dbh = myconnect();
656 local $FS::UID::dbh = $temp_dbh;
657 local $FS::UID::AutoCommit = 0;
659 my $fake_self = FS::quotation->new({ $self->hash });
661 # if this is a prospect, make them into a customer for now
662 # XXX prospects currently can't have service locations
663 my $cust_or_prospect = $self->cust_or_prospect;
665 if ( $cust_or_prospect->isa('FS::prospect_main') ) {
666 $cust_main = $cust_or_prospect->convert_cust_main;
667 die "$cust_main (simulating customer signup)\n" unless ref $cust_main;
668 $fake_self->set('prospectnum', '');
669 $fake_self->set('custnum', $cust_main->custnum);
671 $cust_main = $cust_or_prospect;
675 $error = $fake_self->order(\%pkgnum_of);
676 die "$error (simulating package order)\n" if $error;
678 my @new_pkgs = map { FS::cust_pkg->by_key($_) } values(%pkgnum_of);
680 # simulate the first bill
683 'pkg_list' => \@new_pkgs,
684 'time' => time, # an option to adjust this?
685 'return_bill' => $return_bill[0],
686 'no_usage_reset' => 1,
688 $error = $cust_main->bill(%bill_opt);
689 die "$error (simulating initial billing)\n" if $error;
691 # pick dates for future bills
693 foreach (@new_pkgs) {
694 my $bill = $_->get('bill');
696 push @{ $next_bill_pkgs{$bill} ||= [] }, $_;
700 foreach my $next_bill (keys %next_bill_pkgs) {
701 $bill_opt{'time'} = $next_bill;
702 $bill_opt{'return_bill'} = $return_bill[$i] = [];
703 $bill_opt{'pkg_list'} = $next_bill_pkgs{$next_bill};
704 $error = $cust_main->bill(%bill_opt);
705 die "$error (simulating recurring billing cycle $i)\n" if $error;
712 ###### END TRANSACTION ######
713 my %quotationpkgnum_of = reverse %pkgnum_of;
716 warn "pkgnums:\n".Dumper(\%pkgnum_of);
717 warn Dumper(\@return_bill);
720 # Careful: none of the foreign keys in here are correct outside the sandbox.
721 # We have a translation table for pkgnums; all others are total lies.
723 my %quotation_pkg; # quotationpkgnum => quotation_pkg
724 foreach my $qp ($self->quotation_pkg) {
725 $quotation_pkg{$qp->quotationpkgnum} = $qp;
726 $qp->set($_, 0) foreach qw(unitsetup unitrecur);
727 $qp->set('freq', '');
728 # flush old tax records
729 foreach ($qp->quotation_pkg_tax) {
731 return "$error (flushing tax records for pkgpart ".$qp->part_pkg->pkgpart.")"
736 my %quotation_pkg_tax; # quotationpkgnum => tax name => quotation_pkg_tax obj
737 my %quotation_pkg_discount; # quotationpkgnum => setuprecur => quotation_pkg_discount obj
739 for (my $i = 0; $i < scalar(@return_bill); $i++) {
740 my $this_bill = $return_bill[$i]->[0];
742 warn "$me billing cycle $i produced no invoice\n";
748 foreach my $cust_bill_pkg (@{ $this_bill->get('cust_bill_pkg') }) {
749 my $pkgnum = $cust_bill_pkg->pkgnum;
750 $cust_bill_pkg{ $cust_bill_pkg->billpkgnum } = $cust_bill_pkg;
752 # taxes/fees; come back to it
753 push @nonpkg_lines, $cust_bill_pkg;
756 my $quotationpkgnum = $quotationpkgnum_of{$pkgnum};
757 my $qp = $quotation_pkg{$quotationpkgnum};
759 # XXX supplemental packages could do this (they have separate pkgnums)
760 # handle that special case at some point
761 warn "$me simulated bill returned a package not on the quotation (pkgpart ".$cust_bill_pkg->pkgpart.")\n";
765 # then this is the first (setup) invoice
766 $qp->set('start_date', $cust_bill_pkg->sdate);
767 $qp->set('unitsetup', $qp->unitsetup + $cust_bill_pkg->unitsetup);
768 # pkgpart_override is a possibility
770 # recurring invoice (should be only one of these per package, though
771 # it may have multiple lineitems with the same pkgnum)
772 $qp->set('unitrecur', $qp->unitrecur + $cust_bill_pkg->unitrecur);
776 if ( $cust_bill_pkg->get('discounts') ) {
777 # discount records are generated as (setup, recur).
778 # well, not always, sometimes it's just (recur), but fixing this
779 # is horribly invasive.
780 my $discount = $cust_bill_pkg->get('discounts')->[0];
783 # find the quotation_pkg_discount record for this billing pass...
784 my $setuprecur = $i ? 'recur' : 'setup';
785 my $qpd = $quotation_pkg_discount{$quotationpkgnum}{$setuprecur}
786 ||= qsearchs('quotation_pkg_discount', {
787 'quotationpkgnum' => $quotationpkgnum,
788 'setuprecur' => $setuprecur,
791 if (!$qpd) { #can't happen
792 warn "$me simulated bill returned a $setuprecur discount but no discount is in effect.\n";
795 $qpd->set('amount', $discount->amount);
798 } # end of discount stuff
803 foreach my $cust_bill_pkg (@nonpkg_lines) {
805 my $itemdesc = $cust_bill_pkg->itemdesc;
807 if ($cust_bill_pkg->feepart) {
808 warn "$me simulated bill included a non-package fee (feepart ".
809 $cust_bill_pkg->feepart.")\n";
812 my $links = $cust_bill_pkg->get('cust_bill_pkg_tax_location') ||
813 $cust_bill_pkg->get('cust_bill_pkg_tax_rate_location') ||
815 # breadth-first unrolled recursion:
816 # take each tax link and any tax-on-tax descendants, and merge them
817 # into a single quotation_pkg_tax record for each pkgnum/taxname
819 while (my $tax_link = shift @$links) {
820 my $target = $cust_bill_pkg{ $tax_link->taxable_billpkgnum }
821 or die "$me unable to resolve tax link\n";
822 if ($target->pkgnum) {
823 my $quotationpkgnum = $quotationpkgnum_of{$target->pkgnum};
824 # create this if there isn't one yet
825 my $qpt = $quotation_pkg_tax{$quotationpkgnum}{$itemdesc} ||=
826 FS::quotation_pkg_tax->new({
827 quotationpkgnum => $quotationpkgnum,
828 itemdesc => $itemdesc,
832 if ( $i == 0 ) { # first invoice
833 $qpt->set('setup_amount', $qpt->setup_amount + $tax_link->amount);
834 } else { # subsequent invoices
835 # this isn't perfectly accurate, but that's why it's an estimate
836 $qpt->set('recur_amount', $qpt->recur_amount + $tax_link->amount);
837 $qpt->set('setup_amount', sprintf('%.2f', $qpt->setup_amount - $tax_link->amount));
838 $qpt->set('setup_amount', 0) if $qpt->setup_amount < 0;
840 } elsif ($target->feepart) {
841 # do nothing; we already warned for the fee itself
843 # tax on tax: the tax target is another tax item.
844 # since this is an estimate, I'm just going to assign it to the
845 # first of the underlying packages. (RT#5243 is why we can't have
847 my $sublinks = $target->cust_bill_pkg_tax_rate_location;
848 if ($sublinks and $sublinks->[0]) {
849 $tax_link->set('taxable_billpkgnum', $sublinks->[0]->taxable_billpkgnum);
850 push @$links, $tax_link; #try again
852 warn "$me unable to assign tax on tax; ignoring\n";
855 } # while my $tax_link
857 } # foreach my $cust_bill_pkg
859 foreach my $quotation_pkg (values %quotation_pkg) {
860 $error = $quotation_pkg->replace;
861 return "$error (recording estimate for ".$quotation_pkg->part_pkg->pkg.")"
864 foreach (values %quotation_pkg_discount) {
865 # { setup => one, recur => another }
866 foreach my $quotation_pkg_discount (values %$_) {
867 $error = $quotation_pkg_discount->replace;
868 return "$error (recording estimated discount)"
872 foreach my $quotation_pkg_tax (map { values %$_ } values %quotation_pkg_tax) {
873 $error = $quotation_pkg_tax->insert;
874 return "$error (recording estimated tax for ".$quotation_pkg_tax->itemdesc.")"
887 =item search_sql_where HASHREF
889 Class method which returns an SQL WHERE fragment to search for parameters
890 specified in HASHREF. Valid parameters are
896 List reference of start date, end date, as UNIX timestamps.
906 List reference of charged limits (exclusive).
910 List reference of charged limits (exclusive).
914 flag, return open invoices only
918 flag, return net invoices only
926 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
930 sub search_sql_where {
931 my($class, $param) = @_;
933 # warn "$me search_sql_where called with params: \n".
934 # join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
940 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
941 push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
945 # if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
946 # push @search, "cust_main.refnum = $1";
950 if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
951 push @search, "quotation.prospectnum = $1";
955 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
956 push @search, "cust_bill.custnum = $1";
960 if ( $param->{_date} ) {
961 my($beginning, $ending) = @{$param->{_date}};
963 push @search, "quotation._date >= $beginning",
964 "quotation._date < $ending";
968 if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
969 push @search, "quotation.quotationnum >= $1";
971 if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
972 push @search, "quotation.quotationnum <= $1";
976 # if ( $param->{charged} ) {
977 # my @charged = ref($param->{charged})
978 # ? @{ $param->{charged} }
979 # : ($param->{charged});
981 # push @search, map { s/^charged/cust_bill.charged/; $_; }
985 my $owed_sql = FS::cust_bill->owed_sql;
988 push @search, "quotation._date < ". (time-86400*$param->{'days'})
991 #agent virtualization
992 my $curuser = $FS::CurrentUser::CurrentUser;
993 #false laziness w/search/quotation.html
994 push @search,' ( '. $curuser->agentnums_sql( table=>'prospect_main' ).
995 ' OR '. $curuser->agentnums_sql( table=>'cust_main' ).
998 join(' AND ', @search );
1004 Return line item hashes for each package on this quotation.
1009 my ($self, %options) = @_;
1010 my $escape = $options{'escape_function'};
1011 my $locale = $self->cust_or_prospect->locale;
1013 my $preref = $options{'preref_callback'};
1015 my $section = $options{'section'};
1016 my $freq = $section->{'category'};
1017 my @pkgs = $self->quotation_pkg;
1019 die "_items_pkg called without section->{'category'}"
1020 unless defined $freq;
1022 my %tax_item; # taxname => hashref, will be aggregated AT DISPLAY TIME
1023 # like we should have done in the first place
1025 foreach my $quotation_pkg (@pkgs) {
1026 my $part_pkg = $quotation_pkg->part_pkg;
1027 my @details = $quotation_pkg->details;
1030 'pkgnum' => $quotation_pkg->quotationpkgnum,
1031 'description' => $quotation_pkg->desc($locale),
1032 'ext_description' => \@details,
1033 'quantity' => $quotation_pkg->quantity,
1037 $setuprecur = 'setup';
1038 if ($part_pkg->freq ne '0') {
1039 # indicate that it's a setup fee on a recur package (cust_bill does
1041 $this_item->{'description'} .= ' Setup';
1044 # recur for this frequency
1045 next if $freq ne $part_pkg->freq;
1046 $setuprecur = 'recur';
1049 $this_item->{'unit_amount'} = sprintf('%.2f',
1050 $quotation_pkg->get('unit'.$setuprecur));
1051 $this_item->{'amount'} = sprintf('%.2f', $this_item->{'unit_amount'}
1052 * $quotation_pkg->quantity);
1053 next if $this_item->{'amount'} == 0;
1056 $this_item->{'preref_html'} = &$preref($quotation_pkg);
1059 push @items, $this_item;
1060 my $discount = $quotation_pkg->_item_discount(setuprecur => $setuprecur);
1062 $_ = &{$escape}($_) foreach @{ $discount->{ext_description} };
1063 push @items, $discount;
1066 # each quotation_pkg_tax has two amounts: the amount charged on the
1067 # setup invoice, and the amount on the recurring invoice.
1068 foreach my $qpt ($quotation_pkg->quotation_pkg_tax) {
1069 my $this_tax = $tax_item{$qpt->itemdesc} ||= {
1071 'description' => $qpt->itemdesc,
1072 'ext_description' => [],
1075 $this_tax->{'amount'} += $qpt->get($setuprecur.'_amount');
1077 } # foreach $quotation_pkg
1079 foreach my $taxname ( sort { $a cmp $b } keys (%tax_item) ) {
1080 my $this_tax = $tax_item{$taxname};
1081 $this_tax->{'amount'} = sprintf('%.2f', $this_tax->{'amount'});
1082 next if $this_tax->{'amount'} == 0;
1083 push @items, $this_tax;
1099 L<FS::Record>, schema.html from the base documentation.