Bug 21268: Don't remove 0 allocated funds from fund list
[koha.git] / acqui / basket.pl
1 #!/usr/bin/perl
2
3 #script to show display basket of orders
4
5 # Copyright 2000 - 2004 Katipo
6 # Copyright 2008 - 2009 BibLibre SARL
7 #
8 # This file is part of Koha.
9 #
10 # Koha is free software; you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
14 #
15 # Koha is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
19 #
20 # You should have received a copy of the GNU General Public License
21 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22
23 use Modern::Perl;
24 use C4::Auth;
25 use C4::Koha;
26 use C4::Output;
27 use CGI qw ( -utf8 );
28 use C4::Acquisition;
29 use C4::Budgets;
30 use C4::Contract;
31 use C4::Debug;
32 use C4::Biblio;
33 use C4::Items;
34 use C4::Suggestions;
35 use Koha::Biblios;
36 use Koha::Acquisition::Baskets;
37 use Koha::Acquisition::Booksellers;
38 use Koha::Acquisition::Orders;
39 use Koha::Libraries;
40 use C4::Letters qw/SendAlerts/;
41 use Date::Calc qw/Add_Delta_Days/;
42 use Koha::Database;
43 use Koha::EDI qw( create_edi_order get_edifact_ean );
44 use Koha::CsvProfiles;
45 use Koha::Patrons;
46
47 use Koha::AdditionalFields;
48
49 =head1 NAME
50
51 basket.pl
52
53 =head1 DESCRIPTION
54
55  This script display all informations about basket for the supplier given
56  on input arg.  Moreover, it allows us to add a new order for this supplier from
57  an existing record, a suggestion or a new record.
58
59 =head1 CGI PARAMETERS
60
61 =over 4
62
63 =item $basketno
64
65 The basket number.
66
67 =item booksellerid
68
69 the supplier this script have to display the basket.
70
71 =item order
72
73 =back
74
75 =cut
76
77 our $query        = new CGI;
78 our $basketno     = $query->param('basketno');
79 our $ean          = $query->param('ean');
80 our $booksellerid = $query->param('booksellerid');
81 my $duplinbatch =  $query->param('duplinbatch');
82
83 our ( $template, $loggedinuser, $cookie, $userflags ) = get_template_and_user(
84     {
85         template_name   => "acqui/basket.tt",
86         query           => $query,
87         type            => "intranet",
88         authnotrequired => 0,
89         flagsrequired   => { acquisition => 'order_manage' },
90         debug           => 1,
91     }
92 );
93
94 my $logged_in_patron = Koha::Patrons->find( $loggedinuser );
95
96 our $basket = GetBasket($basketno);
97 $booksellerid = $basket->{booksellerid} unless $booksellerid;
98 my $bookseller = Koha::Acquisition::Booksellers->find( $booksellerid );
99 my $schema = Koha::Database->new()->schema();
100 my $rs = $schema->resultset('VendorEdiAccount')->search(
101     { vendor_id => $booksellerid, } );
102 $template->param( ediaccount => ($rs->count > 0));
103
104 unless (CanUserManageBasket($loggedinuser, $basket, $userflags)) {
105     $template->param(
106         cannot_manage_basket => 1,
107         basketno => $basketno,
108         basketname => $basket->{basketname},
109         booksellerid => $booksellerid,
110         booksellername => $bookseller->name,
111     );
112     output_html_with_http_headers $query, $cookie, $template->output;
113     exit;
114 }
115
116 # FIXME : what about the "discount" percentage?
117 # FIXME : the query->param('booksellerid') below is probably useless. The bookseller is always known from the basket
118 # if no booksellerid in parameter, get it from basket
119 # warn "=>".$basket->{booksellerid};
120 my $op = $query->param('op') // 'list';
121
122 our $confirm_pref= C4::Context->preference("BasketConfirmations") || '1';
123 $template->param( skip_confirm_reopen => 1) if $confirm_pref eq '2';
124
125 my @messages;
126
127 if ( $op eq 'delete_confirm' ) {
128
129     output_and_exit( $query, $cookie, $template, 'insufficient_permission' )
130       unless $logged_in_patron->has_permission( { acquisition => 'delete_baskets' } );
131
132     my $basketno = $query->param('basketno');
133     my $delbiblio = $query->param('delbiblio');
134     my @orders = GetOrders($basketno);
135 #Delete all orders included in that basket, and all items received.
136     foreach my $myorder (@orders){
137         DelOrder($myorder->{biblionumber},$myorder->{ordernumber});
138     }
139 # if $delbiblio = 1, delete the records if possible
140     if ((defined $delbiblio)and ($delbiblio ==1)){
141         my @cannotdelbiblios ;
142         foreach my $myorder (@orders){
143             my $biblionumber = $myorder->{'biblionumber'};
144             my $biblio = Koha::Biblios->find( $biblionumber );
145             my $countbiblio = $biblio->active_orders->count;
146             my $ordernumber = $myorder->{'ordernumber'};
147             my $cnt_subscriptions = $biblio->subscriptions->count;
148             my $itemcount = $biblio->items->count;
149             my $error;
150             if ($countbiblio == 0 && $itemcount == 0 && not $cnt_subscriptions ) {
151                 $error = DelBiblio($myorder->{biblionumber}) }
152             else {
153                 push @cannotdelbiblios, {biblionumber=> ($myorder->{biblionumber}),
154                                          title=> $myorder->{'title'},
155                                          author=> $myorder->{'author'},
156                                          countbiblio=> $countbiblio,
157                                          itemcount=>$itemcount,
158                                          subscriptions => $cnt_subscriptions};
159             }
160             if ($error) {
161                 push @cannotdelbiblios, {biblionumber=> ($myorder->{biblionumber}),
162                                          title=> $myorder->{'title'},
163                                          author=> $myorder->{'author'},
164                                          othererror=> $error};
165             }
166         }
167         $template->param( cannotdelbiblios => \@cannotdelbiblios );
168     }
169  # delete the basket
170     DelBasket($basketno,);
171     $template->param(
172         delete_confirmed => 1,
173         booksellername => $bookseller->name,
174         booksellerid => $booksellerid,
175     );
176 } elsif ( !$bookseller ) {
177     $template->param( NO_BOOKSELLER => 1 );
178 } elsif ($op eq 'export') {
179     print $query->header(
180         -type       => 'text/csv',
181         -attachment => 'basket' . $basket->{'basketno'} . '.csv',
182     );
183     my $csv_profile_id = $query->param('csv_profile');
184     print GetBasketAsCSV( scalar $query->param('basketno'), $query, $csv_profile_id ); # if no csv_profile_id passed, using default rows
185     exit;
186 } elsif ($op eq 'email') {
187     my $err = eval {
188         SendAlerts( 'orderacquisition', $query->param('basketno'), 'ACQORDER' );
189     };
190     if ( $@ ) {
191         push @messages, { type => 'error', code => $@ };
192     } elsif ( ref $err and exists $err->{error} ) {
193         push @messages, { type => 'error', code => $err->{error} };
194     } else {
195         push @messages, { type => 'message', code => 'email_sent' };
196     }
197
198     $op = 'list';
199 } elsif ($op eq 'close') {
200     my $confirm = $query->param('confirm') || $confirm_pref eq '2';
201     if ($confirm) {
202         my $basketno = $query->param('basketno');
203         my $booksellerid = $query->param('booksellerid');
204         $basketno =~ /^\d+$/ and CloseBasket($basketno);
205         # if requested, create basket group, close it and attach the basket
206         if ($query->param('createbasketgroup')) {
207             my $branchcode;
208             if(C4::Context->userenv and C4::Context->userenv->{'branch'}) {
209                 $branchcode = C4::Context->userenv->{'branch'};
210             }
211             my $basketgroupid = NewBasketgroup( { name => $basket->{basketname},
212                             booksellerid => $booksellerid,
213                             deliveryplace => $branchcode,
214                             billingplace => $branchcode,
215                             closed => 1,
216                             });
217             ModBasket( { basketno => $basketno,
218                          basketgroupid => $basketgroupid } );
219             print $query->redirect('/cgi-bin/koha/acqui/basketgroup.pl?booksellerid='.$booksellerid.'&closed=1');
220         } else {
221             print $query->redirect('/cgi-bin/koha/acqui/booksellers.pl?booksellerid=' . $booksellerid);
222         }
223         exit;
224     } else {
225     $template->param(
226         confirm_close   => "1",
227         booksellerid    => $booksellerid,
228         booksellername  => $bookseller->name,
229         basketno        => $basket->{'basketno'},
230         basketname      => $basket->{'basketname'},
231         basketgroupname => $basket->{'basketname'},
232     );
233     }
234 } elsif ($op eq 'reopen') {
235     ReopenBasket(scalar $query->param('basketno'));
236     print $query->redirect('/cgi-bin/koha/acqui/basket.pl?basketno='.$basket->{'basketno'})
237 }
238 elsif ( $op eq 'ediorder' ) {
239     edi_close_and_order()
240 } elsif ( $op eq 'mod_users' ) {
241     my $basketusers_ids = $query->param('users_ids');
242     my @basketusers = split( /:/, $basketusers_ids );
243     ModBasketUsers($basketno, @basketusers);
244     print $query->redirect("/cgi-bin/koha/acqui/basket.pl?basketno=$basketno");
245     exit;
246 } elsif ( $op eq 'mod_branch' ) {
247     my $branch = $query->param('branch');
248     $branch = undef if(defined $branch and $branch eq '');
249     ModBasket({
250         basketno => $basket->{basketno},
251         branch   => $branch
252     });
253     print $query->redirect("/cgi-bin/koha/acqui/basket.pl?basketno=$basketno");
254     exit;
255 }
256
257 if ( $op eq 'list' ) {
258     my @branches_loop;
259     # get librarian branch...
260     if ( C4::Context->preference("IndependentBranches") ) {
261         my $userenv = C4::Context->userenv;
262         unless ( C4::Context->IsSuperLibrarian() ) {
263             my $validtest = ( $basket->{creationdate} eq '' )
264               || ( $userenv->{branch} eq $basket->{branch} )
265               || ( $userenv->{branch} eq '' )
266               || ( $basket->{branch}  eq '' );
267             unless ($validtest) {
268                 print $query->redirect("../mainpage.pl");
269                 exit 0;
270             }
271         }
272
273         if (!defined $basket->{branch} or $basket->{branch} eq $userenv->{branch}) {
274             push @branches_loop, {
275                 branchcode => $userenv->{branch},
276                 branchname => $userenv->{branchname},
277                 selected => 1,
278             };
279         }
280     } else {
281         # get branches
282         my $branches = Koha::Libraries->search( {}, { order_by => ['branchname'] } )->unblessed;
283         foreach my $branch (@$branches) {
284             my $selected = 0;
285             if (defined $basket->{branch}) {
286                 $selected = 1 if $branch->{branchcode} eq $basket->{branch};
287             } else {
288                 $selected = 1 if $branch->{branchcode} eq C4::Context->userenv->{branch};
289             }
290             push @branches_loop, {
291                 branchcode => $branch->{branchcode},
292                 branchname => $branch->{branchname},
293                 selected => $selected
294             };
295         }
296     }
297
298 #if the basket is closed,and the user has the permission to edit basketgroups, display a list of basketgroups
299     my ($basketgroup, $basketgroups);
300     my $patron = Koha::Patrons->find($loggedinuser);
301     if ($basket->{closedate} && haspermission($patron->userid, { acquisition => 'group_manage'} )) {
302         $basketgroups = GetBasketgroups($basket->{booksellerid});
303         for my $bg ( @{$basketgroups} ) {
304             if ($basket->{basketgroupid} && $basket->{basketgroupid} == $bg->{id}){
305                 $bg->{default} = 1;
306                 $basketgroup = $bg;
307             }
308         }
309     }
310
311     # if the basket is closed, calculate estimated delivery date
312     my $estimateddeliverydate;
313     if( $basket->{closedate} ) {
314         my ($year, $month, $day) = ($basket->{closedate} =~ /(\d+)-(\d+)-(\d+)/);
315         ($year, $month, $day) = Add_Delta_Days($year, $month, $day, $bookseller->deliverytime);
316         $estimateddeliverydate = sprintf( "%04d-%02d-%02d", $year, $month, $day );
317     }
318
319     # if new basket, pre-fill infos
320     $basket->{creationdate} = ""            unless ( $basket->{creationdate} );
321     $basket->{authorisedby} = $loggedinuser unless ( $basket->{authorisedby} );
322     $debug
323       and warn sprintf
324       "loggedinuser: $loggedinuser; creationdate: %s; authorisedby: %s",
325       $basket->{creationdate}, $basket->{authorisedby};
326
327     my @basketusers_ids = GetBasketUsers($basketno);
328     my @basketusers;
329     foreach my $basketuser_id (@basketusers_ids) {
330         # FIXME Could be improved with a search -in
331         my $basket_patron = Koha::Patrons->find( $basketuser_id );
332         push @basketusers, $basket_patron if $basket_patron;
333     }
334
335     my $active_currency = Koha::Acquisition::Currencies->get_active;
336
337     my @orders = GetOrders( $basketno );
338     my @books_loop;
339
340     my @book_foot_loop;
341     my %foot;
342     my $total_quantity = 0;
343     my $total_tax_excluded = 0;
344     my $total_tax_included = 0;
345     my $total_tax_value = 0;
346     for my $order (@orders) {
347         my $line = get_order_infos( $order, $bookseller);
348         if ( $line->{uncertainprice} ) {
349             $template->param( uncertainprices => 1 );
350         }
351
352         $line->{tax_rate} = $line->{tax_rate_on_ordering} // 0;
353         $line->{tax_value} = $line->{tax_value_on_ordering} // 0;
354
355         push @books_loop, $line;
356
357         $foot{$$line{tax_rate}}{tax_rate} = $$line{tax_rate};
358         $foot{$$line{tax_rate}}{tax_value} += get_rounded_price($$line{tax_value});
359         $total_tax_value += $$line{tax_value};
360         $foot{$$line{tax_rate}}{quantity}  += get_rounded_price($$line{quantity});
361         $total_quantity += $$line{quantity};
362         $foot{$$line{tax_rate}}{total_tax_excluded} += $$line{total_tax_excluded};
363         $total_tax_excluded += $$line{total_tax_excluded};
364         $foot{$$line{tax_rate}}{total_tax_included} += $$line{total_tax_included};
365         $total_tax_included += $$line{total_tax_included};
366     }
367
368     push @book_foot_loop, map {$_} values %foot;
369
370     # Get cancelled orders
371     my @cancelledorders = GetOrders($basketno, { cancelled => 1 });
372     my @cancelledorders_loop;
373     for my $order (@cancelledorders) {
374         my $line = get_order_infos( $order, $bookseller);
375         push @cancelledorders_loop, $line;
376     }
377
378     my $contract = GetContract({
379         contractnumber => $basket->{contractnumber}
380     });
381
382     if ($basket->{basketgroupid}){
383         $basketgroup = GetBasketgroup($basket->{basketgroupid});
384     }
385     my $budgets = GetBudgetHierarchy;
386     my $has_budgets = 0;
387     foreach my $r (@{$budgets}) {
388         next unless (CanUserUseBudget($loggedinuser, $r, $userflags));
389
390         $has_budgets = 1;
391         last;
392     }
393
394     $template->param(
395         basketno             => $basketno,
396         basket               => $basket,
397         basketname           => $basket->{'basketname'},
398         basketbranchcode     => $basket->{branch},
399         basketnote           => $basket->{note},
400         basketbooksellernote => $basket->{booksellernote},
401         basketcontractno     => $basket->{contractnumber},
402         basketcontractname   => $contract->{contractname},
403         branches_loop        => \@branches_loop,
404         creationdate         => $basket->{creationdate},
405         authorisedby         => $basket->{authorisedby},
406         authorisedbyname     => $basket->{authorisedbyname},
407         users_ids            => join(':', @basketusers_ids),
408         users                => \@basketusers,
409         closedate            => $basket->{closedate},
410         estimateddeliverydate=> $estimateddeliverydate,
411         is_standing          => $basket->{is_standing},
412         deliveryplace        => $basket->{deliveryplace},
413         billingplace         => $basket->{billingplace},
414         active               => $bookseller->active,
415         booksellerid         => $bookseller->id,
416         booksellername       => $bookseller->name,
417         books_loop           => \@books_loop,
418         book_foot_loop       => \@book_foot_loop,
419         cancelledorders_loop => \@cancelledorders_loop,
420         total_quantity       => $total_quantity,
421         total_tax_excluded   => $total_tax_excluded,
422         total_tax_included   => $total_tax_included,
423         total_tax_value      => $total_tax_value,
424         currency             => $active_currency->currency,
425         listincgst           => $bookseller->listincgst,
426         basketgroups         => $basketgroups,
427         basketgroup          => $basketgroup,
428         grouped              => $basket->{basketgroupid},
429         # The double negatives and booleans here mean:
430         # "A basket cannot be closed if there are no orders in it or it's a standing order basket."
431         #
432         # (The template has another implicit restriction that the order cannot be closed if there
433         # are any orders with uncertain prices.)
434         unclosable           => @orders ? $basket->{is_standing} : 1,
435         has_budgets          => $has_budgets,
436         duplinbatch          => $duplinbatch,
437         csv_profiles         => [ Koha::CsvProfiles->search({ type => 'sql', used_for => 'export_basket' }) ],
438         available_additional_fields => [ Koha::AdditionalFields->search( { tablename => 'aqbasket' } ) ],
439         additional_field_values => { map {
440             $_->field->name => $_->value
441         } Koha::Acquisition::Baskets->find($basketno)->additional_field_values->as_list },
442     );
443 }
444
445 $template->param( messages => \@messages );
446 output_html_with_http_headers $query, $cookie, $template->output;
447
448 sub get_order_infos {
449     my $order = shift;
450     my $bookseller = shift;
451     my $qty = $order->{'quantity'} || 0;
452     if ( !defined $order->{quantityreceived} ) {
453         $order->{quantityreceived} = 0;
454     }
455     my $budget = GetBudget($order->{budget_id});
456     my $basket = GetBasket($order->{basketno});
457
458     my %line = %{ $order };
459     # Don't show unreceived standing orders as received
460     $line{order_received} = ( $qty == $order->{'quantityreceived'} && ( $basket->{is_standing} ? $qty : 1 ) );
461     $line{basketno}       = $basketno;
462     $line{budget_name}    = $budget->{budget_name};
463
464     # If we have an actual cost that should be the total, otherwise use the ecost
465     $line{unitprice_tax_included} += 0;
466     $line{unitprice_tax_excluded} += 0;
467     my $cost_tax_included = $line{unitprice_tax_included} || $line{ecost_tax_included};
468     my $cost_tax_excluded = $line{unitprice_tax_excluded} || $line{ecost_tax_excluded};
469     $line{total_tax_included} = get_rounded_price($cost_tax_included) * $line{quantity};
470     $line{total_tax_excluded} = get_rounded_price($cost_tax_excluded) * $line{quantity};
471     $line{tax_value} = $line{tax_value_on_ordering};
472     $line{tax_rate} = $line{tax_rate_on_ordering};
473
474     if ( $line{'title'} ) {
475         my $volume      = $order->{'volume'};
476         my $seriestitle = $order->{'seriestitle'};
477         $line{'title'} .= " / $seriestitle" if $seriestitle;
478         $line{'title'} .= " / $volume"      if $volume;
479     }
480
481     my $biblionumber = $order->{'biblionumber'};
482     if ( $biblionumber ) { # The biblio still exists
483         my $biblio = Koha::Biblios->find( $biblionumber );
484         my $countbiblio = $biblio->active_orders->count;
485
486         my $ordernumber = $order->{'ordernumber'};
487         my $cnt_subscriptions = $biblio->subscriptions->count;
488         my $itemcount   = $biblio->items->count;
489         my $holds_count = $biblio->holds->count;
490         my $order = Koha::Acquisition::Orders->find($ordernumber); # FIXME We should certainly do that at the beginning of this sub
491         my $items = $order->items;
492         my $itemholds  = $biblio->holds->search({ itemnumber => { -in => [ $items->get_column('itemnumber') ] } })->count;
493
494         # if the biblio is not in other orders and if there is no items elsewhere and no subscriptions and no holds we can then show the link "Delete order and Biblio" see bug 5680
495         $line{can_del_bib}          = 1 if $countbiblio <= 1 && $itemcount == $items->count && !($cnt_subscriptions) && !($holds_count);
496         $line{items}                = $itemcount - $items->count;
497         $line{left_item}            = 1 if $line{items} >= 1;
498         $line{left_biblio}          = 1 if $countbiblio > 1;
499         $line{biblios}              = $countbiblio - 1;
500         $line{left_subscription}    = 1 if $cnt_subscriptions;
501         $line{subscriptions}        = $cnt_subscriptions;
502         ($holds_count >= 1) ? $line{left_holds} = 1 : $line{left_holds} = 0;
503         $line{left_holds_on_order}  = 1 if $line{left_holds}==1 && ($line{items} == 0 || $itemholds );
504         $line{holds}                = $holds_count;
505         $line{holds_on_order}       = $itemholds?$itemholds:$holds_count if $line{left_holds_on_order};
506         $line{order_object}         = $order;
507     }
508
509
510     my $suggestion   = GetSuggestionInfoFromBiblionumber($line{biblionumber});
511     $line{suggestionid}         = $$suggestion{suggestionid};
512     $line{surnamesuggestedby}   = $$suggestion{surnamesuggestedby};
513     $line{firstnamesuggestedby} = $$suggestion{firstnamesuggestedby};
514
515     foreach my $key (qw(transferred_from transferred_to)) {
516         if ($line{$key}) {
517             my $order = GetOrder($line{$key});
518             my $basket = GetBasket($order->{basketno});
519             my $bookseller = Koha::Acquisition::Booksellers->find( $basket->{booksellerid} );
520             $line{$key} = {
521                 order => $order,
522                 basket => $basket,
523                 bookseller => $bookseller,
524                 timestamp => $line{$key . '_timestamp'},
525             };
526         }
527     }
528
529     return \%line;
530 }
531
532 sub edi_close_and_order {
533     my $confirm = $query->param('confirm') || $confirm_pref eq '2';
534     if ($confirm) {
535             my $edi_params = {
536                 basketno => $basketno,
537                 ean    => $ean,
538             };
539             if ( $basket->{branch} ) {
540                 $edi_params->{branchcode} = $basket->{branch};
541             }
542             if ( create_edi_order($edi_params) ) {
543                 #$template->param( edifile => 1 );
544             }
545         CloseBasket($basketno);
546
547         # if requested, create basket group, close it and attach the basket
548         if ( $query->param('createbasketgroup') ) {
549             my $branchcode;
550             if (    C4::Context->userenv
551                 and C4::Context->userenv->{'branch'} )
552             {
553                 $branchcode = C4::Context->userenv->{'branch'};
554             }
555             my $basketgroupid = NewBasketgroup(
556                 {
557                     name          => $basket->{basketname},
558                     booksellerid  => $booksellerid,
559                     deliveryplace => $branchcode,
560                     billingplace  => $branchcode,
561                     closed        => 1,
562                 }
563             );
564             ModBasket(
565                 {
566                     basketno      => $basketno,
567                     basketgroupid => $basketgroupid
568                 }
569             );
570             print $query->redirect(
571 "/cgi-bin/koha/acqui/basketgroup.pl?booksellerid=$booksellerid&closed=1"
572             );
573         }
574         else {
575             print $query->redirect(
576                 "/cgi-bin/koha/acqui/booksellers.pl?booksellerid=$booksellerid"
577             );
578         }
579         exit;
580     }
581     else {
582         $template->param(
583             edi_confirm     => 1,
584             booksellerid    => $booksellerid,
585             basketno        => $basket->{basketno},
586             basketname      => $basket->{basketname},
587             basketgroupname => $basket->{basketname},
588         );
589         if ($ean) {
590             $template->param( ean => $ean );
591         }
592
593     }
594     return;
595 }