Bug 28854: Record and display who lost the item

This patch records the bundle issue from which an item is marked as lost
so that we may use that to infer who lost the item (for later charges
and display).

Test plan
0) Apply all patches up to this point
1) Checkout a bundle to a user
2) Checkin the bundle and do not scan one of the barcodes at
   confirmation
   * Note that the item not scanned is marked as lost
3) Navigate to the biblio for the lost item and note that it is marked
   as lost.
4) Navigate to the biblio for the collection and expand the collection
   item that contains the lost item. Note the item is marked as lost and
   checkout details are listed.
5) Checkin the lost item
   * The item should be marked as found and the return_claims line should
   be marked as resolved.

Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
This commit is contained in:
Martin Renvoize 2021-09-23 13:58:50 +01:00 committed by Tomas Cohen Arazi
parent d7a339d8b7
commit 3770ebc0e9
No known key found for this signature in database
GPG Key ID: 0A272EA1B2F3C15F
6 changed files with 118 additions and 42 deletions

View File

@ -70,7 +70,8 @@ FIXME : if no replacement price, borrower just doesn't get charged?
sub chargelostitem {
my $dbh = C4::Context->dbh();
my ($borrowernumber, $itemnumber, $amount, $description) = @_;
my $itype = Koha::ItemTypes->find({ itemtype => Koha::Items->find($itemnumber)->effective_itemtype() });
my $item = Koha::Items->find($itemnumber);
my $itype = Koha::ItemTypes->find({ itemtype => $item->effective_itemtype() });
my $replacementprice = $amount;
my $defaultreplacecost = $itype->defaultreplacecost;
my $processfee = $itype->processfee;
@ -80,6 +81,10 @@ sub chargelostitem {
$replacementprice = $defaultreplacecost;
}
my $checkout = Koha::Checkouts->find({ itemnumber => $itemnumber });
if ( !$checkout && $item->in_bundle ) {
my $host = $item->bundle_host;
$checkout = $host->checkout;
}
my $issue_id = $checkout ? $checkout->issue_id : undef;
my $account = Koha::Account->new({ patron_id => $borrowernumber });

View File

@ -441,6 +441,20 @@ sub item_group {
return $item_group;
}
=head3 return_claims
my $return_claims = $item->return_claims;
Return any return_claims associated with this item
=cut
sub return_claims {
my ( $self, $params, $attrs ) = @_;
my $claims_rs = $self->_result->return_claims->search($params, $attrs);
return Koha::Checkouts::ReturnClaims->_new_from_dbic( $claims_rs );
}
=head3 holds
my $holds = $item->holds();
@ -1114,7 +1128,7 @@ Internal function, not exported, called only by Koha::Item->store.
sub _set_found_trigger {
my ( $self, $pre_mod_item ) = @_;
## If item was lost, it has now been found, reverse any list item charges if necessary.
# Reverse any lost item charges if necessary.
my $no_refund_after_days =
C4::Context->preference('NoRefundOnLostReturnedItemsAge');
if ($no_refund_after_days) {

View File

@ -391,43 +391,6 @@ if ($barcode) {
}
}
# Mark missing bundle items as lost and report unexpected items
if ( $item->is_bundle ) {
my $BundleLostValue = C4::Context->preference('BundleLostValue');
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;
@ -445,6 +408,78 @@ if ($barcode) {
items_bundle_return_confirmation => 1,
);
}
# Mark missing bundle items as lost and report unexpected items
if ( $item->is_bundle && $query->param('confirm_items_bundle_return') ) {
my $BundleLostValue = C4::Context->preference('BundleLostValue');
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);
# Update last_seen
$verify_item->datelastseen( dt_from_string()->ymd() );
# Update last_borrowed if actual checkin
$verify_item->datelastborrowed( dt_from_string()->ymd() ) if $issue;
# 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();
# Add return_claim record if this is an actual checkin
if ($issue) {
$bundle_item->_result->create_related(
'return_claims',
{
issue_id => $issue->issue_id,
itemnumber => $bundle_item->itemnumber,
borrowernumber => $issue->borrowernumber,
created_by => C4::Context->userenv()->{number},
created_on => dt_from_string
}
);
}
push @missing_items, $bundle_item;
# NOTE: We cannot use C4::LostItem here because the item itself doesn't have a checkout
# and thus would not get charged.. it's checked out as part of the bundle.
if ( C4::Context->preference('WhenLostChargeReplacementFee') && $issue ) {
C4::Accounts::chargelostitem(
$issue->borrowernumber,
$bundle_item->itemnumber,
$bundle_item->replacementprice,
sprintf( "%s %s %s",
$bundle_item->biblio->title || q{},
$bundle_item->barcode || q{},
$bundle_item->itemcallnumber || q{},
),
);
}
}
$template->param(
unexpected_items => \@unexpected_items,
missing_items => \@missing_items,
bundle_items => \@bundle_items
);
}
}
$template->param( inputloop => \@inputloop );

View File

@ -115,6 +115,8 @@
[% ELSE %]
[% IF subfield.IS_LOST_AV && Koha.Preference("ClaimReturnedLostValue") && aval == Koha.Preference("ClaimReturnedLostValue") %]
<option disabled="disabled" value="[%- aval | html -%]" title="Return claims must be processed from the patron details page">[%- mv.labels.$aval | html -%]</option>
[% ELSIF subfield.IS_LOST_AV && Koha.Preference("BundleLostValue") && aval == Koha.Preference("BundleLostValue") %]
<option disabled="disabled" value="[%- aval | html -%]" title="Bundle losses are set at checkin automatically">[%- mv.labels.$aval | html -%]</option>
[% ELSE %]
<option value="[%- aval | html -%]">[%- mv.labels.$aval | html -%]</option>
[% END %]

View File

@ -1577,6 +1577,7 @@ Note that permanent location is a code, and location may be an authval.
[% IF bundlesEnabled %]
var bundle_settings = [% TablesSettings.GetTableSettings('catalogue', 'detail','bundle_tables','json') | $raw %];
var bundle_lost_value = [% Koha.Preference('BundleLostValue') %];
[% END %]
$(document).ready(function() {
@ -1668,10 +1669,10 @@ Note that permanent location is a code, and location may be an authval.
"searchable": false,
"orderable": true,
"render": function(data, type, row, meta) {
if ( row.lost_status ) {
return "Lost: " + row.lost_status;
if ( row.lost_status == bundle_lost_value ) {
return "Last seen: " + row.last_seen_date;
}
return "";
return "Present";
}
},
{

View File

@ -429,11 +429,14 @@
<th>[% t('Author') | html %]</th>
<th>[% t('Item type') | html %]</th>
<th>[% t('Barcode') | html %]</th>
[% IF !item.onloan %]
<th>[% t('Status') | html %]</th>
[% END %]
</tr>
</thead>
<tbody>
[% FOREACH bundle_item IN item.bundle_items %]
[% IF !item.onloan %]
<tr data-barcode="[% bundle_item.barcode | html %]">
<td>[% INCLUDE 'biblio-title.inc' biblio=bundle_item.biblio link = 1 %]</td>
<td>[% bundle_item.biblio.author | html %]</td>
@ -441,6 +444,14 @@
<td>[% bundle_item.barcode | html %]</td>
<td>[% INCLUDE 'item-status.inc' item=bundle_item %]</td>
</tr>
[% ELSIF !bundle_item.itemlost %]
<tr data-barcode="[% bundle_item.barcode | html %]">
<td>[% INCLUDE 'biblio-title.inc' biblio=bundle_item.biblio link = 1 %]</td>
<td>[% bundle_item.biblio.author | html %]</td>
<td>[% ItemTypes.GetDescription(bundle_item.itype) | html %]</td>
<td>[% bundle_item.barcode | html %]</td>
</tr>
[% END %]
[% END %]
</tbody>
</table>
@ -448,7 +459,11 @@
<div class="form-group">
<label for="verify-items-bundle-contents-barcodes">Barcodes</label>
<textarea autocomplete="off" id="verify-items-bundle-contents-barcodes" name="verify-items-bundle-contents-barcodes" class="form-control"></textarea>
[% IF item.onloan %]
<div class="help-block">[% t('Scan all barcodes of items found in the items bundle. If any items are missing, they will be marked as lost') | html %]</div>
[% ELSE %]
<div class="help-block">[% t('Optionally scan all barcodes of items found in the items bundle to perform an inventory check. If any items are missing, they will be marked as lost') | html %]</div>
[% END %]
</div>
</div>
@ -460,7 +475,11 @@
<input type="hidden" name="dd-[% inputloo.counter | html %]" value="[% inputloo.duedate | html %]" />
<input type="hidden" name="bn-[% inputloo.counter | html %]" value="[% inputloo.borrowernumber | html %]" />
[% END %]
[% IF item.onloan %]
<button type="submit" class="btn btn-default"><i class="fa fa-check"></i> [% t('Confirm checkin and mark missing items as lost') | html %]</button>
[% ELSE %]
<button type="submit" class="btn btn-default"><i class="fa fa-check"></i> [% t('Confirm inventory check and mark items as lost') | html %]</button>
[% END %]
<button type="button" data-dismiss="modal" class="btn btn-default"><i class="fa fa-close"></i> [% t('Cancel') | html %]</button>
</div>
</form>