From 563d3467051df5a92a4a5618d2fca767338bca8a Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Thu, 23 Sep 2021 11:37:52 +0100 Subject: [PATCH] Bug 28854: Update circulation functionality for bundles This patch updates the circulation system to account for bundle checkins. We add a content verification step to ensure bundle content is all present at checkin and we use this comparison to mark missing items as lost. Test plan 0) Apply patches up to this point 1) Checkin an item that belongs to a bundle * An alert should be triggered noting that the item belongs to a bundle * The option to remove the item from the bundle should be clear * Click remove should result in the alert dissapearing and the item having been removed from the bundle. 2) Checkin an item bundle * A modal confirmation dialog should appear requesting each item barcode be scanned * As items are scanned they should be highlighted in yellow in the bundle content table * Upon submission; * The user will be alerted to any unexpected items that were scanned and told to put them to one side. * The user will be alerted that any missing items in the validation will have been marked as lost. * The bundle item will be marked as checked in. --- C4/Circulation.pm | 6 + Koha/Item.pm | 16 ++ circ/returns.pl | 64 +++++++- .../prog/en/includes/item-status.inc | 84 +++++++++++ .../en/includes/modals/bundle_contents.inc | 35 +++++ .../admin/preferences/circulation.pref | 4 + .../prog/en/modules/circ/returns.tt | 138 ++++++++++++++++++ 7 files changed, 343 insertions(+), 4 deletions(-) create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/item-status.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/modals/bundle_contents.inc diff --git a/C4/Circulation.pm b/C4/Circulation.pm index 5342668ba3..67c795f377 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -2311,6 +2311,12 @@ sub AddReturn { } } + # Check for bundle status + if ( $item->in_bundle ) { + my $host = $item->bundle_host; + $messages->{InBundle} = $host; + } + my $indexer = Koha::SearchEngine::Indexer->new({ index => $Koha::SearchEngine::BIBLIOS_INDEX }); $indexer->index_records( $item->biblionumber, "specialUpdate", "biblioserver" ); diff --git a/Koha/Item.pm b/Koha/Item.pm index fdb48af7e0..40a082608c 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -397,6 +397,22 @@ sub checkout { return Koha::Checkout->_new_from_dbic( $checkout_rs ); } +=head3 last_checkout + + my $old_checkout = $item->last_checkout; + +Return the last_checkout for this item + +=cut + +sub last_checkout { + my ( $self ) = @_; + my $checkout_rs = $self->_result->old_issues->search( {}, + { order_by => { '-desc' => 'returndate' }, rows => 1 } )->single; + return unless $checkout_rs; + return Koha::Old::Checkout->_new_from_dbic( $checkout_rs ); +} + =head3 holds my $holds = $item->holds(); diff --git a/circ/returns.pl b/circ/returns.pl index 045a30ee31..2d7ea42346 100755 --- a/circ/returns.pl +++ b/circ/returns.pl @@ -38,7 +38,6 @@ use C4::Auth qw( get_template_and_user get_session haspermission ); use C4::Output qw( output_html_with_http_headers ); use C4::Circulation qw( barcodedecode GetBranchItemRule AddReturn updateWrongTransfer LostItem ); use C4::Reserves qw( ModReserve ModReserveAffect GetOtherReserves ); -use C4::Circulation qw( barcodedecode GetBranchItemRule AddReturn updateWrongTransfer LostItem ); use C4::Context; use C4::Items qw( ModItemTransfer ); use C4::Members::Messaging; @@ -301,10 +300,18 @@ if ($barcode) { $template->param( 'multiple_confirmed' => 1 ) if $query->param('multiple_confirm'); + # Block return if bundle and confirm has not been received + my $bundle_confirm = + $item + && $item->is_bundle + && !$query->param('confirm_items_bundle_return'); + $template->param( 'confirm_items_bundle_returned' => 1 ) + if $query->param('confirm_items_bundle_return'); + # do the return ( $returned, $messages, $issue, $borrower ) = AddReturn( $barcode, $userenv_branch, $exemptfine, $return_date ) - unless $needs_confirm; + unless ( $needs_confirm || $bundle_confirm ); if ($returned) { my $time_now = dt_from_string()->truncate( to => 'minute'); @@ -345,7 +352,46 @@ if ($barcode) { ); } } - } elsif ( C4::Context->preference('ShowAllCheckins') and !$messages->{'BadBarcode'} and !$needs_confirm ) { + + # Mark missing bundle items as lost and report unexpected items + if ( $item->is_bundle ) { + my $BundleLostValue = C4::Context->preference('BundleLostValue'); + my $checkout = $item->last_checkout; + my $barcodes = $query->param('verify-items-bundle-contents-barcodes'); + my @barcodes = map { s/^\s+|\s+$//gr } ( split /\n/, $barcodes ); + my $expected_items = { map { $_->barcode => $_ } $item->bundle_items->as_list }; + my $verify_items = Koha::Items->search( { barcode => { 'in' => \@barcodes } } ); + my @unexpected_items; + my @missing_items; + my @bundle_items; + while ( my $verify_item = $verify_items->next ) { + # Fix and lost statuses + $verify_item->itemlost(0); + + # Expected item, remove from lookup table + if ( delete $expected_items->{$verify_item->barcode} ) { + push @bundle_items, $verify_item; + } + # Unexpected item, warn and remove from bundle + else { + $verify_item->remove_from_bundle; + push @unexpected_items, $verify_item; + } + # Store results + $verify_item->store(); + } + for my $missing_item ( keys %{$expected_items} ) { + my $bundle_item = $expected_items->{$missing_item}; + $bundle_item->itemlost($BundleLostValue)->store(); + push @missing_items, $bundle_item; + } + $template->param( + unexpected_items => \@unexpected_items, + missing_items => \@missing_items, + bundle_items => \@bundle_items + ); + } + } elsif ( C4::Context->preference('ShowAllCheckins') and !$messages->{'BadBarcode'} and !$needs_confirm and !$bundle_confirm ) { $input{duedate} = 0; $returneditems{0} = $barcode; $riduedate{0} = 0; @@ -356,6 +402,12 @@ if ($barcode) { if ( $needs_confirm ) { $template->param( needs_confirm => $needs_confirm ); } + + if ( $bundle_confirm ) { + $template->param( + items_bundle_return_confirmation => 1, + ); + } } $template->param( inputloop => \@inputloop ); @@ -553,7 +605,11 @@ foreach my $code ( keys %$messages ) { } elsif ( $code eq 'ReturnClaims' ) { $template->param( ReturnClaims => $messages->{ReturnClaims} ); - } else { + } + elsif ( $code eq 'InBundle' ) { + $template->param( InBundle => $messages->{InBundle} ); + } + else { die "Unknown error code $code"; # note we need all the (empty) elsif's above, or we die. # This forces the issue of staying in sync w/ Circulation.pm } diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/item-status.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/item-status.inc new file mode 100644 index 0000000000..ca6dc1d1ff --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/item-status.inc @@ -0,0 +1,84 @@ +[% USE AuthorisedValues %] +[% USE Branches %] +[% USE Koha %] +[% USE KohaDates %] +[% PROCESS 'i18n.inc' %] +[% transfer = item.get_transfer() %] +[% IF item.checkout %] + + [% IF item.checkout.onsite_checkout %] + [% t('Currently in local use by') | html %] + [% ELSE %] + [% t('Checked out to') | html %] + [% END %] + [% INCLUDE 'patron-title.inc' patron=item.checkout.patron hide_patron_infos_if_needed=1 %] + : [% tx('due {date_due}', { date_due = item.checkout.date_due }) | html %] + +[% ELSIF transfer %] + [% datesent = BLOCK %][% transfer.datesent | $KohaDates %][% END %] + [% tx('In transit from {frombranch} to {tobranch} since {datesent}', { frombranch = Branches.GetName(transfer.frombranch), tobranch = Branches.GetName(transfer.tobranch), datesent = datesent }) %] +[% END %] + +[% IF item.itemlost %] + [% itemlost_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.itemlost', authorised_value = item.itemlost }) %] + [% IF itemlost_description %] + [% itemlost_description | html %] + [% ELSE %] + [% t('Unavailable (lost or missing)') | html %] + [% END %] +[% END %] + +[% IF item.withdrawn %] + [% withdrawn_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.withdrawn', authorised_value = item.withdrawn }) %] + [% IF withdrawn_description %] + [% withdrawn_description | html %] + [% ELSE %] + [% t('Withdrawn') | html %] + [% END %] +[% END %] + +[% IF item.damaged %] + [% damaged_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.damaged', authorised_value = item.damaged }) %] + [% IF damaged_description %] + [% damaged_description | html %] + [% ELSE %] + [% t('Damaged') | html %] + [% END %] +[% END %] + +[% IF item.notforloan || item.effective_itemtype.notforloan %] + + [% t('Not for loan') | html %] + [% notforloan_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.notforloan', authorised_value = item.notforloan }) %] + [% IF notforloan_description %] + ([% notforloan_description | html %]) + [% END %] + +[% END %] + +[% hold = item.holds.next %] +[% IF hold %] + + [% IF hold.waitingdate %] + Waiting at [% Branches.GetName(hold.get_column('branchcode')) | html %] since [% hold.waitingdate | $KohaDates %]. + [% ELSE %] + Item-level hold (placed [% hold.reservedate | $KohaDates %]) for delivery at [% Branches.GetName(hold.get_column('branchcode')) | html %]. + [% END %] + [% IF Koha.Preference('canreservefromotherbranches') %] + [% t('Hold for:') | html %] + [% INCLUDE 'patron-title.inc' patron=hold.borrower hide_patron_infos_if_needed=1 %] + [% END %] + +[% END %] +[% UNLESS item.notforloan || item.effective_itemtype.notforloan || item.onloan || item.itemlost || item.withdrawn || item.damaged || transfer || hold %] + [% t('Available') | html %] +[% END %] + +[% IF ( item.restricted ) %] + [% restricted_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.restricted', authorised_value = item.restricted }) %] + [% IF restricted_description %] + ([% restricted_description | html %]) + [% ELSE %] + [% t('Restricted') | html %] + [% END %] +[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/bundle_contents.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/bundle_contents.inc new file mode 100644 index 0000000000..c88822b4e9 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/bundle_contents.inc @@ -0,0 +1,35 @@ + + diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref index 6c2a33bc6c..90317e316d 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref @@ -12,6 +12,10 @@ Circulation: 1: "Require" 0: "Don't require" - staff to confirm that all parts of an item are present at checkin/checkout. + - + - Use the LOST authorised value + - pref: BundleLostValue + - to represent 'missing from bundle' at return. - - pref: AutoSwitchPatron choices: diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt index 27e5c305da..1caf9a4672 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt @@ -6,6 +6,7 @@ [% USE ItemTypes %] [% USE AuthorisedValues %] [% USE TablesSettings %] +[% PROCESS 'i18n.inc' %] [% PROCESS 'member-display-address-style.inc' %] [% SET footerjs = 1 %] [% BLOCK display_bormessagepref %] @@ -217,6 +218,36 @@ [% END %] + + [% IF missing_items %] +
+

Bundle had missing items

+

Bundle contents list updated

+

View updated contents list

+
+ [% END %] + + + [% IF unexpected_items %] +
+

Bundle had unexpected items

+

Please place the following items to one side

+ +
+ [% END %] + + + [% IF InBundle %] +
+

Item belongs in bundle

+

This item belongs to a bundle: [% INCLUDE 'biblio-title.inc' biblio=InBundle.biblio %] - [% InBundle.barcode | html %]

+

+
+ [% END %] [% IF ( errmsgloop ) %]
@@ -381,6 +412,67 @@
[% END %] + [% IF items_bundle_return_confirmation %] + + [% END %] + [% IF wrongbranch %] + [% INCLUDE 'modals/bundle_contents.inc' %] + [% MACRO jsinclude BLOCK %] [% INCLUDE 'datatables.inc' %] [% INCLUDE 'columns_settings.inc' %] @@ -1188,8 +1282,52 @@ window.open("/cgi-bin/koha/members/printslip.pl?borrowernumber=" + borrowernumber + "&print=checkinslip", "printwindow"); }); + // item bundles + $('#verify-items-bundle-contents-barcodes').on('input', function (ev) { + const barcodes = ev.target.value.split('\n').map(function(s) { return s.trim() }); + $('#items-bundle-contents-table tr').each(function () { + const barcode = this.getAttribute('data-barcode'); + if (barcodes.includes(barcode)) { + this.classList.add('ok'); + } else { + this.classList.remove('ok'); + } + }) + }); + + $('.bundle_remove').on('click', function() { + var component_itemnumber = $(this).data('itemnumber'); + var host_itemnumber = $(this).data('hostnumber'); + var alert = $(this).closest('div'); + var unlink_item_url = "/api/v1/items/" + host_itemnumber + "/bundled_items/item/" + component_itemnumber; + $.ajax({ + type: "DELETE", + url: unlink_item_url, + success: function(){ + alert.remove(); + } + }); + }); + + // print modals + $('.modal.printable').on('shown.bs.modal', function() { + $('.modal-dialog', this).addClass('focused'); + $('body').addClass('modalprinter'); + + if ($(this).hasClass('autoprint')) { + window.print(); + } + }).on('hidden.bs.modal', function() { + $('.modal-dialog', this).removeClass('focused'); + $('body').removeClass('modalprinter'); + }); + + $('.printModal').click(function() { + window.print(); + }); }); + [% END %] [% INCLUDE 'intranet-bottom.inc' %] -- 2.20.1