explicitly pass quotationnum as an API param and other fixes, #33852
[freeside.git] / FS / FS / ClientAPI / MyAccount / quotation.pm
1 package FS::ClientAPI::MyAccount::quotation;
2
3 use strict;
4 use FS::Record qw(qsearch qsearchs);
5 use FS::quotation;
6 use FS::quotation_pkg;
7
8 our $DEBUG = 0;
9
10 sub _custoragent_session_custnum {
11   FS::ClientAPI::MyAccount::_custoragent_session_custnum(@_);
12 }
13
14 # _quotation(session, quotationnum)
15 # returns that quotation, or '' if it doesn't exist and belong to this
16 # customer
17
18 sub _quotation {
19   my $session = shift;
20   my $quotationnum = shift;
21   my $quotation;
22
23   if ( $quotationnum =~ /^(\d+)$/ ) {
24     $quotation = qsearchs( 'quotation', {
25         'custnum'       => $session->{'custnum'},
26         'usernum'       => $FS::CurrentUser::CurrentUser->usernum,
27         'disabled'      => '',
28         'quotationnum'  => $1,
29     }); 
30     warn "found selfservice quotation #". $quotation->quotationnum."\n"
31       if $quotation and $DEBUG;
32
33     return $quotation;
34   }
35   '';
36 }
37
38 =item list_quotations { session }
39
40 Returns a hashref listing this customer's active self-service quotations.
41 Contents are:
42
43 - 'quotations', an arrayref containing an element for each quotation.
44   - quotationnum, the primary key
45   - _date, the date it was started
46   - num_pkgs, the number of packages
47   - total_setup, the sum of setup fees
48   - total_recur, the sum of recurring charges
49
50 =cut
51
52 sub list_quotations {
53   my $p = shift;
54
55   my($context, $session, $custnum) = _custoragent_session_custnum($p);
56   return { 'error' => $session } if $context eq 'error';
57
58   my @quotations = qsearch('quotation', {
59       'custnum'   => $session->{'custnum'},
60       'usernum'   => $FS::CurrentUser::CurrentUser->usernum,
61       'disabled'  => '',
62   });
63   my @q;
64   foreach my $quotation (@quotations) {
65     warn "found selfservice quotation #". $quotation->quotationnum."\n"
66       if $quotation and $DEBUG;
67     push @q, { 'quotationnum' => $quotation->quotationnum,
68                '_date'        => $quotation->_date,
69                'num_pkgs'     => scalar($quotation->quotation_pkg),
70                'total_setup'  => $quotation->total_setup,
71                'total_recur'  => $quotation->total_recur,
72              };
73   }
74   return { 'quotations' => \@q, 'error' => '' };
75 }
76
77 =item quotation_new { session }
78
79 Creates a quotation and returns its quotationnum.
80
81 =cut
82
83 sub quotation_new {
84   my $p = shift;
85
86   my($context, $session, $custnum) = _custoragent_session_custnum($p);
87   return { 'error' => $session } if $context eq 'error';
88
89   my $quotation = FS::quotation->new({
90       'custnum'   => $session->{'custnum'},
91       'usernum'   => $FS::CurrentUser::CurrentUser->usernum,
92       '_date'     => time,
93   }); 
94   my $error = $quotation->insert;
95   if ( $error ) {
96     warn "failed to create selfservice quotation for custnum #" .
97       $session->{custnum} . "\n";
98     return { 'error' => $error };
99   } else {
100     warn "started new selfservice quotation #". $quotation->quotationnum."\n"
101       if $DEBUG;
102     return { 'error' => $error, 'quotationnum' => $quotation->quotationnum };
103   }
104 }
105
106 =item quotation_delete { session, quotationnum }
107
108 Disables (doesn't actually delete) the specified quotationnum.
109
110 =cut
111
112 sub quotation_delete {
113   my $p = shift;
114
115   my($context, $session, $custnum) = _custoragent_session_custnum($p);
116   return { 'error' => $session } if $context eq 'error';
117
118   my $quotation = _quotation($session, $p->{quotationnum})
119     or return { 'error' => "Quotation not found" };
120   warn "quotation_delete #".$quotation->quotationnum
121     if $DEBUG;
122
123   $quotation->set('disabled' => 'Y');
124   my $error = $quotation->replace;
125   return { 'error' => $error };
126 }
127
128 =item quotation_info { session, quotationnum }
129
130 Returns a hashref describing the specified quotation, containing:
131
132 - "sections", an arrayref containing one section for each billing frequency.
133   Each one will have:
134   - "description"
135   - "subtotal"
136   - "detail_items", an arrayref of detail items, each with:
137     - "pkgnum", the reference number (actually the quotationpkgnum field)
138     - "description", the package name (or tax name)
139     - "quantity"
140     - "amount"
141 - "num_pkgs", the number of packages in the quotation
142 - "total_setup", the sum of setup/one-time charges and their taxes
143 - "total_recur", the sum of all recurring charges and their taxes
144
145 =cut
146
147 sub quotation_info {
148   my $p = shift;
149
150   my($context, $session, $custnum) = _custoragent_session_custnum($p);
151   return { 'error' => $session } if $context eq 'error';
152
153   my $quotation = _quotation($session, $p->{quotationnum})
154     or return { 'error' => "Quotation not found" };
155   warn "quotation_info #".$quotation->quotationnum
156     if $DEBUG;
157
158   my $null_escape = sub { @_ };
159   # 3.x only; 4.x quotation redesign uses actual sections for this
160   # and isn't a weird hack
161   my @items =
162     map { $_->{'pkgnum'} = $_->{'preref_html'}; $_ }
163     $quotation->_items_pkg(escape_function => $null_escape,
164                            preref_callback => sub { shift->quotationpkgnum });
165   push @items, $quotation->_items_total();
166
167   my $sections = [
168     { 'description' => 'Estimated Charges',
169       'detail_items' => \@items
170     }
171   ];
172
173   return { 'error'        => '',
174            'sections'     => $sections,
175            'num_pkgs'     => scalar($quotation->quotation_pkg),
176            'total_setup'  => $quotation->total_setup,
177            'total_recur'  => $quotation->total_recur,
178          };
179 }
180
181 =item quotation_print { session, 'format' }
182
183 Renders the quotation. 'format' can be either 'html' or 'pdf'; the resulting
184 hashref will contain 'document' => the HTML or PDF contents.
185
186 =cut
187
188 sub quotation_print {
189   my $p = shift;
190
191   my($context, $session, $custnum) = _custoragent_session_custnum($p);
192   return { 'error' => $session } if $context eq 'error';
193
194   my $quotation = _quotation($session, $p->{quotationnum})
195     or return { 'error' => "Quotation not found" };
196   warn "quotation_print #".$quotation->quotationnum
197     if $DEBUG;
198
199   my $format = $p->{'format'}
200    or return { 'error' => "No rendering format specified" };
201
202   my $document;
203   if ($format eq 'html') {
204     $document = $quotation->print_html;
205   } elsif ($format eq 'pdf') {
206     $document = $quotation->print_pdf;
207   }
208   warn "$format, ".length($document)." bytes\n"
209     if $DEBUG;
210   return { 'error' => '', 'document' => $document };
211 }
212
213 =item quotation_add_pkg { session, 'pkgpart', 'quantity', [ location opts ] }
214
215 Adds a package to the user's current quotation. Session info and 'pkgpart' are
216 required. 'quantity' defaults to 1.
217
218 Location can be specified as 'locationnum' to use an existing location, or
219 'address1', 'address2', 'city', 'state', 'zip', 'country' to create a new one,
220 or it will default to the customer's service location.
221
222 =cut
223
224 sub quotation_add_pkg {
225   my $p = shift;
226
227   my($context, $session, $custnum) = _custoragent_session_custnum($p);
228   return { 'error' => $session } if $context eq 'error';
229   
230   my $quotation = _quotation($session, $p->{quotationnum})
231     or return { 'error' => "Quotation not found" };
232   my $cust_main = $quotation->cust_main;
233
234   my $pkgpart = $p->{'pkgpart'};
235   my $allowed_pkgpart = $cust_main->agent->pkgpart_hashref;
236
237   my $part_pkg = FS::part_pkg->by_key($pkgpart);
238
239   if (!$part_pkg or
240       (!$allowed_pkgpart->{$pkgpart} and 
241        $cust_main->agentnum != ($part_pkg->agentnum || 0))
242   ) {
243     warn "disallowed quotation_pkg pkgpart $pkgpart\n"
244       if $DEBUG;
245     return { 'error' => "unknown package $pkgpart" };
246   }
247
248   warn "creating quotation_pkg with pkgpart $pkgpart\n"
249     if $DEBUG;
250   my $quotation_pkg = FS::quotation_pkg->new({
251     'quotationnum'  => $quotation->quotationnum,
252     'pkgpart'       => $p->{'pkgpart'},
253     'quantity'      => $p->{'quantity'} || 1,
254   });
255   if ( $p->{locationnum} > 0 ) {
256     $quotation_pkg->set('locationnum', $p->{locationnum});
257   } elsif ( $p->{address1} ) {
258     my $location = FS::cust_location->find_or_insert(
259       'custnum' => $cust_main->custnum,
260       map { $_ => $p->{$_} }
261         qw( address1 address2 city county state zip country )
262     );
263     $quotation_pkg->set('locationnum', $location->locationnum);
264   }
265
266   my $error = $quotation_pkg->insert
267            || $quotation->estimate
268            || '';
269
270   { 'error'         => $error,
271     'quotationnum'  => $quotation->quotationnum };
272 }
273  
274 =item quotation_remove_pkg { session, 'pkgnum' }
275
276 Removes the package from the user's current quotation. 'pkgnum' is required.
277
278 =cut
279
280 sub quotation_remove_pkg {
281   my $p = shift;
282
283   my($context, $session, $custnum) = _custoragent_session_custnum($p);
284   return { 'error' => $session } if $context eq 'error';
285   
286   my $quotation = _quotation($session, $p->{quotationnum})
287     or return { 'error' => "Quotation not found" };
288   my $quotationpkgnum = $p->{pkgnum};
289   my $quotation_pkg = FS::quotation_pkg->by_key($quotationpkgnum);
290   if (!$quotation_pkg
291       or $quotation_pkg->quotationnum != $quotation->quotationnum) {
292     return { 'error' => "unknown quotation item $quotationpkgnum" };
293   }
294   warn "removing quotation_pkg with pkgpart ".$quotation_pkg->pkgpart."\n"
295     if $DEBUG;
296
297   my $error = $quotation_pkg->delete
298            || $quotation->estimate;
299
300   { 'error'         => $error,
301     'quotationnum'  => $quotation->quotationnum };
302 }
303
304 =item quotation_order
305
306 Convert the current quotation to a package order.
307
308 =cut
309
310 sub quotation_order {
311   my $p = shift;
312
313   my($context, $session, $custnum) = _custoragent_session_custnum($p);
314   return { 'error' => $session } if $context eq 'error';
315   
316   my $quotation = _quotation($session, $p->{quotationnum})
317     or return { 'error' => "Quotation not found" };
318
319   my $error = $quotation->order;
320
321   $quotation->set('disabled' => 'Y');
322   $error ||= $quotation->replace;
323
324   return { 'error' => $error };
325 }
326
327 1;