From bf5d8115f5db26b43f829932b657bf864dce7a04 Mon Sep 17 00:00:00 2001 From: Jonathan Druart Date: Fri, 27 Jul 2018 15:48:31 -0300 Subject: [PATCH] Bug 15184: Add the ability to duplicate existing order lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patchset adds the ability to duplicate existing order lines to a given basket. It will help acquisitions of serials of when the same publication is ordered frequently. The workflow will be: - Create a new basket - Use the "Add to basket" button - Select the new entry "From existing orders (copy)" - Search and select the order you want to duplicate - Define some default values for these orders - Duplicate! Sponsored-by: BULAC - http://www.bulac.fr/ Signed-off-by: Josef Moravec Signed-off-by: Séverine QUEUNE Signed-off-by: Katrin Fischer Signed-off-by: Nick Clemens --- Koha/Acquisition/Order.pm | 62 ++- acqui/duplicate_orders.pl | 177 ++++++++ .../includes/acquisitions-add-to-basket.inc | 1 + .../prog/en/includes/blocking_errors.inc | 2 + .../prog/en/includes/filter-orders.inc | 1 + .../prog/en/modules/acqui/duplicate_orders.tt | 428 ++++++++++++++++++ 6 files changed, 670 insertions(+), 1 deletion(-) create mode 100644 acqui/duplicate_orders.pl create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/acqui/duplicate_orders.tt diff --git a/Koha/Acquisition/Order.pm b/Koha/Acquisition/Order.pm index 5620cf61be..0619fc196b 100644 --- a/Koha/Acquisition/Order.pm +++ b/Koha/Acquisition/Order.pm @@ -22,9 +22,10 @@ use Carp qw( croak ); use Koha::Acquisition::Baskets; use Koha::Acquisition::Funds; use Koha::Acquisition::Invoices; -use Koha::Subscriptions; use Koha::Database; use Koha::DateUtils qw( dt_from_string output_pref ); +use Koha::Items; +use Koha::Subscriptions; use base qw(Koha::Object); @@ -167,6 +168,65 @@ sub subscription { return Koha::Subscription->_new_from_dbic( $subscription_rs ); } +sub items { + my ( $self ) = @_; + # aqorders_items is not a join table + # There is no FK on items (may have been deleted) + my $items_rs = $self->_result->aqorders_items; + my @itemnumbers = $items_rs->get_column( 'itemnumber' )->all; + return Koha::Items->search({ itemnumber => \@itemnumbers }); +} + +sub duplicate_to { + my ( $self, $basket, $default_values ) = @_; + my $new_order; + $default_values //= {}; + Koha::Database->schema->txn_do( + sub { + my $order_info = $self->unblessed; + undef $order_info->{ordernumber}; + for my $field ( + qw( + ordernumber + received_on + datereceived + datecancellationprinted + cancellationreason + purchaseordernumber + claims_count + claimed_date + parent_ordernumber + ) + ) + { + undef $order_info->{$field}; + } + $order_info->{placed_on} = dt_from_string; + $order_info->{entrydate} = dt_from_string; + $order_info->{orderstatus} = 'new'; + $order_info->{quantityreceived} = 0; + while ( my ( $field, $value ) = each %$default_values ) { + $order_info->{$field} = $value; + } + + # FIXME $order_info->{created_by} = logged_in_user? + $order_info->{basketno} = $basket->basketno; + + $new_order = Koha::Acquisition::Order->new($order_info)->store; + my $items = $self->items; + while ( my ($item) = $items->next ) { + my $item_info = $item->unblessed; + undef $item_info->{itemnumber}; + undef $item_info->{barcode}; + my $new_item = Koha::Item->new($item_info)->store; + $new_order->add_item( $new_item->itemnumber ); + } + } + ); + return $new_order; +} + + =head2 Internal methods =head3 _type diff --git a/acqui/duplicate_orders.pl b/acqui/duplicate_orders.pl new file mode 100644 index 0000000000..549c8b495a --- /dev/null +++ b/acqui/duplicate_orders.pl @@ -0,0 +1,177 @@ +#!/usr/bin/perl + +# Copyright 2018 Koha Development Team +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use CGI qw ( -utf8 ); + +use C4::Auth; +use C4::Output; +use C4::Acquisition qw(GetHistory); +use C4::Budgets qw(GetBudgetPeriods GetBudgetHierarchy CanUserUseBudget); +use Koha::Acquisition::Baskets; +use Koha::Acquisition::Currencies; +use Koha::Acquisition::Orders; +use Koha::DateUtils qw(dt_from_string output_pref); + +my $input = new CGI; +my $basketno = $input->param('basketno'); +my $op = $input->param('op') || 'search'; # search, select, batch_edit + +my ( $template, $loggedinuser, $cookie, $userflags ) = get_template_and_user( + { + template_name => "acqui/duplicate_orders.tt", + query => $input, + type => "intranet", + authnotrequired => 0, + flagsrequired => { acquisition => 'order_manage' }, + } +); + +my $basket = Koha::Acquisition::Baskets->find($basketno); + +output_and_exit( $input, $cookie, $template, 'unknown_basket' ) + unless $basket; + +my $vendor = $basket->bookseller; +my $patron = Koha::Patrons->find($loggedinuser)->unblessed; + +my $filters = { + basket => scalar $input->param('basket'), + title => scalar $input->param('title'), + author => scalar $input->param('author'), + isbn => scalar $input->param('isbn'), + name => scalar $input->param('name'), + ean => scalar $input->param('ean'), + basketgroupname => scalar $input->param('basketgroupname'), + booksellerinvoicenumber => scalar $input->param('booksellerinvoicenumber'), + budget => scalar $input->param('budget'), + orderstatus => scalar $input->param('orderstatus'), + ordernumber => scalar $input->param('ordernumber'), + search_children_too => scalar $input->param('search_children_too'), + created_by => scalar $input->multi_param('created_by'), +}; +my $from_placed_on = + eval { dt_from_string( scalar $input->param('from') ) } || dt_from_string; +my $to_placed_on = + eval { dt_from_string( scalar $input->param('to') ) } || dt_from_string; + +unless ( $input->param('from') ) { + # Fill the form with year-1 + $from_placed_on->subtract( years => 1 ); +} +$filters->{from_placed_on} = + output_pref( { dt => $from_placed_on, dateformat => 'iso', dateonly => 1 } ), + $filters->{to_placed_on} = + output_pref( { dt => $to_placed_on, dateformat => 'iso', dateonly => 1 } ), + + my ( @result_order_loop, @selected_order_loop ); +my @ordernumbers = split ',', scalar $input->param('ordernumbers') || ''; +if ( $op eq 'select' ) { + @result_order_loop = map { + my $order = $_; + ( grep { /^$order->{ordernumber}$/ } @ordernumbers ) ? () : $order + } @{ C4::Acquisition::GetHistory(%$filters) }; + + @selected_order_loop = + scalar @ordernumbers + ? @{ C4::Acquisition::GetHistory( ordernumbers => \@ordernumbers ) } + : (); +} +elsif ( $op eq 'batch_edit' ) { + @ordernumbers = $input->multi_param('ordernumber'); + + # build budget list + my $budget_loop = []; + my $budgets_hierarchy = GetBudgetHierarchy; + foreach my $r ( @{$budgets_hierarchy} ) { + next + unless ( C4::Budgets::CanUserUseBudget( $patron, $r, $userflags ) ); + if ( !defined $r->{budget_amount} || $r->{budget_amount} == 0 ) { + next; + } + push @{$budget_loop}, + { + b_id => $r->{budget_id}, + b_txt => $r->{budget_name}, + b_code => $r->{budget_code}, + b_sort1_authcat => $r->{'sort1_authcat'}, + b_sort2_authcat => $r->{'sort2_authcat'}, + b_active => $r->{budget_period_active}, + }; + } + @{$budget_loop} = + sort { uc( $a->{b_txt} ) cmp uc( $b->{b_txt} ) } @{$budget_loop}; + + my @currencies = Koha::Acquisition::Currencies->search; + $template->param( + currencies => \@currencies, + budget_loop => $budget_loop, + ); +} +elsif ( $op eq 'do_duplicate' ) { + my @fields_to_copy = $input->multi_param('copy_existing_value'); + + my $default_values; + for my $field ( + qw(currency budget_id order_internalnote order_vendornote sort1 sort2 )) + { + next if grep { /^$field$/ } @fields_to_copy; + $default_values->{$field} = $input->param("all_$field"); + } + + @ordernumbers = $input->multi_param('ordernumber'); + my @new_ordernumbers; + for my $ordernumber (@ordernumbers) { + my $original_order = Koha::Acquisition::Orders->find($ordernumber); + next unless $original_order; + my $new_order = + $original_order->duplicate_to( $basket, $default_values ); + push @new_ordernumbers, $new_order->ordernumber; + } + + my $new_orders = + C4::Acquisition::GetHistory( ordernumbers => \@new_ordernumbers ); + $template->param( new_orders => $new_orders ); + $op = 'duplication_done'; +} + +my $budgetperiods = C4::Budgets::GetBudgetPeriods; +my $bp_loop = $budgetperiods; +for my $bp ( @{$budgetperiods} ) { + my $hierarchy = C4::Budgets::GetBudgetHierarchy( $$bp{budget_period_id} ); + for my $budget ( @{$hierarchy} ) { + $$budget{budget_display_name} = + sprintf( "%s", ">" x $$budget{depth} . $$budget{budget_name} ); + } + $$bp{hierarchy} = $hierarchy; +} + +$template->param( + basket => $basket, + vendor => $vendor, + filters => $filters, + result_order_loop => \@result_order_loop, + selected_order_loop => \@selected_order_loop, + bp_loop => $bp_loop, + ordernumbers => \@ordernumbers, + op => $op, +); + +output_html_with_http_headers $input, $cookie, $template->output; diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/acquisitions-add-to-basket.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/acquisitions-add-to-basket.inc index 1e2943324f..6e5698f08c 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/acquisitions-add-to-basket.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/acquisitions-add-to-basket.inc @@ -15,6 +15,7 @@
  • From a suggestion
  • From a subscription
  • From a new (empty) record
  • +
  • From existing orders (copy)
  • From an external source
  • From a new file
  • From a staged file
  • diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/blocking_errors.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/blocking_errors.inc index e803427eeb..26664001fc 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/blocking_errors.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/blocking_errors.inc @@ -9,6 +9,8 @@
    This item does not exist.
    [% CASE 'unknown_subscription' %]
    This subscription does not exist.
    + [% CASE 'unknown_basket' %] +
    This basket does not exist.
    [% CASE %][% blocking_error | html %] [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/filter-orders.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/filter-orders.inc index 806551cc3e..84c1bedb20 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/filter-orders.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/filter-orders.inc @@ -1,4 +1,5 @@ [% USE Koha %] +[% USE KohaDates %]
    1. diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/duplicate_orders.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/duplicate_orders.tt new file mode 100644 index 0000000000..e90933d919 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/duplicate_orders.tt @@ -0,0 +1,428 @@ +[% USE Asset %] +[% USE Koha %] +[% USE KohaDates %] +[% SET footerjs = 1 %] +[% INCLUDE 'doc-head-open.inc' %] +Koha › Acquisitions › +[% UNLESS blocking_error %] +Basket [% basket.basketno %] › Duplicate existing orders +[% END %] + +[% INCLUDE 'doc-head-close.inc' %] +[% Asset.css("css/datatables.css") %] + + + + + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'acquisitions-search.inc' %] + + + +
      +
      +
      +
      + +[% INCLUDE 'blocking_errors.inc' %] + +

      Duplicate existing orders

      + +[% IF op == 'search' || op == 'select' %] +
      +
      + + [% IF op == 'search' %] + Search orders + [% ELSE %] + Refine search + [% END %] + [+] + [-] + +
      + [% INCLUDE 'filter-orders.inc' %] + + + + +
      +
      +
      +
      +[% END %] + +[% BLOCK display_order_line %] + [% IF selected %] + + [% ELSE %] + + [% END %] + + [% IF selected %] + + [% ELSE %] + + [% END %] + [% order.ordernumber %] + [% IF order.ordernumber != order.parent_ordernumber %]([% order.parent_ordernumber %])[% END %] + + + [% SWITCH order.orderstatus %] + [% CASE 'new' %]New + [% CASE 'ordered' %]Ordered + [% CASE 'partial' %]Partially received + [% CASE 'complete' %]Received + [% CASE 'cancelled' %]Cancelled + [% END %] + + [% order.basketname %] ([% order.basketno %]) + [% order.authorisedbyname %] + + [% IF ( order.basketgroupid ) %] + [% order.groupname %] ([% order.basketgroupid %]) + [% ELSE %] +   + [% END %] + + [% IF ( order.invoicenumber ) %] + [% order.invoicenumber %] + [% ELSE %] +   + [% END %] + + + [% order.title |html %] +
      [% order.author %]
      [% order.isbn %] + + [% order.name %] + [% order.creationdate | $KohaDates %] + + [% IF order.datereceived %] + [% order.datereceived | $KohaDates %] + [% ELSE %] + + [% END %] + + [% order.quantityreceived %] + [% order.quantity %] + [% order.ecost %] + [% order.budget_name %] + +[% END %] + + +[% IF op == 'select' && ( result_order_loop || selected_order_loop ) %] +
      +
      + + + + + + + + + + + + + + + + + + + + + + + [% FOREACH order IN selected_order_loop %] + [% INCLUDE display_order_line selected => 1 %] + [% END %] + + + [% FOREACH order IN result_order_loop %] + [% INCLUDE display_order_line %] + [% END %] + +
      + Select all + | Clear all +
      Order line (parent)StatusBasketBasket creatorBasket groupInvoice numberSummaryVendorPlaced onReceived onQuantity receivedPending orderUnit costFund
      +
      + + + +
      +
      +
      + +[% ELSIF op == "batch_edit" %] + +
      +
      +

      Duplicate all the orders with the following accounting details:

      +
      + Accounting details +
        +
      1. + + +
      2. +
      3. +
      4. + + + + The original currency value will be copied +
      5. +
      6. + + + + + + The original fund will be used +
      7. + +
      8. + + + + The original internal note will be used +
      9. +
      10. + + + + The original vendor note will be used +
      11. +
      12. +
        The 2 following fields are available for your own usage. They can be useful for statistical purposes
        + + + + The original statistic 1 will be used + +
      13. +
      14. + + + + The original statistic 2 will be used +
      15. +
      +
      +
      + +
      + [% FOREACH ordernumber IN ordernumbers %] + + [% END %] + + + + Cancel +
      +
      + +[% ELSIF op == 'duplication_done' %] + [% IF new_orders %] + + + + + + + + + + + + + + + + + + + + + [% FOREACH order IN new_orders %] + [% INCLUDE display_order_line %] + [% END %] + +
      Order lineStatusBasketBasket creatorBasket groupInvoice numberSummaryVendorPlaced onReceived onQuantity receivedPending orderUnit costFund
      + Return to the basketNo order has been duplicated. Maybe something wrong happened? + [% END %] +[% END %] + +
      +
      + +
      + +
      +
      + +[% MACRO jsinclude BLOCK %] + [% Asset.js("js/acquisitions-menu.js") %] + [% INCLUDE 'calendar.inc' %] + [% INCLUDE 'datatables.inc' %] + [% INCLUDE 'columns_settings.inc' %] + [% Asset.js("js/autocomplete/patrons.js") %] + [% Asset.js("js/acq.js") %] + [% Asset.js("js/funds_sorts.js") %] + [% Asset.js("lib/jquery/plugins/jquery.checkboxes.min.js") %] + +[% END %] + +[% INCLUDE 'intranet-bottom.inc' %]