Bug 23971: Add logging for basket related actions
[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
195         # close the basket
196         # FIXME: we should fetch the object at the beginning of this script
197         #        and get rid of the hash that is passed around
198         Koha::Acquisition::Baskets->find($basketno)->close;
199
200         # if requested, create basket group, close it and attach the basket
201         if ($query->param('createbasketgroup')) {
202             my $branchcode;
203             if(C4::Context->userenv and C4::Context->userenv->{'branch'}) {
204                 $branchcode = C4::Context->userenv->{'branch'};
205             }
206             my $basketgroupid = NewBasketgroup( { name => $basket->{basketname},
207                             booksellerid => $booksellerid,
208                             deliveryplace => $branchcode,
209                             billingplace => $branchcode,
210                             closed => 1,
211                             });
212             ModBasket( { basketno => $basketno,
213                          basketgroupid => $basketgroupid,
214                          borrowernumber => $loggedinuser } );
215             print $query->redirect('/cgi-bin/koha/acqui/basketgroup.pl?booksellerid='.$booksellerid.'&closed=1');
216         } else {
217             print $query->redirect('/cgi-bin/koha/acqui/booksellers.pl?booksellerid=' . $booksellerid);
218         }
219         exit;
220     } else {
221     $template->param(
222         confirm_close   => "1",
223         booksellerid    => $booksellerid,
224         booksellername  => $bookseller->name,
225         basketno        => $basket->{'basketno'},
226         basketname      => $basket->{'basketname'},
227         basketgroupname => $basket->{'basketname'},
228     );
229     }
230 } elsif ($op eq 'reopen') {
231     ReopenBasket(scalar $query->param('basketno'));
232     print $query->redirect('/cgi-bin/koha/acqui/basket.pl?basketno='.$basket->{'basketno'})
233 }
234 elsif ( $op eq 'ediorder' ) {
235     edi_close_and_order()
236 } elsif ( $op eq 'mod_users' ) {
237     my $basketusers_ids = $query->param('users_ids');
238     my @basketusers = split( /:/, $basketusers_ids );
239     ModBasketUsers($basketno, @basketusers);
240     print $query->redirect("/cgi-bin/koha/acqui/basket.pl?basketno=$basketno");
241     exit;
242 } elsif ( $op eq 'mod_branch' ) {
243     my $branch = $query->param('branch');
244     $branch = undef if(defined $branch and $branch eq '');
245     ModBasket({
246         basketno => $basket->{basketno},
247         branch   => $branch,
248         borrowernumber => $loggedinuser
249     });
250     print $query->redirect("/cgi-bin/koha/acqui/basket.pl?basketno=$basketno");
251     exit;
252 }
253
254 if ( $op eq 'list' ) {
255     my @branches_loop;
256     # get librarian branch...
257     if ( C4::Context->preference("IndependentBranches") ) {
258         my $userenv = C4::Context->userenv;
259         unless ( C4::Context->IsSuperLibrarian() ) {
260             my $validtest = ( $basket->{creationdate} eq '' )
261               || ( $userenv->{branch} eq $basket->{branch} )
262               || ( $userenv->{branch} eq '' )
263               || ( $basket->{branch}  eq '' );
264             unless ($validtest) {
265                 print $query->redirect("../mainpage.pl");
266                 exit 0;
267             }
268         }
269
270         if (!defined $basket->{branch} or $basket->{branch} eq $userenv->{branch}) {
271             push @branches_loop, {
272                 branchcode => $userenv->{branch},
273                 branchname => $userenv->{branchname},
274                 selected => 1,
275             };
276         }
277     } else {
278         # get branches
279         my $branches = Koha::Libraries->search( {}, { order_by => ['branchname'] } )->unblessed;
280         foreach my $branch (@$branches) {
281             my $selected = 0;
282             if (defined $basket->{branch}) {
283                 $selected = 1 if $branch->{branchcode} eq $basket->{branch};
284             } else {
285                 $selected = 1 if $branch->{branchcode} eq C4::Context->userenv->{branch};
286             }
287             push @branches_loop, {
288                 branchcode => $branch->{branchcode},
289                 branchname => $branch->{branchname},
290                 selected => $selected
291             };
292         }
293     }
294
295 #if the basket is closed,and the user has the permission to edit basketgroups, display a list of basketgroups
296     my ($basketgroup, $basketgroups);
297     my $patron = Koha::Patrons->find($loggedinuser);
298     if ($basket->{closedate} && haspermission($patron->userid, { acquisition => 'group_manage'} )) {
299         $basketgroups = GetBasketgroups($basket->{booksellerid});
300         for my $bg ( @{$basketgroups} ) {
301             if ($basket->{basketgroupid} && $basket->{basketgroupid} == $bg->{id}){
302                 $bg->{default} = 1;
303                 $basketgroup = $bg;
304             }
305         }
306     }
307
308     # if the basket is closed, calculate estimated delivery date
309     my $estimateddeliverydate;
310     if( $basket->{closedate} ) {
311         my ($year, $month, $day) = ($basket->{closedate} =~ /(\d+)-(\d+)-(\d+)/);
312         ($year, $month, $day) = Add_Delta_Days($year, $month, $day, $bookseller->deliverytime);
313         $estimateddeliverydate = sprintf( "%04d-%02d-%02d", $year, $month, $day );
314     }
315
316     # if new basket, pre-fill infos
317     $basket->{creationdate} = ""            unless ( $basket->{creationdate} );
318     $basket->{authorisedby} = $loggedinuser unless ( $basket->{authorisedby} );
319     $debug
320       and warn sprintf
321       "loggedinuser: $loggedinuser; creationdate: %s; authorisedby: %s",
322       $basket->{creationdate}, $basket->{authorisedby};
323
324     my @basketusers_ids = GetBasketUsers($basketno);
325     my @basketusers;
326     foreach my $basketuser_id (@basketusers_ids) {
327         # FIXME Could be improved with a search -in
328         my $basket_patron = Koha::Patrons->find( $basketuser_id );
329         push @basketusers, $basket_patron if $basket_patron;
330     }
331
332     my $active_currency = Koha::Acquisition::Currencies->get_active;
333
334     my @orders = GetOrders( $basketno );
335     my @books_loop;
336
337     my @book_foot_loop;
338     my %foot;
339     my $total_quantity = 0;
340     my $total_tax_excluded = 0;
341     my $total_tax_included = 0;
342     my $total_tax_value = 0;
343     for my $order (@orders) {
344         my $line = get_order_infos( $order, $bookseller);
345         if ( $line->{uncertainprice} ) {
346             $template->param( uncertainprices => 1 );
347         }
348
349         $line->{tax_rate} = $line->{tax_rate_on_ordering} // 0;
350         $line->{tax_value} = $line->{tax_value_on_ordering} // 0;
351
352         push @books_loop, $line;
353
354         $foot{$$line{tax_rate}}{tax_rate} = $$line{tax_rate};
355         $foot{$$line{tax_rate}}{tax_value} += get_rounded_price($$line{tax_value});
356         $total_tax_value += $$line{tax_value};
357         $foot{$$line{tax_rate}}{quantity}  += get_rounded_price($$line{quantity});
358         $total_quantity += $$line{quantity};
359         $foot{$$line{tax_rate}}{total_tax_excluded} += $$line{total_tax_excluded};
360         $total_tax_excluded += $$line{total_tax_excluded};
361         $foot{$$line{tax_rate}}{total_tax_included} += $$line{total_tax_included};
362         $total_tax_included += $$line{total_tax_included};
363     }
364
365     push @book_foot_loop, map {$_} values %foot;
366
367     # Get cancelled orders
368     my @cancelledorders = GetOrders($basketno, { cancelled => 1 });
369     my @cancelledorders_loop;
370     for my $order (@cancelledorders) {
371         my $line = get_order_infos( $order, $bookseller);
372         push @cancelledorders_loop, $line;
373     }
374
375     my $contract = GetContract({
376         contractnumber => $basket->{contractnumber}
377     });
378
379     if ($basket->{basketgroupid}){
380         $basketgroup = GetBasketgroup($basket->{basketgroupid});
381     }
382     my $budgets = GetBudgetHierarchy;
383     my $has_budgets = 0;
384     foreach my $r (@{$budgets}) {
385         next unless (CanUserUseBudget($loggedinuser, $r, $userflags));
386
387         $has_budgets = 1;
388         last;
389     }
390
391     $template->param(
392         basketno             => $basketno,
393         basket               => $basket,
394         basketname           => $basket->{'basketname'},
395         basketbranchcode     => $basket->{branch},
396         basketnote           => $basket->{note},
397         basketbooksellernote => $basket->{booksellernote},
398         basketcontractno     => $basket->{contractnumber},
399         basketcontractname   => $contract->{contractname},
400         branches_loop        => \@branches_loop,
401         creationdate         => $basket->{creationdate},
402         authorisedby         => $basket->{authorisedby},
403         authorisedbyname     => $basket->{authorisedbyname},
404         users_ids            => join(':', @basketusers_ids),
405         users                => \@basketusers,
406         closedate            => $basket->{closedate},
407         estimateddeliverydate=> $estimateddeliverydate,
408         is_standing          => $basket->{is_standing},
409         deliveryplace        => $basket->{deliveryplace},
410         billingplace         => $basket->{billingplace},
411         active               => $bookseller->active,
412         booksellerid         => $bookseller->id,
413         booksellername       => $bookseller->name,
414         books_loop           => \@books_loop,
415         book_foot_loop       => \@book_foot_loop,
416         cancelledorders_loop => \@cancelledorders_loop,
417         total_quantity       => $total_quantity,
418         total_tax_excluded   => $total_tax_excluded,
419         total_tax_included   => $total_tax_included,
420         total_tax_value      => $total_tax_value,
421         currency             => $active_currency->currency,
422         listincgst           => $bookseller->listincgst,
423         basketgroups         => $basketgroups,
424         basketgroup          => $basketgroup,
425         grouped              => $basket->{basketgroupid},
426         # The double negatives and booleans here mean:
427         # "A basket cannot be closed if there are no orders in it or it's a standing order basket."
428         #
429         # (The template has another implicit restriction that the order cannot be closed if there
430         # are any orders with uncertain prices.)
431         unclosable           => @orders || @cancelledorders ? $basket->{is_standing} : 1,
432         has_budgets          => $has_budgets,
433         duplinbatch          => $duplinbatch,
434         csv_profiles         => [ Koha::CsvProfiles->search({ type => 'sql', used_for => 'export_basket' }) ],
435         available_additional_fields => [ Koha::AdditionalFields->search( { tablename => 'aqbasket' } ) ],
436         additional_field_values => { map {
437             $_->field->name => $_->value
438         } Koha::Acquisition::Baskets->find($basketno)->additional_field_values->as_list },
439     );
440 }
441
442 $template->param( messages => \@messages );
443 output_html_with_http_headers $query, $cookie, $template->output;
444
445 sub get_order_infos {
446     my $order = shift;
447     my $bookseller = shift;
448     my $qty = $order->{'quantity'} || 0;
449     if ( !defined $order->{quantityreceived} ) {
450         $order->{quantityreceived} = 0;
451     }
452     my $budget = GetBudget($order->{budget_id});
453     my $basket = GetBasket($order->{basketno});
454
455     my %line = %{ $order };
456     # Don't show unreceived standing orders as received
457     $line{order_received} = ( $qty == $order->{'quantityreceived'} && ( $basket->{is_standing} ? $qty : 1 ) );
458     $line{basketno}       = $basketno;
459     $line{budget_name}    = $budget->{budget_name};
460
461     # If we have an actual cost that should be the total, otherwise use the ecost
462     $line{unitprice_tax_included} += 0;
463     $line{unitprice_tax_excluded} += 0;
464     my $cost_tax_included = $line{unitprice_tax_included} || $line{ecost_tax_included};
465     my $cost_tax_excluded = $line{unitprice_tax_excluded} || $line{ecost_tax_excluded};
466     $line{total_tax_included} = get_rounded_price($cost_tax_included) * $line{quantity};
467     $line{total_tax_excluded} = get_rounded_price($cost_tax_excluded) * $line{quantity};
468     $line{tax_value} = $line{tax_value_on_ordering};
469     $line{tax_rate} = $line{tax_rate_on_ordering};
470
471     if ( $line{'title'} ) {
472         my $volume      = $order->{'volume'};
473         my $seriestitle = $order->{'seriestitle'};
474         $line{'title'} .= " / $seriestitle" if $seriestitle;
475         $line{'title'} .= " / $volume"      if $volume;
476     }
477
478     my $biblionumber = $order->{'biblionumber'};
479     if ( $biblionumber ) { # The biblio still exists
480         my $biblio = Koha::Biblios->find( $biblionumber );
481         my $countbiblio = $biblio->active_orders->count;
482
483         my $ordernumber = $order->{'ordernumber'};
484         my $cnt_subscriptions = $biblio->subscriptions->count;
485         my $itemcount   = $biblio->items->count;
486         my $holds_count = $biblio->holds->count;
487         my $order = Koha::Acquisition::Orders->find($ordernumber); # FIXME We should certainly do that at the beginning of this sub
488         my $items = $order->items;
489         my $itemholds  = $biblio->holds->search({ itemnumber => { -in => [ $items->get_column('itemnumber') ] } })->count;
490
491         # 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
492         $line{can_del_bib}          = 1 if $countbiblio <= 1 && $itemcount == $items->count && !($cnt_subscriptions) && !($holds_count);
493         $line{items}                = $itemcount - $items->count;
494         $line{left_item}            = 1 if $line{items} >= 1;
495         $line{left_biblio}          = 1 if $countbiblio > 1;
496         $line{biblios}              = $countbiblio - 1;
497         $line{left_subscription}    = 1 if $cnt_subscriptions;
498         $line{subscriptions}        = $cnt_subscriptions;
499         ($holds_count >= 1) ? $line{left_holds} = 1 : $line{left_holds} = 0;
500         $line{left_holds_on_order}  = 1 if $line{left_holds}==1 && ($line{items} == 0 || $itemholds );
501         $line{holds}                = $holds_count;
502         $line{holds_on_order}       = $itemholds?$itemholds:$holds_count if $line{left_holds_on_order};
503         $line{order_object}         = $order;
504     }
505
506
507     my $suggestion   = GetSuggestionInfoFromBiblionumber($line{biblionumber});
508     $line{suggestionid}         = $$suggestion{suggestionid};
509     $line{surnamesuggestedby}   = $$suggestion{surnamesuggestedby};
510     $line{firstnamesuggestedby} = $$suggestion{firstnamesuggestedby};
511
512     foreach my $key (qw(transferred_from transferred_to)) {
513         if ($line{$key}) {
514             my $order = GetOrder($line{$key});
515             my $basket = GetBasket($order->{basketno});
516             my $bookseller = Koha::Acquisition::Booksellers->find( $basket->{booksellerid} );
517             $line{$key} = {
518                 order => $order,
519                 basket => $basket,
520                 bookseller => $bookseller,
521                 timestamp => $line{$key . '_timestamp'},
522             };
523         }
524     }
525
526     return \%line;
527 }
528
529 sub edi_close_and_order {
530     my $confirm = $query->param('confirm') || $confirm_pref eq '2';
531     if ($confirm) {
532         my $edi_params = {
533             basketno => $basketno,
534             ean    => $ean,
535         };
536         if ( $basket->{branch} ) {
537             $edi_params->{branchcode} = $basket->{branch};
538         }
539         if ( create_edi_order($edi_params) ) {
540             #$template->param( edifile => 1 );
541         }
542         Koha::Acquisition::Baskets->find($basketno)->close;
543
544         # if requested, create basket group, close it and attach the basket
545         if ( $query->param('createbasketgroup') ) {
546             my $branchcode;
547             if (    C4::Context->userenv
548                 and C4::Context->userenv->{'branch'} )
549             {
550                 $branchcode = C4::Context->userenv->{'branch'};
551             }
552             my $basketgroupid = NewBasketgroup(
553                 {
554                     name          => $basket->{basketname},
555                     booksellerid  => $booksellerid,
556                     deliveryplace => $branchcode,
557                     billingplace  => $branchcode,
558                     closed        => 1,
559                 }
560             );
561             ModBasket(
562                 {
563                     basketno       => $basketno,
564                     basketgroupid  => $basketgroupid,
565                     borrowernumber => $loggedinuser
566                 }
567             );
568             print $query->redirect(
569 "/cgi-bin/koha/acqui/basketgroup.pl?booksellerid=$booksellerid&closed=1"
570             );
571         }
572         else {
573             print $query->redirect(
574                 "/cgi-bin/koha/acqui/booksellers.pl?booksellerid=$booksellerid"
575             );
576         }
577         exit;
578     }
579     else {
580         $template->param(
581             edi_confirm     => 1,
582             booksellerid    => $booksellerid,
583             basketno        => $basket->{basketno},
584             basketname      => $basket->{basketname},
585             basketgroupname => $basket->{basketname},
586         );
587         if ($ean) {
588             $template->param( ean => $ean );
589         }
590
591     }
592     return;
593 }