Merge branch '3.16-help-split' of https://github.com/bgkriegel/koha-translate
[koha.git] / acqui / basketgroup.pl
1 #!/usr/bin/perl
2
3 #script to group (closed) baskets into basket groups for easier order management
4 #written by john.soros@biblibre.com 01/10/2008
5
6 # Copyright 2008 - 2009 BibLibre SARL
7 # Parts Copyright Catalyst 2010
8 #
9 # This file is part of Koha.
10 #
11 # Koha is free software; you can redistribute it and/or modify it under the
12 # terms of the GNU General Public License as published by the Free Software
13 # Foundation; either version 2 of the License, or (at your option) any later
14 # version.
15 #
16 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
17 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
18 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License along
21 # with Koha; if not, write to the Free Software Foundation, Inc.,
22 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23
24
25 =head1 NAME
26
27 basketgroup.pl
28
29 =head1 DESCRIPTION
30
31  This script lets the user group (closed) baskets into basket groups for easier order management. Note that the grouped baskets have to be from the same bookseller and
32  have to be closed to be printed or exported.
33
34 =head1 CGI PARAMETERS
35
36 =over 4
37
38 =item $booksellerid
39
40 The bookseller who we want to display the baskets (and basketgroups) of.
41
42 =back
43
44 =cut
45
46 use strict;
47 use warnings;
48 use Carp;
49
50 use C4::Input;
51 use C4::Auth;
52 use C4::Output;
53 use CGI;
54
55 use C4::Bookseller qw/GetBookSellerFromId/;
56 use C4::Budgets qw/ConvertCurrency/;
57 use C4::Acquisition qw/CloseBasketgroup ReOpenBasketgroup GetOrders GetBasketsByBasketgroup GetBasketsByBookseller ModBasketgroup NewBasketgroup DelBasketgroup GetBasketgroups ModBasket GetBasketgroup GetBasket GetBasketGroupAsCSV/;
58 use C4::Bookseller qw/GetBookSellerFromId/;
59 use C4::Branch qw/GetBranches/;
60 use C4::Members qw/GetMember/;
61
62 our $input=new CGI;
63
64 our ($template, $loggedinuser, $cookie)
65     = get_template_and_user({template_name => "acqui/basketgroup.tmpl",
66                              query => $input,
67                              type => "intranet",
68                              authnotrequired => 0,
69                              flagsrequired => {acquisition => 'group_manage'},
70                              debug => 1,
71                 });
72
73 sub parseinputbaskets {
74     my $booksellerid = shift;
75     my $baskets = &GetBasketsByBookseller($booksellerid);
76     for(my $i=0; $i < scalar @$baskets; ++$i) {
77         if( @$baskets[$i] && ! @$baskets[$i]->{'closedate'} ) {
78             splice(@$baskets, $i, 1);
79             --$i;
80         }
81     }
82     foreach my $basket (@$baskets){
83 #perl DBI uses value "undef" for the mysql "NULL" value, so i need to check everywhere where $basket->{'basketgroupid'} is used for undef ☹
84         $basket->{'basketgroupid'} = $input->param($basket->{'basketno'}.'-group') || undef;
85     }
86     return $baskets;
87 }
88
89
90
91 sub parseinputbasketgroups {
92     my $booksellerid = shift;
93     my $baskets = shift;
94     my $basketgroups = &GetBasketgroups($booksellerid);
95     my $newbasketgroups;
96     foreach my $basket (@$baskets){
97         my $basketgroup;
98         my $i = 0;
99         my $exists;
100         if(! $basket->{'basketgroupid'} || $basket->{'basketgroupid'} == 0){
101             $exists = "true";
102         } else {
103             foreach my $basketgroup (@$basketgroups){
104                 if($basket->{'basketgroupid'} == $basketgroup->{'id'}){
105                     $exists = "true";
106                     push(@{$basketgroup->{'basketlist'}}, $basket->{'basketno'});
107                     last;
108                 }
109             }
110         }
111         if (! $exists){
112 #if the basketgroup doesn't exist yet
113             $basketgroup = $newbasketgroups->{$basket->{'basketgroupid'}} || undef;
114             $basketgroup->{'booksellerid'} = $booksellerid;
115         } else {
116             while($i < scalar @$basketgroups && @$basketgroups[$i]->{'id'} != $basket->{'basketgroupid'}){
117                 ++$i;
118             }
119             $basketgroup = @$basketgroups[$i];
120         }
121         $basketgroup->{'id'}=$basket->{'basketgroupid'};
122         $basketgroup->{'name'}=$input->param('basketgroup-'.$basketgroup->{'id'}.'-name') || "";
123         $basketgroup->{'closed'}= $input->param('basketgroup-'.$basketgroup->{'id'}.'-closed');
124         push(@{$basketgroup->{'basketlist'}}, $basket->{'basketno'});
125         if (! $exists){
126             $newbasketgroups->{$basket->{'basketgroupid'}} = $basketgroup;
127         } else {
128             if($basketgroup->{'id'}){
129                 @$basketgroups[$i] = $basketgroup;
130             }
131         }
132     }
133     return($basketgroups, $newbasketgroups);
134 }
135
136 sub BasketTotal {
137     my $basketno = shift;
138     my $bookseller = shift;
139     my $total = 0;
140     my @orders = GetOrders($basketno);
141     for my $order (@orders){
142         $total = $total + ( $order->{ecost} * $order->{quantity} );
143         if ($bookseller->{invoiceincgst} && ! $bookseller->{listincgst} && ( $bookseller->{gstrate} // C4::Context->preference("gist") )) {
144             my $gst = $bookseller->{gstrate} // C4::Context->preference("gist");
145             $total = $total * ( $gst / 100 +1);
146         }
147     }
148     $total .= $bookseller->{invoiceprice} // 0;
149     return $total;
150 }
151
152 #displays all basketgroups and all closed baskets (in their respective groups)
153 sub displaybasketgroups {
154     my $basketgroups = shift;
155     my $bookseller = shift;
156     my $baskets = shift;
157     if (scalar @$basketgroups != 0) {
158         foreach my $basketgroup (@$basketgroups){
159             my $i = 0;
160             my $basketsqty = 0;
161             while($i < scalar(@$baskets)){
162                 my $basket = @$baskets[$i];
163                 if($basket->{'basketgroupid'} && $basket->{'basketgroupid'} == $basketgroup->{'id'}){
164                     $basket->{total} = BasketTotal($basket->{basketno}, $bookseller);
165                     push(@{$basketgroup->{'baskets'}}, $basket);
166                     splice(@$baskets, $i, 1);
167                     ++$basketsqty;
168                     --$i;
169                 }
170                 ++$i;
171             }
172             $basketgroup -> {'basketsqty'} = $basketsqty;
173         }
174         $template->param(basketgroups => $basketgroups);
175     }
176     for(my $i=0; $i < scalar @$baskets; ++$i) {
177         if( ! @$baskets[$i]->{'closedate'} ) {
178             splice(@$baskets, $i, 1);
179             --$i;
180         }else{
181             @$baskets[$i]->{total} = BasketTotal(@$baskets[$i]->{basketno}, $bookseller);
182         }
183     }
184     $template->param(baskets => $baskets);
185     $template->param( booksellername => $bookseller ->{'name'});
186 }
187
188 sub printbasketgrouppdf{
189     my ($basketgroupid) = @_;
190     
191     my $pdfformat = C4::Context->preference("OrderPdfFormat");
192     if ($pdfformat eq 'pdfformat::layout3pages' || $pdfformat eq 'pdfformat::layout2pages' || $pdfformat eq 'pdfformat::layout3pagesfr'){
193         eval {
194         eval "require $pdfformat";
195             import $pdfformat;
196         };
197         if ($@){
198         }
199     }
200     else {
201         print $input->header;  
202         print $input->start_html;  # FIXME Should do a nicer page
203         print "<h1>Invalid PDF Format set</h1>";
204         print "Please go to the systempreferences and set a valid pdfformat";
205         exit;
206     }
207     
208     my $basketgroup = GetBasketgroup($basketgroupid);
209     my $bookseller = GetBookSellerFromId($basketgroup->{'booksellerid'});
210     my $baskets = GetBasketsByBasketgroup($basketgroupid);
211     
212     my %orders;
213     for my $basket (@$baskets) {
214         my @ba_orders;
215         my @ords = &GetOrders($basket->{basketno});
216         for my $ord (@ords) {
217
218             next unless ( $ord->{biblionumber} or $ord->{quantity}> 0 );
219             eval {
220                 require C4::Biblio;
221                 import C4::Biblio;
222             };
223             if ($@){
224                 croak $@;
225             }
226             eval {
227                 require C4::Koha;
228                 import C4::Koha;
229             };
230             if ($@){
231                 croak $@;
232             }
233
234             $ord->{rrp} = ConvertCurrency( $ord->{'currency'}, $ord->{rrp} );
235             if ( $bookseller->{'listincgst'} ) {
236                 $ord->{rrpgsti} = sprintf( "%.2f", $ord->{rrp} );
237                 $ord->{gstgsti} = sprintf( "%.2f", $ord->{gstrate} * 100 );
238                 $ord->{rrpgste} = sprintf( "%.2f", $ord->{rrp} / ( 1 + ( $ord->{gstgsti} / 100 ) ) );
239                 $ord->{gstgste} = sprintf( "%.2f", $ord->{gstgsti} / ( 1 + ( $ord->{gstgsti} / 100 ) ) );
240                 $ord->{ecostgsti} = sprintf( "%.2f", $ord->{ecost} );
241                 $ord->{ecostgste} = sprintf( "%.2f", $ord->{ecost} / ( 1 + ( $ord->{gstgsti} / 100 ) ) );
242                 $ord->{gstvalue} = sprintf( "%.2f", ( $ord->{ecostgsti} - $ord->{ecostgste} ) * $ord->{quantity});
243                 $ord->{totalgste} = sprintf( "%.2f", $ord->{quantity} * $ord->{ecostgste} );
244                 $ord->{totalgsti} = sprintf( "%.2f", $ord->{quantity} * $ord->{ecostgsti} );
245             } else {
246                 $ord->{rrpgsti} = sprintf( "%.2f", $ord->{rrp} * ( 1 + ( $ord->{gstrate} ) ) );
247                 $ord->{rrpgste} = sprintf( "%.2f", $ord->{rrp} );
248                 $ord->{gstgsti} = sprintf( "%.2f", $ord->{gstrate} * 100 );
249                 $ord->{gstgste} = sprintf( "%.2f", $ord->{gstrate} * 100 );
250                 $ord->{ecostgsti} = sprintf( "%.2f", $ord->{ecost} * ( 1 + ( $ord->{gstrate} ) ) );
251                 $ord->{ecostgste} = sprintf( "%.2f", $ord->{ecost} );
252                 $ord->{gstvalue} = sprintf( "%.2f", ( $ord->{ecostgsti} - $ord->{ecostgste} ) * $ord->{quantity});
253                 $ord->{totalgste} = sprintf( "%.2f", $ord->{quantity} * $ord->{ecostgste} );
254                 $ord->{totalgsti} = sprintf( "%.2f", $ord->{quantity} * $ord->{ecostgsti} );
255             }
256             my $bib = GetBiblioData($ord->{biblionumber});
257             my $itemtypes = GetItemTypes();
258
259             #FIXME DELETE ME
260             # 0      1        2        3         4            5         6       7      8        9
261             #isbn, itemtype, author, title, publishercode, quantity, listprice ecost discount gstrate
262
263             # Editor Number
264             my $en;
265             my $marcrecord=eval{MARC::Record::new_from_xml( $ord->{marcxml},'UTF-8' )};
266             if ($marcrecord){
267                 if ( C4::Context->preference("marcflavour") eq 'UNIMARC' ) {
268                     $en = $marcrecord->subfield( '345', "b" );
269                 } elsif ( C4::Context->preference("marcflavour") eq 'MARC21' ) {
270                     $en = $marcrecord->subfield( '037', "a" );
271                 }
272             }
273
274             my $ba_order = {
275                 isbn => ($ord->{isbn} ? $ord->{isbn} : undef),
276                 itemtype => ( $ord->{itemtype} and $bib->{itemtype} ? $itemtypes->{$bib->{itemtype}}->{description} : undef ),
277                 en => ( $en ? $en : undef ),
278             };
279             for my $key ( qw/ gstrate author title itemtype publishercode discount quantity rrpgsti rrpgste gstgsti gstgste ecostgsti ecostgste gstvalue totalgste totalgsti / ) {
280                 $ba_order->{$key} = $ord->{$key};
281             }
282
283             push(@ba_orders, $ba_order);
284         }
285         $orders{$basket->{basketno}} = \@ba_orders;
286     }
287     print $input->header(
288         -type       => 'application/pdf',
289         -attachment => ( $basketgroup->{name} || $basketgroupid ) . '.pdf'
290     );
291     my $pdf = printpdf($basketgroup, $bookseller, $baskets, \%orders, $bookseller->{gstrate} // C4::Context->preference("gist")) || die "pdf generation failed";
292     print $pdf;
293
294 }
295
296 my $op = $input->param('op') || 'display';
297 # possible values of $op :
298 # - add : adds a new basketgroup, or edit an open basketgroup, or display a closed basketgroup
299 # - mod_basket : modify an individual basket of the basketgroup
300 # - validate :  FIXME dead code
301 # - closeandprint : close and print an closed basketgroup in pdf. called by clicking on "Close and print" button in closed basketgroups list
302 # - print : print a closed basketgroup. called by clicking on "Print" button in closed basketgroups list
303 # - export : export in CSV a closed basketgroup. called by clicking on "Export" button in closed basketgroups list
304 # - delete : delete an open basketgroup. called by clicking on "Delete" button in open basketgroups list
305 # - reopen : reopen a closed basketgroup. called by clicking on "Reopen" button in closed basketgroup list
306 # - attachbasket : save a modified basketgroup, or creates a new basketgroup when a basket is closed. called from basket page
307 # - display : display the list of all basketgroups for a vendor
308 my $booksellerid = $input->param('booksellerid');
309 $template->param(booksellerid => $booksellerid);
310
311 if ( $op eq "add" ) {
312 #
313 # if no param('basketgroupid') is not defined, adds a new basketgroup
314 # else, edit (if it is open) or display (if it is close) the basketgroup basketgroupid
315 # the template will know if basketgroup must be displayed or edited, depending on the value of closed key
316 #
317     if(! $booksellerid){
318 # Unknown bookseller
319 # FIXME : ungroupedlist does not seem to be used in this file nor in template
320         $template->param( ungroupedlist => 1);
321         my @booksellers = GetBookSeller('');
322        for (my $i=0; $i < scalar @booksellers; $i++) {
323             my $baskets = &GetBasketsByBookseller($booksellers[$i]->{id});
324             for (my $j=0; $j < scalar @$baskets; $j++) {
325                 if(! @$baskets[$i]->{closedate} || @$baskets[$i]->{basketgroupid}) {
326                     splice(@$baskets, $j, 1);
327                     $j--;
328                 }
329             }
330             if (scalar @$baskets == 0){
331                 splice(@booksellers, $i, 1);
332                 $i--;
333             }
334         }
335     } else {
336 # Known bookseller
337         my $basketgroupid = $input->param('basketgroupid');
338         my $billingplace;
339         my $deliveryplace;
340         my $freedeliveryplace;
341         if ( $basketgroupid ) {
342             # Get the selected baskets in the basketgroup to display them
343             my $selecteds = GetBasketsByBasketgroup($basketgroupid);
344             foreach (@{$selecteds}){
345                 $_->{total} = BasketTotal($_->{basketno}, $_);
346             }
347             $template->param(basketgroupid => $basketgroupid,
348                              selectedbaskets => $selecteds);
349
350             # Get general informations about the basket group to prefill the form
351             my $basketgroup = GetBasketgroup($basketgroupid);
352             $template->param(
353                 name            => $basketgroup->{name},
354                 deliverycomment => $basketgroup->{deliverycomment},
355                 freedeliveryplace => $basketgroup->{freedeliveryplace},
356             );
357             $billingplace  = $basketgroup->{billingplace};
358             $deliveryplace = $basketgroup->{deliveryplace};
359             $freedeliveryplace = $basketgroup->{freedeliveryplace};
360             $template->param( closedbg => ($basketgroup ->{'closed'}) ? 1 : 0);
361         } else {
362             $template->param( closedbg => 0);
363         }
364         # determine default billing and delivery places depending on librarian homebranch and existing basketgroup data
365         my $borrower = GetMember( ( 'borrowernumber' => $loggedinuser ) );
366         $billingplace  = $billingplace  || $borrower->{'branchcode'};
367         $deliveryplace = $deliveryplace || $borrower->{'branchcode'};
368
369         my $branches = C4::Branch::GetBranchesLoop( $billingplace );
370         $template->param( billingplaceloop => $branches );
371         $branches = C4::Branch::GetBranchesLoop( $deliveryplace );
372         $template->param( deliveryplaceloop => $branches );
373         $template->param( booksellerid => $booksellerid );
374     }
375     # the template will display a unique basketgroup
376     $template->param(grouping => 1);
377     my $basketgroups = &GetBasketgroups($booksellerid);
378     my $bookseller = &GetBookSellerFromId($booksellerid);
379     my $baskets = &GetBasketsByBookseller($booksellerid);
380     displaybasketgroups($basketgroups, $bookseller, $baskets);
381 } elsif ($op eq 'mod_basket') {
382 #
383 # edit an individual basket contained in this basketgroup
384 #
385   my $basketno=$input->param('basketno');
386   my $basketgroupid=$input->param('basketgroupid');
387   ModBasket( { basketno => $basketno,
388                          basketgroupid => $basketgroupid } );
389   print $input->redirect("basket.pl?basketno=" . $basketno);
390 } elsif ($op eq 'validate') {
391 #
392 #  FIXME dead code
393 #
394     if(! $booksellerid){
395         $template->param( booksellererror => 1);
396     } else {
397         $template->param( booksellerid => $booksellerid );
398     }
399     my $baskets = parseinputbaskets($booksellerid);
400     my ($basketgroups, $newbasketgroups) = parseinputbasketgroups($booksellerid, $baskets);
401     foreach my $nbgid (keys %$newbasketgroups){
402 #javascript just picks an ID that's higher than anything else, the ID might not be correct..change it and change all the basket's basketgroupid as well
403         my $bgid = NewBasketgroup($newbasketgroups->{$nbgid});
404         ${$newbasketgroups->{$nbgid}}->{'id'} = $bgid;
405         ${$newbasketgroups->{$nbgid}}->{'oldid'} = $nbgid;
406     }
407     foreach my $basket (@$baskets){
408 #if the basket was added to a new basketgroup, first change the groupid to the groupid of the basket in mysql, because it contains the id from javascript otherwise.
409         if ( $basket->{'basketgroupid'} && $newbasketgroups->{$basket->{'basketgroupid'}} ){
410             $basket->{'basketgroupid'} = ${$newbasketgroups->{$basket->{'basketgroupid'}}}->{'id'};
411         }
412         ModBasket($basket);
413     }
414     foreach my $basketgroup (@$basketgroups){
415         if(! $basketgroup->{'id'}){
416             foreach my $basket (@{$basketgroup->{'baskets'}}){
417                 if($input->param('basket'.$basket->{'basketno'}.'changed')){
418                     ModBasket($basket);
419                 }
420             }
421         } elsif ($input->param('basketgroup-'.$basketgroup->{'id'}.'-changed')){
422             ModBasketgroup($basketgroup);
423         }
424     }
425     $basketgroups = &GetBasketgroups($booksellerid);
426     my $bookseller = &GetBookSellerFromId($booksellerid);
427     $baskets = &GetBasketsByBookseller($booksellerid);
428     # keep ungroupedbaskets
429
430     displaybasketgroups($basketgroups, $bookseller, $baskets);
431 } elsif ( $op eq 'closeandprint') {
432 #
433 # close an open basketgroup and generates a pdf
434 #
435     my $basketgroupid = $input->param('basketgroupid');
436     CloseBasketgroup($basketgroupid);
437     printbasketgrouppdf($basketgroupid);
438     exit;
439 }elsif ($op eq 'print'){
440 #
441 # print a closed basketgroup
442 #
443     my $basketgroupid = $input->param('basketgroupid');
444     printbasketgrouppdf($basketgroupid);
445     exit;
446 }elsif ( $op eq "export" ) {
447 #
448 # export a closed basketgroup in csv
449 #
450     my $basketgroupid = $input->param('basketgroupid');
451     print $input->header(
452         -type       => 'text/csv',
453         -attachment => 'basketgroup' . $basketgroupid . '.csv',
454     );
455     print GetBasketGroupAsCSV( $basketgroupid, $input );
456     exit;
457 }elsif( $op eq "delete"){
458 #
459 # delete an closed basketgroup
460 #
461     my $basketgroupid = $input->param('basketgroupid');
462     DelBasketgroup($basketgroupid);
463     print $input->redirect('/cgi-bin/koha/acqui/basketgroup.pl?booksellerid=' . $booksellerid.'&amp;listclosed=1');
464 }elsif ( $op eq 'reopen'){
465 #
466 # reopen a closed basketgroup
467 #
468     my $basketgroupid   = $input->param('basketgroupid');
469     my $booksellerid    = $input->param('booksellerid');
470     ReOpenBasketgroup($basketgroupid);
471     my $redirectpath = ((defined $input->param('mode'))&& ($input->param('mode') eq 'singlebg')) ?'/cgi-bin/koha/acqui/basketgroup.pl?op=add&amp;basketgroupid='.$basketgroupid.'&amp;booksellerid='.$booksellerid : '/cgi-bin/koha/acqui/basketgroup.pl?booksellerid=' .$booksellerid.'&amp;listclosed=1';
472     print $input->redirect($redirectpath);
473 } elsif ( $op eq 'attachbasket') {
474 #
475 # save a modified basketgroup, or creates a new basketgroup when a basket is closed. called from basket page
476 #
477     # Getting parameters
478     my $basketgroup       = {};
479     my @baskets           = $input->param('basket');
480     my $basketgroupid     = $input->param('basketgroupid');
481     my $basketgroupname   = $input->param('basketgroupname');
482     my $booksellerid      = $input->param('booksellerid');
483     my $billingplace      = $input->param('billingplace');
484     my $deliveryplace     = $input->param('deliveryplace');
485     my $freedeliveryplace = $input->param('freedeliveryplace');
486     my $deliverycomment   = $input->param('deliverycomment');
487     my $closedbg          = $input->param('closedbg') ? 1 : 0;
488     if ($basketgroupid) {
489     # If we have a basketgroupid we edit the basketgroup
490         $basketgroup = {
491               name              => $basketgroupname,
492               id                => $basketgroupid,
493               basketlist        => \@baskets,
494               billingplace      => $billingplace,
495               deliveryplace     => $deliveryplace,
496               freedeliveryplace => $freedeliveryplace,
497               deliverycomment   => $deliverycomment,
498               closed            => $closedbg,
499         };
500         ModBasketgroup($basketgroup);
501         if($closedbg){
502 # FIXME
503         }
504     }else{
505     # we create a new basketgroup (whith a closed basket)
506         $basketgroup = {
507             name              => $basketgroupname,
508             booksellerid      => $booksellerid,
509             basketlist        => \@baskets,
510             billingplace      => $billingplace,
511             deliveryplace     => $deliveryplace,
512             freedeliveryplace => $freedeliveryplace,
513             deliverycomment   => $deliverycomment,
514             closed            => $closedbg,
515         };
516         $basketgroupid = NewBasketgroup($basketgroup);
517     }
518     my $redirectpath = ((defined $input->param('mode')) && ($input->param('mode') eq 'singlebg')) ?'/cgi-bin/koha/acqui/basketgroup.pl?op=add&amp;basketgroupid='.$basketgroupid.'&amp;booksellerid='.$booksellerid : '/cgi-bin/koha/acqui/basketgroup.pl?booksellerid=' . $booksellerid;
519     $redirectpath .=  "&amp;listclosed=1" if $closedbg ;
520     print $input->redirect($redirectpath );
521     
522 }else{
523 # no param : display the list of all basketgroups for a given vendor
524     my $basketgroups = &GetBasketgroups($booksellerid);
525     my $bookseller = &GetBookSellerFromId($booksellerid);
526     my $baskets = &GetBasketsByBookseller($booksellerid);
527
528     displaybasketgroups($basketgroups, $bookseller, $baskets);
529 }
530 $template->param(listclosed => ((defined $input->param('listclosed')) && ($input->param('listclosed') eq '1'))? 1:0 );
531 #prolly won't use all these, maybe just use print, the rest can be done inside validate
532 output_html_with_http_headers $input, $cookie, $template->output;