Bug 26190: Allow to close a basket with only cancelled order lines
[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        = CGI->new;
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         flagsrequired   => { acquisition => 'order_manage' },
89         debug           => 1,
90     }
91 );
92
93 my $logged_in_patron = Koha::Patrons->find( $loggedinuser );
94
95 our $basket = GetBasket($basketno);
96 $booksellerid = $basket->{booksellerid} unless $booksellerid;
97 my $bookseller = Koha::Acquisition::Booksellers->find( $booksellerid );
98 my $schema = Koha::Database->new()->schema();
99 my $rs = $schema->resultset('VendorEdiAccount')->search(
100     { vendor_id => $booksellerid, } );
101 $template->param( ediaccount => ($rs->count > 0));
102
103 unless (CanUserManageBasket($loggedinuser, $basket, $userflags)) {
104     $template->param(
105         cannot_manage_basket => 1,
106         basketno => $basketno,
107         basketname => $basket->{basketname},
108         booksellerid => $booksellerid,
109         booksellername => $bookseller->name,
110     );
111     output_html_with_http_headers $query, $cookie, $template->output;
112     exit;
113 }
114
115 # FIXME : what about the "discount" percentage?
116 # FIXME : the query->param('booksellerid') below is probably useless. The bookseller is always known from the basket
117 # if no booksellerid in parameter, get it from basket
118 # warn "=>".$basket->{booksellerid};
119 my $op = $query->param('op') // 'list';
120
121 our $confirm_pref= C4::Context->preference("BasketConfirmations") || '1';
122 $template->param( skip_confirm_reopen => 1) if $confirm_pref eq '2';
123
124 my @messages;
125
126 if ( $op eq 'delete_confirm' ) {
127
128     output_and_exit( $query, $cookie, $template, 'insufficient_permission' )
129       unless $logged_in_patron->has_permission( { acquisition => 'delete_baskets' } );
130
131     my $basketno   = $query->param('basketno');
132     my $delbiblio  = $query->param('delbiblio');
133     my $basket_obj = Koha::Acquisition::Baskets->find($basketno);
134
135     my $orders = $basket_obj->orders;
136
137     my @cannotdelbiblios;
138
139     while ( my $order = $orders->next ) {
140         # cancel the order
141         $order->cancel({ delete_biblio => $delbiblio });
142         my @messages = @{ $order->messages };
143
144         if ( scalar @messages > 0 ) {
145
146             my $biblio = $order->biblio;
147
148             push @cannotdelbiblios, {
149                 biblionumber  => $biblio->id,
150                 title         => $biblio->title // '',
151                 author        => $biblio->author // '',
152                 countbiblio   => $biblio->active_orders->count,
153                 itemcount     => $biblio->items->count,
154                 subscriptions => $biblio->subscriptions->count,
155             };
156         }
157     }
158
159     $template->param( cannotdelbiblios => \@cannotdelbiblios );
160
161     # delete the basket
162     $basket_obj->delete;
163     $template->param(
164         delete_confirmed => 1,
165         booksellername => $bookseller->name,
166         booksellerid => $booksellerid,
167     );
168 } elsif ( !$bookseller ) {
169     $template->param( NO_BOOKSELLER => 1 );
170 } elsif ($op eq 'export') {
171     print $query->header(
172         -type       => 'text/csv',
173         -attachment => 'basket' . $basket->{'basketno'} . '.csv',
174     );
175     my $csv_profile_id = $query->param('csv_profile');
176     print GetBasketAsCSV( scalar $query->param('basketno'), $query, $csv_profile_id ); # if no csv_profile_id passed, using default rows
177     exit;
178 } elsif ($op eq 'email') {
179     my $err = eval {
180         SendAlerts( 'orderacquisition', $query->param('basketno'), 'ACQORDER' );
181     };
182     if ( $@ ) {
183         push @messages, { type => 'error', code => $@ };
184     } elsif ( ref $err and exists $err->{error} ) {
185         push @messages, { type => 'error', code => $err->{error} };
186     } else {
187         push @messages, { type => 'message', code => 'email_sent' };
188     }
189
190     $op = 'list';
191 } elsif ($op eq 'close') {
192     my $confirm = $query->param('confirm') || $confirm_pref eq '2';
193     if ($confirm) {
194         my $basketno = $query->param('basketno');
195         my $booksellerid = $query->param('booksellerid');
196         $basketno =~ /^\d+$/ and CloseBasket($basketno);
197         # if requested, create basket group, close it and attach the basket
198         if ($query->param('createbasketgroup')) {
199             my $branchcode;
200             if(C4::Context->userenv and C4::Context->userenv->{'branch'}) {
201                 $branchcode = C4::Context->userenv->{'branch'};
202             }
203             my $basketgroupid = NewBasketgroup( { name => $basket->{basketname},
204                             booksellerid => $booksellerid,
205                             deliveryplace => $branchcode,
206                             billingplace => $branchcode,
207                             closed => 1,
208                             });
209             ModBasket( { basketno => $basketno,
210                          basketgroupid => $basketgroupid } );
211             print $query->redirect('/cgi-bin/koha/acqui/basketgroup.pl?booksellerid='.$booksellerid.'&closed=1');
212         } else {
213             print $query->redirect('/cgi-bin/koha/acqui/booksellers.pl?booksellerid=' . $booksellerid);
214         }
215         exit;
216     } else {
217     $template->param(
218         confirm_close   => "1",
219         booksellerid    => $booksellerid,
220         booksellername  => $bookseller->name,
221         basketno        => $basket->{'basketno'},
222         basketname      => $basket->{'basketname'},
223         basketgroupname => $basket->{'basketname'},
224     );
225     }
226 } elsif ($op eq 'reopen') {
227     ReopenBasket(scalar $query->param('basketno'));
228     print $query->redirect('/cgi-bin/koha/acqui/basket.pl?basketno='.$basket->{'basketno'})
229 }
230 elsif ( $op eq 'ediorder' ) {
231     edi_close_and_order()
232 } elsif ( $op eq 'mod_users' ) {
233     my $basketusers_ids = $query->param('users_ids');
234     my @basketusers = split( /:/, $basketusers_ids );
235     ModBasketUsers($basketno, @basketusers);
236     print $query->redirect("/cgi-bin/koha/acqui/basket.pl?basketno=$basketno");
237     exit;
238 } elsif ( $op eq 'mod_branch' ) {
239     my $branch = $query->param('branch');
240     $branch = undef if(defined $branch and $branch eq '');
241     ModBasket({
242         basketno => $basket->{basketno},
243         branch   => $branch
244     });
245     print $query->redirect("/cgi-bin/koha/acqui/basket.pl?basketno=$basketno");
246     exit;
247 }
248
249 if ( $op eq 'list' ) {
250     my @branches_loop;
251     # get librarian branch...
252     if ( C4::Context->preference("IndependentBranches") ) {
253         my $userenv = C4::Context->userenv;
254         unless ( C4::Context->IsSuperLibrarian() ) {
255             my $validtest = ( $basket->{creationdate} eq '' )
256               || ( $userenv->{branch} eq $basket->{branch} )
257               || ( $userenv->{branch} eq '' )
258               || ( $basket->{branch}  eq '' );
259             unless ($validtest) {
260                 print $query->redirect("../mainpage.pl");
261                 exit 0;
262             }
263         }
264
265         if (!defined $basket->{branch} or $basket->{branch} eq $userenv->{branch}) {
266             push @branches_loop, {
267                 branchcode => $userenv->{branch},
268                 branchname => $userenv->{branchname},
269                 selected => 1,
270             };
271         }
272     } else {
273         # get branches
274         my $branches = Koha::Libraries->search( {}, { order_by => ['branchname'] } )->unblessed;
275         foreach my $branch (@$branches) {
276             my $selected = 0;
277             if (defined $basket->{branch}) {
278                 $selected = 1 if $branch->{branchcode} eq $basket->{branch};
279             } else {
280                 $selected = 1 if $branch->{branchcode} eq C4::Context->userenv->{branch};
281             }
282             push @branches_loop, {
283                 branchcode => $branch->{branchcode},
284                 branchname => $branch->{branchname},
285                 selected => $selected
286             };
287         }
288     }
289
290 #if the basket is closed,and the user has the permission to edit basketgroups, display a list of basketgroups
291     my ($basketgroup, $basketgroups);
292     my $patron = Koha::Patrons->find($loggedinuser);
293     if ($basket->{closedate} && haspermission($patron->userid, { acquisition => 'group_manage'} )) {
294         $basketgroups = GetBasketgroups($basket->{booksellerid});
295         for my $bg ( @{$basketgroups} ) {
296             if ($basket->{basketgroupid} && $basket->{basketgroupid} == $bg->{id}){
297                 $bg->{default} = 1;
298                 $basketgroup = $bg;
299             }
300         }
301     }
302
303     # if the basket is closed, calculate estimated delivery date
304     my $estimateddeliverydate;
305     if( $basket->{closedate} ) {
306         my ($year, $month, $day) = ($basket->{closedate} =~ /(\d+)-(\d+)-(\d+)/);
307         ($year, $month, $day) = Add_Delta_Days($year, $month, $day, $bookseller->deliverytime);
308         $estimateddeliverydate = sprintf( "%04d-%02d-%02d", $year, $month, $day );
309     }
310
311     # if new basket, pre-fill infos
312     $basket->{creationdate} = ""            unless ( $basket->{creationdate} );
313     $basket->{authorisedby} = $loggedinuser unless ( $basket->{authorisedby} );
314     $debug
315       and warn sprintf
316       "loggedinuser: $loggedinuser; creationdate: %s; authorisedby: %s",
317       $basket->{creationdate}, $basket->{authorisedby};
318
319     my @basketusers_ids = GetBasketUsers($basketno);
320     my @basketusers;
321     foreach my $basketuser_id (@basketusers_ids) {
322         # FIXME Could be improved with a search -in
323         my $basket_patron = Koha::Patrons->find( $basketuser_id );
324         push @basketusers, $basket_patron if $basket_patron;
325     }
326
327     my $active_currency = Koha::Acquisition::Currencies->get_active;
328
329     my @orders = GetOrders( $basketno );
330     my @books_loop;
331
332     my @book_foot_loop;
333     my %foot;
334     my $total_quantity = 0;
335     my $total_tax_excluded = 0;
336     my $total_tax_included = 0;
337     my $total_tax_value = 0;
338     for my $order (@orders) {
339         my $line = get_order_infos( $order, $bookseller);
340         if ( $line->{uncertainprice} ) {
341             $template->param( uncertainprices => 1 );
342         }
343
344         $line->{tax_rate} = $line->{tax_rate_on_ordering} // 0;
345         $line->{tax_value} = $line->{tax_value_on_ordering} // 0;
346
347         push @books_loop, $line;
348
349         $foot{$$line{tax_rate}}{tax_rate} = $$line{tax_rate};
350         $foot{$$line{tax_rate}}{tax_value} += get_rounded_price($$line{tax_value});
351         $total_tax_value += $$line{tax_value};
352         $foot{$$line{tax_rate}}{quantity}  += get_rounded_price($$line{quantity});
353         $total_quantity += $$line{quantity};
354         $foot{$$line{tax_rate}}{total_tax_excluded} += $$line{total_tax_excluded};
355         $total_tax_excluded += $$line{total_tax_excluded};
356         $foot{$$line{tax_rate}}{total_tax_included} += $$line{total_tax_included};
357         $total_tax_included += $$line{total_tax_included};
358     }
359
360     push @book_foot_loop, map {$_} values %foot;
361
362     # Get cancelled orders
363     my @cancelledorders = GetOrders($basketno, { cancelled => 1 });
364     my @cancelledorders_loop;
365     for my $order (@cancelledorders) {
366         my $line = get_order_infos( $order, $bookseller);
367         push @cancelledorders_loop, $line;
368     }
369
370     my $contract = GetContract({
371         contractnumber => $basket->{contractnumber}
372     });
373
374     if ($basket->{basketgroupid}){
375         $basketgroup = GetBasketgroup($basket->{basketgroupid});
376     }
377     my $budgets = GetBudgetHierarchy;
378     my $has_budgets = 0;
379     foreach my $r (@{$budgets}) {
380         next unless (CanUserUseBudget($loggedinuser, $r, $userflags));
381
382         $has_budgets = 1;
383         last;
384     }
385
386     $template->param(
387         basketno             => $basketno,
388         basket               => $basket,
389         basketname           => $basket->{'basketname'},
390         basketbranchcode     => $basket->{branch},
391         basketnote           => $basket->{note},
392         basketbooksellernote => $basket->{booksellernote},
393         basketcontractno     => $basket->{contractnumber},
394         basketcontractname   => $contract->{contractname},
395         branches_loop        => \@branches_loop,
396         creationdate         => $basket->{creationdate},
397         authorisedby         => $basket->{authorisedby},
398         authorisedbyname     => $basket->{authorisedbyname},
399         users_ids            => join(':', @basketusers_ids),
400         users                => \@basketusers,
401         closedate            => $basket->{closedate},
402         estimateddeliverydate=> $estimateddeliverydate,
403         is_standing          => $basket->{is_standing},
404         deliveryplace        => $basket->{deliveryplace},
405         billingplace         => $basket->{billingplace},
406         active               => $bookseller->active,
407         booksellerid         => $bookseller->id,
408         booksellername       => $bookseller->name,
409         books_loop           => \@books_loop,
410         book_foot_loop       => \@book_foot_loop,
411         cancelledorders_loop => \@cancelledorders_loop,
412         total_quantity       => $total_quantity,
413         total_tax_excluded   => $total_tax_excluded,
414         total_tax_included   => $total_tax_included,
415         total_tax_value      => $total_tax_value,
416         currency             => $active_currency->currency,
417         listincgst           => $bookseller->listincgst,
418         basketgroups         => $basketgroups,
419         basketgroup          => $basketgroup,
420         grouped              => $basket->{basketgroupid},
421         # The double negatives and booleans here mean:
422         # "A basket cannot be closed if there are no orders in it or it's a standing order basket."
423         #
424         # (The template has another implicit restriction that the order cannot be closed if there
425         # are any orders with uncertain prices.)
426         unclosable           => @orders || @cancelledorders ? $basket->{is_standing} : 1,
427         has_budgets          => $has_budgets,
428         duplinbatch          => $duplinbatch,
429         csv_profiles         => [ Koha::CsvProfiles->search({ type => 'sql', used_for => 'export_basket' }) ],
430         available_additional_fields => [ Koha::AdditionalFields->search( { tablename => 'aqbasket' } ) ],
431         additional_field_values => { map {
432             $_->field->name => $_->value
433         } Koha::Acquisition::Baskets->find($basketno)->additional_field_values->as_list },
434     );
435 }
436
437 $template->param( messages => \@messages );
438 output_html_with_http_headers $query, $cookie, $template->output;
439
440 sub get_order_infos {
441     my $order = shift;
442     my $bookseller = shift;
443     my $qty = $order->{'quantity'} || 0;
444     if ( !defined $order->{quantityreceived} ) {
445         $order->{quantityreceived} = 0;
446     }
447     my $budget = GetBudget($order->{budget_id});
448     my $basket = GetBasket($order->{basketno});
449
450     my %line = %{ $order };
451     # Don't show unreceived standing orders as received
452     $line{order_received} = ( $qty == $order->{'quantityreceived'} && ( $basket->{is_standing} ? $qty : 1 ) );
453     $line{basketno}       = $basketno;
454     $line{budget_name}    = $budget->{budget_name};
455
456     # If we have an actual cost that should be the total, otherwise use the ecost
457     $line{unitprice_tax_included} += 0;
458     $line{unitprice_tax_excluded} += 0;
459     my $cost_tax_included = $line{unitprice_tax_included} || $line{ecost_tax_included};
460     my $cost_tax_excluded = $line{unitprice_tax_excluded} || $line{ecost_tax_excluded};
461     $line{total_tax_included} = get_rounded_price($cost_tax_included) * $line{quantity};
462     $line{total_tax_excluded} = get_rounded_price($cost_tax_excluded) * $line{quantity};
463     $line{tax_value} = $line{tax_value_on_ordering};
464     $line{tax_rate} = $line{tax_rate_on_ordering};
465
466     if ( $line{'title'} ) {
467         my $volume      = $order->{'volume'};
468         my $seriestitle = $order->{'seriestitle'};
469         $line{'title'} .= " / $seriestitle" if $seriestitle;
470         $line{'title'} .= " / $volume"      if $volume;
471     }
472
473     my $biblionumber = $order->{'biblionumber'};
474     if ( $biblionumber ) { # The biblio still exists
475         my $biblio = Koha::Biblios->find( $biblionumber );
476         my $countbiblio = $biblio->active_orders->count;
477
478         my $ordernumber = $order->{'ordernumber'};
479         my $cnt_subscriptions = $biblio->subscriptions->count;
480         my $itemcount   = $biblio->items->count;
481         my $holds_count = $biblio->holds->count;
482         my $order = Koha::Acquisition::Orders->find($ordernumber); # FIXME We should certainly do that at the beginning of this sub
483         my $items = $order->items;
484         my $itemholds  = $biblio->holds->search({ itemnumber => { -in => [ $items->get_column('itemnumber') ] } })->count;
485
486         # 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
487         $line{can_del_bib}          = 1 if $countbiblio <= 1 && $itemcount == $items->count && !($cnt_subscriptions) && !($holds_count);
488         $line{items}                = $itemcount - $items->count;
489         $line{left_item}            = 1 if $line{items} >= 1;
490         $line{left_biblio}          = 1 if $countbiblio > 1;
491         $line{biblios}              = $countbiblio - 1;
492         $line{left_subscription}    = 1 if $cnt_subscriptions;
493         $line{subscriptions}        = $cnt_subscriptions;
494         ($holds_count >= 1) ? $line{left_holds} = 1 : $line{left_holds} = 0;
495         $line{left_holds_on_order}  = 1 if $line{left_holds}==1 && ($line{items} == 0 || $itemholds );
496         $line{holds}                = $holds_count;
497         $line{holds_on_order}       = $itemholds?$itemholds:$holds_count if $line{left_holds_on_order};
498         $line{order_object}         = $order;
499     }
500
501
502     my $suggestion   = GetSuggestionInfoFromBiblionumber($line{biblionumber});
503     $line{suggestionid}         = $$suggestion{suggestionid};
504     $line{surnamesuggestedby}   = $$suggestion{surnamesuggestedby};
505     $line{firstnamesuggestedby} = $$suggestion{firstnamesuggestedby};
506
507     foreach my $key (qw(transferred_from transferred_to)) {
508         if ($line{$key}) {
509             my $order = GetOrder($line{$key});
510             my $basket = GetBasket($order->{basketno});
511             my $bookseller = Koha::Acquisition::Booksellers->find( $basket->{booksellerid} );
512             $line{$key} = {
513                 order => $order,
514                 basket => $basket,
515                 bookseller => $bookseller,
516                 timestamp => $line{$key . '_timestamp'},
517             };
518         }
519     }
520
521     return \%line;
522 }
523
524 sub edi_close_and_order {
525     my $confirm = $query->param('confirm') || $confirm_pref eq '2';
526     if ($confirm) {
527             my $edi_params = {
528                 basketno => $basketno,
529                 ean    => $ean,
530             };
531             if ( $basket->{branch} ) {
532                 $edi_params->{branchcode} = $basket->{branch};
533             }
534             if ( create_edi_order($edi_params) ) {
535                 #$template->param( edifile => 1 );
536             }
537         CloseBasket($basketno);
538
539         # if requested, create basket group, close it and attach the basket
540         if ( $query->param('createbasketgroup') ) {
541             my $branchcode;
542             if (    C4::Context->userenv
543                 and C4::Context->userenv->{'branch'} )
544             {
545                 $branchcode = C4::Context->userenv->{'branch'};
546             }
547             my $basketgroupid = NewBasketgroup(
548                 {
549                     name          => $basket->{basketname},
550                     booksellerid  => $booksellerid,
551                     deliveryplace => $branchcode,
552                     billingplace  => $branchcode,
553                     closed        => 1,
554                 }
555             );
556             ModBasket(
557                 {
558                     basketno      => $basketno,
559                     basketgroupid => $basketgroupid
560                 }
561             );
562             print $query->redirect(
563 "/cgi-bin/koha/acqui/basketgroup.pl?booksellerid=$booksellerid&closed=1"
564             );
565         }
566         else {
567             print $query->redirect(
568                 "/cgi-bin/koha/acqui/booksellers.pl?booksellerid=$booksellerid"
569             );
570         }
571         exit;
572     }
573     else {
574         $template->param(
575             edi_confirm     => 1,
576             booksellerid    => $booksellerid,
577             basketno        => $basket->{basketno},
578             basketname      => $basket->{basketname},
579             basketgroupname => $basket->{basketname},
580         );
581         if ($ean) {
582             $template->param( ean => $ean );
583         }
584
585     }
586     return;
587 }