From 55ba29ab62c6b6ef6e69adfee83a5d7205b9f038 Mon Sep 17 00:00:00 2001 From: Thibaud Guillot Date: Thu, 25 Jan 2024 14:14:07 +0100 Subject: [PATCH] Bug 35906: Add bookable option on itemtype MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Actually new "booking" feature can be set on an item but no directly on an itemtype. This patch adds this possibility. Test plan: 1) Test this new feature on an item as it were currently working. 2) Apply this patch 3) Run updatedatabase.pl 4) Reload Schema by running update_dbix_class_files.pl 5) Edit an itemtype, there is a new checkbox to add 'bookable' option 6) Test it with item with this itemtype, if there is 1 item at least you will see 'Booking' tab. 7) You now have a new setting on the item bookable option to 'Follow itemtype" Sponsored by: Association de Gestion des Ĺ’uvres Sociales d'Inria (AGOS) --- Koha/Item.pm | 12 +++++++ Koha/ItemType.pm | 1 + Koha/Items.pm | 12 +++++-- Koha/REST/V1/Biblios.pm | 3 ++ admin/itemtypes.pl | 3 ++ api/v1/swagger/definitions/item.yaml | 14 ++++++-- api/v1/swagger/paths/biblios.yaml | 1 + api/v1/swagger/paths/items.yaml | 4 ++- catalogue/updateitem.pl | 3 +- .../Bug_35906_add-column-bookable-itemtype.pl | 35 +++++++++++++++++++ installer/data/mysql/kohastructure.sql | 5 +-- .../prog/en/modules/admin/itemtypes.tt | 9 +++++ .../prog/en/modules/catalogue/moredetail.tt | 19 +++++++--- t/db_dependent/Koha/Items.t | 27 ++++++++++---- t/lib/TestBuilder.pm | 1 + 15 files changed, 128 insertions(+), 21 deletions(-) create mode 100755 installer/data/mysql/atomicupdate/Bug_35906_add-column-bookable-itemtype.pl diff --git a/Koha/Item.pm b/Koha/Item.pm index 91c15b22d55..03e2d0794ad 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -1830,6 +1830,18 @@ that this complicates filtering, but some query trickery could do it in the cont sub item_type { return shift->itemtype; +=head3 effective_bookable + + my $bookable = $item->effective_bookable; + +Returns the effective bookability of the current item, be that item or itemtype level + +=cut + +sub effective_bookable { + my ($self) = @_; + + return defined( $self->bookable ) ? $self->bookable : $self->itemtype->bookable; } =head3 orders diff --git a/Koha/ItemType.pm b/Koha/ItemType.pm index fb88518c257..f4c0c4aee98 100644 --- a/Koha/ItemType.pm +++ b/Koha/ItemType.pm @@ -234,6 +234,7 @@ sub to_api_mapping { rentalcharge_daily_calendar => 'daily_rental_charge_calendar', rentalcharge_hourly => 'hourly_rental_charge', rentalcharge_hourly_calendar => 'hourly_rental_charge_calendar', + bookable_itemtype => 'bookable_itemtype', }; } diff --git a/Koha/Items.pm b/Koha/Items.pm index 7c594e6798e..40cc5809a65 100644 --- a/Koha/Items.pm +++ b/Koha/Items.pm @@ -169,9 +169,17 @@ Returns a new resultset, containing only those items that are allowed to be book sub filter_by_bookable { my ($self) = @_; - my $params = { bookable => 1 }; + my @bookable_itemtypes = Koha::ItemTypes->search( { bookable => 1 } )->get_column('itemtype'); - return $self->search($params); + my ( $where, $attr ); + if ( C4::Context->preference("item-level_itypes") ) { + $where = [ { bookable => 1 }, { bookable => undef, itype => { -in => \@bookable_itemtypes } } ]; + } else { + $where = [ { bookable => 1 }, { bookable => undef, 'biblioitem.itemtype' => { -in => \@bookable_itemtypes } } ]; + $attr = { join => 'biblioitem' }; + } + + return $self->search( $where, $attr ); } =head3 move_to_biblio diff --git a/Koha/REST/V1/Biblios.pm b/Koha/REST/V1/Biblios.pm index 1c15e8ec25b..ae4d5d1c26c 100644 --- a/Koha/REST/V1/Biblios.pm +++ b/Koha/REST/V1/Biblios.pm @@ -266,6 +266,9 @@ sub get_items { my $biblio = Koha::Biblios->find( { biblionumber => $c->param('biblio_id') }, { prefetch => ['items'] } ); my $bookable_only = $c->param('bookable'); + # Deletion of parameter to avoid filtering on the items table in the case of bookings on 'itemtype' + $c->req->params->remove('bookable'); + return $c->render_resource_not_found("Bibliographic record") unless $biblio; diff --git a/admin/itemtypes.pl b/admin/itemtypes.pl index 684f9377dbc..b5dbc7c98c6 100755 --- a/admin/itemtypes.pl +++ b/admin/itemtypes.pl @@ -98,6 +98,7 @@ if ( $op eq 'add_form' ) { my $rentalcharge_daily_calendar = $input->param('rentalcharge_daily_calendar') // 0; my $rentalcharge_hourly_calendar = $input->param('rentalcharge_hourly_calendar') // 0; my $automatic_checkin = $input->param('automatic_checkin') // 0; + my $bookable = $input->param('bookable')// 0; if ( $itemtype and $is_a_modif ) { # it's a modification $itemtype->description($description); @@ -118,6 +119,7 @@ if ( $op eq 'add_form' ) { $itemtype->rentalcharge_daily_calendar($rentalcharge_daily_calendar); $itemtype->rentalcharge_hourly_calendar($rentalcharge_hourly_calendar); $itemtype->automatic_checkin($automatic_checkin); + $itemtype->bookable($bookable); eval { $itemtype->store; @@ -151,6 +153,7 @@ if ( $op eq 'add_form' ) { rentalcharge_daily_calendar => $rentalcharge_daily_calendar, rentalcharge_hourly_calendar => $rentalcharge_hourly_calendar, automatic_checkin => $automatic_checkin, + bookable => $bookable, } ); eval { diff --git a/api/v1/swagger/definitions/item.yaml b/api/v1/swagger/definitions/item.yaml index ba1a8a890e1..f44b3d5ae3c 100644 --- a/api/v1/swagger/definitions/item.yaml +++ b/api/v1/swagger/definitions/item.yaml @@ -24,6 +24,11 @@ properties: - "null" description: Information about the acquisition source (it is not really a vendor id) bookable: + type: + - boolean + - "null" + description: Item level bookability override. + effective_bookable: type: boolean description: Allow bookings on this item. home_library_id: @@ -90,7 +95,8 @@ properties: - string - "null" format: date-time - description: The date and time an item was last marked as withdrawn, NULL if not + description: + The date and time an item was last marked as withdrawn, NULL if not withdrawn callnumber: type: @@ -155,14 +161,16 @@ properties: type: - string - "null" - description: Linked to the CART and PROC temporary locations feature, stores the + description: + Linked to the CART and PROC temporary locations feature, stores the permanent shelving location checked_out_date: type: - string - "null" format: date - description: Defines if item is checked out (NULL for not checked out, and checkout + description: + Defines if item is checked out (NULL for not checked out, and checkout date for checked out) call_number_source: type: diff --git a/api/v1/swagger/paths/biblios.yaml b/api/v1/swagger/paths/biblios.yaml index 2da90e27d62..09ef006d749 100644 --- a/api/v1/swagger/paths/biblios.yaml +++ b/api/v1/swagger/paths/biblios.yaml @@ -450,6 +450,7 @@ - cover_image_ids - item_group_item.item_group.description - serial_item.serial + - effective_bookable collectionFormat: csv - $ref: "../swagger.yaml#/parameters/match" - $ref: "../swagger.yaml#/parameters/order_by" diff --git a/api/v1/swagger/paths/items.yaml b/api/v1/swagger/paths/items.yaml index 3f4677a9a35..12edb8eb3af 100644 --- a/api/v1/swagger/paths/items.yaml +++ b/api/v1/swagger/paths/items.yaml @@ -22,6 +22,7 @@ enum: - +strings - biblio + - effective_bookable collectionFormat: csv - $ref: "../swagger.yaml#/parameters/match" - $ref: "../swagger.yaml#/parameters/order_by" @@ -81,6 +82,7 @@ type: string enum: - +strings + - effective_bookable collectionFormat: csv consumes: - application/json @@ -150,7 +152,7 @@ * linked_analytics: The item has linked analytic records * not_same_branch: The item is blocked by independent branches schema: - $ref: "../swagger.yaml#/definitions/error" + $ref: "../swagger.yaml#/definitions/error" "500": description: | Internal server error. Possible `error_code` attribute values: diff --git a/catalogue/updateitem.pl b/catalogue/updateitem.pl index 5b092763dfa..087dd98f8aa 100755 --- a/catalogue/updateitem.pl +++ b/catalogue/updateitem.pl @@ -39,7 +39,7 @@ my $itemnotes_nonpublic=$cgi->param('itemnotes_nonpublic'); my $withdrawn=$cgi->param('withdrawn'); my $damaged=$cgi->param('damaged'); my $exclude_from_local_holds_priority = $cgi->param('exclude_from_local_holds_priority'); -my $bookable = $cgi->param('bookable'); +my $bookable = $cgi->param('bookable') // q{}; my $confirm=$cgi->param('confirm'); my $dbh = C4::Context->dbh; @@ -77,6 +77,7 @@ elsif ( $op eq "cud-set_public_note" ) { # i.e., itemnotes parameter passed from $item->exclude_from_local_holds_priority($exclude_from_local_holds_priority); $messages = "updated_exclude_from_local_holds_priority=$exclude_from_local_holds_priority&"; } elsif ( $op eq "cud-set_bookable" && $bookable ne $item_data_hashref->{'bookable'} ) { + undef($bookable) if $bookable eq ""; $item->bookable($bookable); } elsif ( $op eq "cud-set_damaged" && $damaged ne $item_data_hashref->{'damaged'}) { $item->damaged($damaged); diff --git a/installer/data/mysql/atomicupdate/Bug_35906_add-column-bookable-itemtype.pl b/installer/data/mysql/atomicupdate/Bug_35906_add-column-bookable-itemtype.pl new file mode 100755 index 00000000000..a289e189ae7 --- /dev/null +++ b/installer/data/mysql/atomicupdate/Bug_35906_add-column-bookable-itemtype.pl @@ -0,0 +1,35 @@ +use Modern::Perl; + +return { + bug_number => "35906", + description => "Add bookable column on itemtypes table", + up => sub { + my ($args) = @_; + my ( $dbh, $out ) = @$args{qw(dbh out)}; + + $dbh->do( + q{ + ALTER TABLE itemtypes ADD IF NOT EXISTS bookable INT(1) DEFAULT 0 + } + ); + + say $out "Added column 'itemtypes.bookable'"; + + $dbh->do( + q{ + ALTER TABLE items MODIFY COLUMN bookable tinyint(1) DEFAULT NULL COMMENT 'nullable boolean value defining whether this this item is available for bookings or not' + } + ); + + say $out "Updated column 'items.bookable' allow nullable"; + + $dbh->do( + q{ + ALTER TABLE deleteditems MODIFY COLUMN bookable tinyint(1) DEFAULT NULL COMMENT 'nullable boolean value defining whether this this item is available for bookings or not' + } + ); + + say $out "Updated column 'deleteditems.bookable' allow nullable"; + + }, +}; diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 14cef909b3a..6472990f531 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -2750,7 +2750,7 @@ CREATE TABLE `deleteditems` ( `biblionumber` int(11) NOT NULL DEFAULT 0 COMMENT 'foreign key from biblio table used to link this item to the right bib record', `biblioitemnumber` int(11) NOT NULL DEFAULT 0 COMMENT 'foreign key from the biblioitems table to link to item to additional information', `barcode` varchar(20) DEFAULT NULL COMMENT 'item barcode (MARC21 952$p)', - `bookable` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'boolean value defining whether this this item is available for bookings or not', + `bookable` tinyint(1) DEFAULT NULL COMMENT 'nullable boolean value defining whether this this item is available for bookings or not', `dateaccessioned` date DEFAULT NULL COMMENT 'date the item was acquired or added to Koha (MARC21 952$d)', `booksellerid` longtext DEFAULT NULL COMMENT 'where the item was purchased (MARC21 952$e)', `homebranch` varchar(10) DEFAULT NULL COMMENT 'foreign key from the branches table for the library that owns this item (MARC21 952$a)', @@ -4008,7 +4008,7 @@ CREATE TABLE `items` ( `biblionumber` int(11) NOT NULL DEFAULT 0 COMMENT 'foreign key from biblio table used to link this item to the right bib record', `biblioitemnumber` int(11) NOT NULL DEFAULT 0 COMMENT 'foreign key from the biblioitems table to link to item to additional information', `barcode` varchar(20) DEFAULT NULL COMMENT 'item barcode (MARC21 952$p)', - `bookable` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'boolean value defining whether this this item is available for bookings or not', + `bookable` tinyint(1) DEFAULT NULL COMMENT 'nullable boolean value defining whether this this item is available for bookings or not', `dateaccessioned` date DEFAULT NULL COMMENT 'date the item was acquired or added to Koha (MARC21 952$d)', `booksellerid` longtext DEFAULT NULL COMMENT 'where the item was purchased (MARC21 952$e)', `homebranch` varchar(10) DEFAULT NULL COMMENT 'foreign key from the branches table for the library that owns this item (MARC21 952$a)', @@ -4137,6 +4137,7 @@ CREATE TABLE `itemtypes` ( `hideinopac` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Hide the item type from the search options in OPAC', `searchcategory` varchar(80) DEFAULT NULL COMMENT 'Group this item type with others with the same value on OPAC search options', `automatic_checkin` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'If automatic checkin is enabled for items of this type', + `bookable` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Activate bookable feature for items related to this item type', PRIMARY KEY (`itemtype`), UNIQUE KEY `itemtype` (`itemtype`), KEY `itemtypes_ibfk_1` (`parent_type`), diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt index b9ea060e304..707237449ac 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/itemtypes.tt @@ -258,6 +258,15 @@ [% END %] If checked, items will be automatically checked in once they've reached their due date. This feature requires the misc/cronjobs/automatic_checkin.pl cronjob. Ask your system administrator to schedule it. +
  • + + [% IF itemtype.bookable %] + + [% ELSE %] + + [% END %] + If checked, all items of this type will be enabled for future bookings unless overridden at the item level. +
  • diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt index 6ef020cecbd..91e18e13dac 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/moredetail.tt @@ -350,7 +350,9 @@
  • - Bookable: + + Bookable: + [% IF ( CAN_user_circulate ) %]
    [% INCLUDE 'csrf-token.inc' %] @@ -358,20 +360,27 @@
    [% ELSE %] - [% IF ITEM_DAT.bookable == 1 %] Yes [% ELSE %] No [% END %] + [% IF ITEM_DAT.bookable == 1 %] Yes [% ELSIF ITEM_DAT.bookable == 0 %] No [% ELSE %] Follow item type [% END %] [% END %] + Item type bookable: [% IF ITEM_DAT.effective_itemtype.bookable == 1 %]Yes[% ELSE %]No[% END %]
  • diff --git a/t/db_dependent/Koha/Items.t b/t/db_dependent/Koha/Items.t index 3a0ffd11cc0..9cbf32e1e6f 100755 --- a/t/db_dependent/Koha/Items.t +++ b/t/db_dependent/Koha/Items.t @@ -2078,24 +2078,37 @@ subtest 'filter_by_for_hold' => sub { }; subtest 'filter_by_bookable' => sub { - plan tests => 2; + plan tests => 4; $schema->storage->txn_begin; - my $biblio = $builder->build_sample_biblio; + t::lib::Mocks::mock_preference('item-level_itypes', 0); + + my $bookable_item_type = $builder->build_object( { class => 'Koha::ItemTypes', value => { bookable => 1 } } ); + my $non_bookable_item_type = $builder->build_object( { class => 'Koha::ItemTypes', value => { bookable => 0 } } ); + my $biblio = $builder->build_sample_biblio({ itemtype => $bookable_item_type->itemtype }); # bookable items - my $bookable_item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 1 } ); + my $bookable_item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $bookable_item_type->itemtype, bookable => 1 } ); # not bookable items - my $non_bookable_item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 0 } ); - my $non_bookable_item2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, bookable => 0 } ); + my $non_bookable_item1 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $non_bookable_item_type->itemtype, bookable => 0 } ); + my $non_bookable_item2 = $builder->build_sample_item( { biblionumber => $biblio->biblionumber, itype => $non_bookable_item_type->itemtype, bookable => 0 } ); - is( $biblio->items->filter_by_bookable->count, 1, "filter_by_bookable returns the correct number of items" ); + is( $biblio->items->filter_by_bookable->count, 1, "filter_by_bookable returns the correct number of items set at item level" ); is( $biblio->items->filter_by_bookable->next->itemnumber, $bookable_item1->itemnumber, - "the correct item is returned from filter_by_bookable" + "the correct item is returned from filter_by_bookable at the item level" ); + $bookable_item1->bookable(undef)->store(); + $non_bookable_item1->bookable(undef)->store(); + $non_bookable_item2->bookable(undef)->store(); + $biblio->get_from_storage; + is( $biblio->items->filter_by_bookable->count, 3, "filter_by_bookable returns the correct number of items when not set at item level and using biblio level itemtypes" ); + + t::lib::Mocks::mock_preference('item-level_itypes', 1); + is( $biblio->items->filter_by_bookable->count, 1, "filter_by_bookable returns the correct number of items when not set at item level and using item level itemtypes" ); + $schema->storage->txn_rollback; }; diff --git a/t/lib/TestBuilder.pm b/t/lib/TestBuilder.pm index 57506b11d78..6a4587fe824 100644 --- a/t/lib/TestBuilder.pm +++ b/t/lib/TestBuilder.pm @@ -638,6 +638,7 @@ sub _gen_default_values { defaultreplacecost => 0, processfee => 0, notforloan => 0, + bookable => 0, }, Aqbookseller => { tax_rate => 0, -- 2.30.2