Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / quotation.pm
1 package FS::quotation;
2 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::otaker_Mixin FS::Record );
3
4 use strict;
5 use FS::Record qw( qsearch qsearchs );
6 use FS::CurrentUser;
7 use FS::cust_main;
8 use FS::prospect_main;
9 use FS::quotation_pkg;
10
11 =head1 NAME
12
13 FS::quotation - Object methods for quotation records
14
15 =head1 SYNOPSIS
16
17   use FS::quotation;
18
19   $record = new FS::quotation \%hash;
20   $record = new FS::quotation { 'column' => 'value' };
21
22   $error = $record->insert;
23
24   $error = $new_record->replace($old_record);
25
26   $error = $record->delete;
27
28   $error = $record->check;
29
30 =head1 DESCRIPTION
31
32 An FS::quotation object represents a quotation.  FS::quotation inherits from
33 FS::Record.  The following fields are currently supported:
34
35 =over 4
36
37 =item quotationnum
38
39 primary key
40
41 =item prospectnum
42
43 prospectnum
44
45 =item custnum
46
47 custnum
48
49 =item _date
50
51 _date
52
53 =item disabled
54
55 disabled
56
57 =item usernum
58
59 usernum
60
61
62 =back
63
64 =head1 METHODS
65
66 =over 4
67
68 =item new HASHREF
69
70 Creates a new quotation.  To add the quotation to the database, see L<"insert">.
71
72 Note that this stores the hash reference, not a distinct copy of the hash it
73 points to.  You can ask the object for a copy with the I<hash> method.
74
75 =cut
76
77 sub table { 'quotation'; }
78 sub notice_name { 'Quotation'; }
79 sub template_conf { 'quotation_'; }
80
81 =item insert
82
83 Adds this record to the database.  If there is an error, returns the error,
84 otherwise returns false.
85
86 =item delete
87
88 Delete this record from the database.
89
90 =item replace OLD_RECORD
91
92 Replaces the OLD_RECORD with this one in the database.  If there is an error,
93 returns the error, otherwise returns false.
94
95 =item check
96
97 Checks all fields to make sure this is a valid quotation.  If there is
98 an error, returns the error, otherwise returns false.  Called by the insert
99 and replace methods.
100
101 =cut
102
103 sub check {
104   my $self = shift;
105
106   my $error = 
107     $self->ut_numbern('quotationnum')
108     || $self->ut_foreign_keyn('prospectnum', 'prospect_main', 'prospectnum' )
109     || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum' )
110     || $self->ut_numbern('_date')
111     || $self->ut_enum('disabled', [ '', 'Y' ])
112     || $self->ut_numbern('usernum')
113   ;
114   return $error if $error;
115
116   $self->_date(time) unless $self->_date;
117
118   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
119
120   $self->SUPER::check;
121 }
122
123 =item prospect_main
124
125 =cut
126
127 sub prospect_main {
128   my $self = shift;
129   qsearchs('prospect_main', { 'prospectnum' => $self->prospectnum } );
130 }
131
132 =item cust_main
133
134 =cut
135
136 sub cust_main {
137   my $self = shift;
138   qsearchs('cust_main', { 'custnum' => $self->custnum } );
139 }
140
141 =item cust_bill_pkg
142
143 =cut
144
145 sub cust_bill_pkg { #actually quotation_pkg objects
146   my $self = shift;
147   qsearch('quotation_pkg', { quotationnum=>$self->quotationnum });
148 }
149
150 =item total_setup
151
152 =cut
153
154 sub total_setup {
155   my $self = shift;
156   $self->_total('setup');
157 }
158
159 =item total_recur [ FREQ ]
160
161 =cut
162
163 sub total_recur {
164   my $self = shift;
165 #=item total_recur [ FREQ ]
166   #my $freq = @_ ? shift : '';
167   $self->_total('recur');
168 }
169
170 sub _total {
171   my( $self, $method ) = @_;
172
173   my $total = 0;
174   $total += $_->$method() for $self->cust_bill_pkg;
175   sprintf('%.2f', $total);
176
177 }
178
179 #prevent things from falsely showing up as taxes, at least until we support
180 # quoting tax amounts..
181 sub _items_tax {
182   return ();
183 }
184 sub _items_nontax {
185   shift->cust_bill_pkg;
186 }
187
188 sub _items_total {
189   my( $self, $total_items ) = @_;
190
191   if ( $self->total_setup > 0 ) {
192     push @$total_items, {
193       'total_item'   => $self->mt( $self->total_recur > 0 ? 'Total Setup' : 'Total' ),
194       'total_amount' => $self->total_setup,
195     };
196   }
197
198   #could/should add up the different recurring frequencies on lines of their own
199   # but this will cover the 95% cases for now
200   if ( $self->total_recur > 0 ) {
201     push @$total_items, {
202       'total_item'   => $self->mt('Total Recurring'),
203       'total_amount' => $self->total_recur,
204     };
205   }
206
207 }
208
209 =item enable_previous
210
211 =cut
212
213 sub enable_previous { 0 }
214
215 =back
216
217 =head1 CLASS METHODS
218
219 =over 4
220
221
222 =item search_sql_where HASHREF
223
224 Class method which returns an SQL WHERE fragment to search for parameters
225 specified in HASHREF.  Valid parameters are
226
227 =over 4
228
229 =item _date
230
231 List reference of start date, end date, as UNIX timestamps.
232
233 =item invnum_min
234
235 =item invnum_max
236
237 =item agentnum
238
239 =item charged
240
241 List reference of charged limits (exclusive).
242
243 =item owed
244
245 List reference of charged limits (exclusive).
246
247 =item open
248
249 flag, return open invoices only
250
251 =item net
252
253 flag, return net invoices only
254
255 =item days
256
257 =item newest_percust
258
259 =back
260
261 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
262
263 =cut
264
265 sub search_sql_where {
266   my($class, $param) = @_;
267   #if ( $DEBUG ) {
268   #  warn "$me search_sql_where called with params: \n".
269   #       join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
270   #}
271
272   my @search = ();
273
274   #agentnum
275   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
276     push @search, "( prospect_main.agentnum = $1 OR cust_main.agentnum = $1 )";
277   }
278
279 #  #refnum
280 #  if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
281 #    push @search, "cust_main.refnum = $1";
282 #  }
283
284   #prospectnum
285   if ( $param->{'prospectnum'} =~ /^(\d+)$/ ) {
286     push @search, "quotation.prospectnum = $1";
287   }
288
289   #custnum
290   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
291     push @search, "cust_bill.custnum = $1";
292   }
293
294   #_date
295   if ( $param->{_date} ) {
296     my($beginning, $ending) = @{$param->{_date}};
297
298     push @search, "quotation._date >= $beginning",
299                   "quotation._date <  $ending";
300   }
301
302   #quotationnum
303   if ( $param->{'quotationnum_min'} =~ /^(\d+)$/ ) {
304     push @search, "quotation.quotationnum >= $1";
305   }
306   if ( $param->{'quotationnum_max'} =~ /^(\d+)$/ ) {
307     push @search, "quotation.quotationnum <= $1";
308   }
309
310 #  #charged
311 #  if ( $param->{charged} ) {
312 #    my @charged = ref($param->{charged})
313 #                    ? @{ $param->{charged} }
314 #                    : ($param->{charged});
315 #
316 #    push @search, map { s/^charged/cust_bill.charged/; $_; }
317 #                      @charged;
318 #  }
319
320   my $owed_sql = FS::cust_bill->owed_sql;
321
322   #days
323   push @search, "quotation._date < ". (time-86400*$param->{'days'})
324     if $param->{'days'};
325
326   #agent virtualization
327   my $curuser = $FS::CurrentUser::CurrentUser;
328   #false laziness w/search/quotation.html
329   push @search,' (    '. $curuser->agentnums_sql( table=>'prospect_main' ).
330                '   OR '. $curuser->agentnums_sql( table=>'cust_main' ).
331                ' )    ';
332
333   join(' AND ', @search );
334
335 }
336
337 =back
338
339 =head1 BUGS
340
341 =head1 SEE ALSO
342
343 L<FS::Record>, schema.html from the base documentation.
344
345 =cut
346
347 1;
348