@@ -, +, @@ return an item this table the patron who last returned this item. Replace existing row based on itemnumber if exists. details page. should be a new field in the history list 'Last returned by' with a link to the last patron to return the item. --- C4/Circulation.pm | 8 ++ Koha/Item.pm | 33 +++++ Koha/Schema/Result/ItemsLastBorrower.pm | 133 +++++++++++++++++++++ catalogue/moredetail.pl | 2 + installer/data/mysql/atomicupdate/bug_14945.sql | 13 ++ installer/data/mysql/kohastructure.sql | 16 +++ installer/data/mysql/sysprefs.sql | 1 + .../prog/en/modules/admin/preferences/opac.pref | 8 +- .../prog/en/modules/catalogue/moredetail.tt | 4 + .../opac-tmpl/bootstrap/en/modules/opac-privacy.tt | 1 + t/db_dependent/Circulation/AnonymiseIssueHistory.t | 84 ++++++++++++- 11 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 Koha/Schema/Result/ItemsLastBorrower.pm create mode 100644 installer/data/mysql/atomicupdate/bug_14945.sql --- a/C4/Circulation.pm +++ a/C4/Circulation.pm @@ -47,6 +47,8 @@ use Algorithm::CheckDigits; use Data::Dumper; use Koha::DateUtils; use Koha::Calendar; +use Koha::Items; +use Koha::Borrowers; use Koha::Borrower::Debarments; use Koha::Database; use Carp; @@ -2178,6 +2180,12 @@ sub MarkIssueReturned { $sth_del->execute($borrowernumber, $itemnumber); ModItem( { 'onloan' => undef }, undef, $itemnumber ); + + if ( C4::Context->preference('StoreLastBorrower') ) { + my $item = Koha::Items->find( $itemnumber ); + my $patron = Koha::Borrowers->find( $borrowernumber ); + $item->last_returned_by( $patron ); + } } =head2 _debar_user_on_return --- a/Koha/Item.pm +++ a/Koha/Item.pm @@ -24,6 +24,7 @@ use Carp; use Koha::Database; use Koha::Branches; +use Koha::Borrowers; use base qw(Koha::Object); @@ -73,6 +74,38 @@ sub holding_branch { return $self->{_holding_branch}; } +=head3 last_returned_by + +Gets and sets the last borrower to return an item. + +Accepts and returns Koha::Borrower objects + +$item->last_returned_by( $borrowernumber ); + +$last_returned_by = $item->last_returned_by(); + +=cut + +sub last_returned_by { + my ( $self, $borrower ) = @_; + + my $items_last_returned_by_rs = Koha::Database->new()->schema()->resultset('ItemsLastBorrower'); + + if ($borrower) { + return $items_last_returned_by_rs->update_or_create( + { borrowernumber => $borrower->borrowernumber, itemnumber => $self->id } ); + } + else { + unless ( $self->{_last_returned_by} ) { + my $result = $items_last_returned_by_rs->single( { itemnumber => $self->id } ); + if ($result) { + $self->{_last_returned_by} = Koha::Borrowers->find( $result->get_column('borrowernumber') ); + } + } + + return $self->{_last_returned_by}; + } +} =head3 type --- a/Koha/Schema/Result/ItemsLastBorrower.pm +++ a/Koha/Schema/Result/ItemsLastBorrower.pm @@ -0,0 +1,133 @@ +use utf8; +package Koha::Schema::Result::ItemsLastBorrower; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::ItemsLastBorrower + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("items_last_borrower"); + +=head1 ACCESSORS + +=head2 id + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 itemnumber + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 borrowernumber + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 created_on + + data_type: 'timestamp' + datetime_undef_if_invalid: 1 + default_value: current_timestamp + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "id", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "itemnumber", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "borrowernumber", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "created_on", + { + data_type => "timestamp", + datetime_undef_if_invalid => 1, + default_value => \"current_timestamp", + is_nullable => 0, + }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("id"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("itemnumber", ["itemnumber"]); + +=head1 RELATIONS + +=head2 borrowernumber + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "borrowernumber", + "Koha::Schema::Result::Borrower", + { borrowernumber => "borrowernumber" }, + { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, +); + +=head2 itemnumber + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "itemnumber", + "Koha::Schema::Result::Item", + { itemnumber => "itemnumber" }, + { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07040 @ 2015-10-02 12:33:33 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ByHKNZCEz4a1AqTnOJgUWA + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; --- a/catalogue/moredetail.pl +++ a/catalogue/moredetail.pl @@ -38,6 +38,7 @@ use C4::Reserves qw(GetReservesFromBiblionumber); use Koha::Acquisition::Bookseller; use Koha::DateUtils; +use Koha::Items; my $query=new CGI; @@ -135,6 +136,7 @@ foreach ( keys %{$data} ) { ($itemnumber) and @items = (grep {$_->{'itemnumber'} == $itemnumber} @items); foreach my $item (@items){ + $item->{object} = Koha::Items->find( $item->{itemnumber} ); $item->{itemlostloop}= GetAuthorisedValues(GetAuthValCode('items.itemlost',$fw),$item->{itemlost}) if GetAuthValCode('items.itemlost',$fw); $item->{itemdamagedloop}= GetAuthorisedValues(GetAuthValCode('items.damaged',$fw),$item->{damaged}) if GetAuthValCode('items.damaged',$fw); $item->{'collection'} = $ccodes->{ $item->{ccode} } if ($ccodes); --- a/installer/data/mysql/atomicupdate/bug_14945.sql +++ a/installer/data/mysql/atomicupdate/bug_14945.sql @@ -0,0 +1,13 @@ +INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ('StoreLastBorrower','0','','If ON, the last borrower to return an item will be stored in items.last_returned_by','YesNo'); + +CREATE TABLE IF NOT EXISTS `items_last_borrower` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `itemnumber` int(11) NOT NULL, + `borrowernumber` int(11) NOT NULL, + `created_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `itemnumber` (`itemnumber`), + KEY `borrowernumber` (`borrowernumber`), + CONSTRAINT `items_last_borrower_ibfk_2` FOREIGN KEY (`borrowernumber`) REFERENCES `borrowers` (`borrowernumber`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `items_last_borrower_ibfk_1` FOREIGN KEY (`itemnumber`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; --- a/installer/data/mysql/kohastructure.sql +++ a/installer/data/mysql/kohastructure.sql @@ -1262,6 +1262,22 @@ CREATE TABLE `items` ( -- holdings/item information ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; -- +-- Table structure for table `items_last_borrower` +-- + +CREATE TABLE IF NOT EXISTS `items_last_borrower` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `itemnumber` int(11) NOT NULL, + `borrowernumber` int(11) NOT NULL, + `created_on` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `itemnumber` (`itemnumber`), + KEY `borrowernumber` (`borrowernumber`), + CONSTRAINT `items_last_borrower_ibfk_2` FOREIGN KEY (`borrowernumber`) REFERENCES `borrowers` (`borrowernumber`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `items_last_borrower_ibfk_1` FOREIGN KEY (`itemnumber`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +-- -- Table structure for table `itemtypes` -- --- a/installer/data/mysql/sysprefs.sql +++ a/installer/data/mysql/sysprefs.sql @@ -424,6 +424,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('StaffSerialIssueDisplayCount','3','','Number of serial issues to display per subscription in the Staff client','Integer'), ('StaticHoldsQueueWeight','0',NULL,'Specify a list of library location codes separated by commas -- the list of codes will be traversed and weighted with first values given higher weight for holds fulfillment -- alternatively, if RandomizeHoldsQueueWeight is set, the list will be randomly selective','Integer'), ('StatisticsFields','location|itype|ccode', NULL, 'Define Fields (from the items table) used for statistics members','Free'), +('StoreLastBorrower','0','','If ON, the last borrower to return an item will be stored in items.last_returned_by','YesNo'), ('SubfieldsToAllowForRestrictedBatchmod','','Define a list of subfields for which edition is authorized when items_batchmod_restricted permission is enabled, separated by spaces. Example: 995\$f 995\$h 995\$j',NULL,'Free'), ('SubfieldsToAllowForRestrictedEditing','','Define a list of subfields for which edition is authorized when edit_items_restricted permission is enabled, separated by spaces. Example: 995\$f 995\$h 995\$j',NULL,'Free'), ('SubfieldsToUseWhenPrefill','','','Define a list of subfields to use when prefilling items (separated by space)','Free'), --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref @@ -563,6 +563,13 @@ OPAC: Privacy: - + - pref: StoreLastBorrower + default: 0 + choices: + yes: Store + no: "Don't store" + - the last patron to return an item. This setting is independent of opacreadinghistory/AnonymousPatron. + - - pref: AnonSuggestions choices: yes: Allow @@ -616,7 +623,6 @@ OPAC: - pref: RestrictedPageTitle class: long - "as title of your restricted page (appears in the breadcrumb and on the top of the restricted page)" - Shelf Browser: - - pref: OPACShelfBrowser --- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt @@ -1,3 +1,4 @@ +[% USE Koha %] [% INCLUDE 'doc-head-open.inc' %] Koha › Catalog › Item details for [% title %] [% FOREACH subtitl IN subtitle %] [% subtitl.subfield %][% END %] [% INCLUDE 'doc-head-close.inc' %] @@ -216,6 +217,9 @@
  • Last seen:[% IF ( ITEM_DAT.datelastseen ) %][% ITEM_DAT.datelastseen | $KohaDates %] [%END %] 
  • Last borrowed:[% IF (ITEM_DAT.datelastborrowed ) %][% ITEM_DAT.datelastborrowed | $KohaDates %][% END %] 
  • + [% IF Koha.Preference('StoreLastBorrower') && ITEM_DAT.object.last_returned_by %] +
  • Last returned by: [% ITEM_DAT.object.last_returned_by.cardnumber %] 
  • + [% END %] [% IF ( ITEM_DAT.card0 ) %]
  • Last borrower: [% ITEM_DAT.card0 %] 
  • [% END %] [% IF ( ITEM_DAT.card1 ) %]
  • Previous borrower: [% ITEM_DAT.card1 %] 
  • [% END %] [% IF ( ITEM_DAT.card2 ) %]
  • Previous borrower: [% ITEM_DAT.card2 %] 
  • [% END %] --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-privacy.tt +++ a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-privacy.tt @@ -77,6 +77,7 @@

    Whatever your privacy rule you choose, you can delete all your reading history immediately by clicking here. BE CAREFUL. Once you've confirmed the deletion, no one can retrieve the list!

    + [% IF Koha.Preference('StoreLastBorrower') %]

    Please note, the last person to return an item is tracked for the management of items returned damaged.

    [% END %] [% END # / IF Ask_data %] --- a/t/db_dependent/Circulation/AnonymiseIssueHistory.t +++ a/t/db_dependent/Circulation/AnonymiseIssueHistory.t @@ -18,11 +18,13 @@ use Modern::Perl; -use Test::More tests => 3; +use Test::More tests => 4; use C4::Context; use C4::Circulation; + use Koha::Database; +use Koha::Items; use t::lib::Mocks; use t::lib::TestBuilder; @@ -161,6 +163,86 @@ subtest 'AnonymousPatron is not defined' => sub { is( $borrowernumber_used_to_anonymised, undef, 'With AnonymousPatron is not defined, the issue should have been anonymised anyway' ); }; +subtest 'Test StoreLastBorrower' => sub { + plan tests => 4; + + t::lib::Mocks::mock_preference( 'StoreLastBorrower', '1' ); + + my $patron = $builder->build( + { + source => 'Borrower', + value => { privacy => 1, } + } + ); + + my $item = $builder->build( + { + source => 'Item', + value => { + itemlost => 0, + withdrawn => 0, + }, + } + ); + + my $issue = $builder->build( + { + source => 'Issue', + value => { + borrowernumber => $patron->{borrowernumber}, + itemnumber => $item->{itemnumber}, + }, + } + ); + + my $item_object = Koha::Items->find( $item->{itemnumber} ); + my $patron_object = $item_object->last_returned_by(); + is( $patron_object, undef, 'Koha::Item::last_returned_by returned undef' ); + + my ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, undef, '2010-10-10' ); + + $item_object = Koha::Items->find( $item->{itemnumber} ); + $patron_object = $item_object->last_returned_by(); + is( ref($patron_object), 'Koha::Borrower', 'Koha::Item::last_returned_by returned Koha::Borrower' ); + + $patron = $builder->build( + { + source => 'Borrower', + value => { privacy => 1, } + } + ); + + $issue = $builder->build( + { + source => 'Issue', + value => { + borrowernumber => $patron->{borrowernumber}, + itemnumber => $item->{itemnumber}, + }, + } + ); + + ( $returned, undef, undef ) = C4::Circulation::AddReturn( $item->{barcode}, undef, undef, undef, '2010-10-10' ); + + $item_object = Koha::Items->find( $item->{itemnumber} ); + $patron_object = $item_object->last_returned_by(); + is( $patron_object->id, $patron->{borrowernumber}, 'Second patron to return item replaces the first' ); + + $patron = $builder->build( + { + source => 'Borrower', + value => { privacy => 1, } + } + ); + $patron_object = Koha::Borrowers->find( $patron->{borrowernumber} ); + + $item_object->last_returned_by($patron_object); + $item_object = Koha::Items->find( $item->{itemnumber} ); + my $patron_object2 = $item_object->last_returned_by(); + is( $patron_object->id, $patron_object2->id, + 'Calling last_returned_by with Borrower object sets last_returned_by to that borrower' ); +}; + $schema->storage->txn_rollback; 1; --