Bug 28854: Expose functionality to attach items to bundles
This patch adds methods the the Koha::Item object for managing item
bundling operations and then exposes those methods via the REST API.
We include the new `BundleNotLoanValue` preference for setting not
for loan values when an item is added to a bundle.
Finally, we expose bundle management via the catalogue details page.
Test plan:
0) Apply patches up to this point and run the database update
1) Configuration: `BundleNotLoanValue` should have been set by the
database update and point to a newly added AV value.
2) Creating a new bundle
* Add a new bib record
* Mark the bib record as a 'collection' type by setting leader
position 7 to 'c'
* Add a new item to this bib record
* You should see a new 'Manage bundle' button available in the
'Actions' column of the Holdings table.
* Clicking 'Manage bundle' should expand the table to include a new
row directly beneath this one.
* Use the new 'Add to bundle' button that appears in this row to
trigger a modal that allows entering the barcode of items you wish
to add to the bundle
* Upon closing the modal, the bundle content table should reload and
contain your newly associated items.
* You can subsequently remove an item from a bundle using the new
'Remove' button.
3) Not for loan
* Items that have been added into a bundle should now appear as 'Not
for loan' from their original biblio record and note which bundle
they belong to.
4) Error cases
* Try adding an item that already belongs to a bundle to another
bundle: Note an error is displayed in the modal form.
5) The bundles feature can be disabled by unsetting the
`BundleNotLoanValue` system preference.
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:
parent
91518ae375
commit
5f614c05fd
158
Koha/Item.pm
158
Koha/Item.pm
|
|
@ -20,6 +20,7 @@ package Koha::Item;
|
|||
use Modern::Perl;
|
||||
|
||||
use List::MoreUtils qw( any );
|
||||
use Try::Tiny qw( catch try );
|
||||
|
||||
use Koha::Database;
|
||||
use Koha::DateUtils qw( dt_from_string output_pref );
|
||||
|
|
@ -1465,6 +1466,163 @@ sub move_to_biblio {
|
|||
return $to_biblionumber;
|
||||
}
|
||||
|
||||
=head3 bundle_items
|
||||
|
||||
my $bundle_items = $item->bundle_items;
|
||||
|
||||
Returns the items associated with this bundle
|
||||
|
||||
=cut
|
||||
|
||||
sub bundle_items {
|
||||
my ($self) = @_;
|
||||
|
||||
if ( !$self->{_bundle_items_cached} ) {
|
||||
my $bundle_items = Koha::Items->search(
|
||||
{ 'item_bundles_item.host' => $self->itemnumber },
|
||||
{ join => 'item_bundles_item' } );
|
||||
$self->{_bundle_items} = $bundle_items;
|
||||
$self->{_bundle_items_cached} = 1;
|
||||
}
|
||||
|
||||
return $self->{_bundle_items};
|
||||
}
|
||||
|
||||
=head3 is_bundle
|
||||
|
||||
my $is_bundle = $item->is_bundle;
|
||||
|
||||
Returns whether the item is a bundle or not
|
||||
|
||||
=cut
|
||||
|
||||
sub is_bundle {
|
||||
my ($self) = @_;
|
||||
return $self->bundle_items->count ? 1 : 0;
|
||||
}
|
||||
|
||||
=head3 bundle_host
|
||||
|
||||
my $bundle = $item->bundle_host;
|
||||
|
||||
Returns the bundle item this item is attached to
|
||||
|
||||
=cut
|
||||
|
||||
sub bundle_host {
|
||||
my ($self) = @_;
|
||||
|
||||
my $bundle_items_rs = $self->_result->item_bundles_item;
|
||||
return unless $bundle_items_rs;
|
||||
return Koha::Item->_new_from_dbic($bundle_items_rs->host);
|
||||
}
|
||||
|
||||
=head3 in_bundle
|
||||
|
||||
my $in_bundle = $item->in_bundle;
|
||||
|
||||
Returns whether this item is currently in a bundle
|
||||
|
||||
=cut
|
||||
|
||||
sub in_bundle {
|
||||
my ($self) = @_;
|
||||
return $self->bundle_host ? 1 : 0;
|
||||
}
|
||||
|
||||
=head3 add_to_bundle
|
||||
|
||||
my $link = $item->add_to_bundle($bundle_item);
|
||||
|
||||
Adds the bundle_item passed to this item
|
||||
|
||||
=cut
|
||||
|
||||
sub add_to_bundle {
|
||||
my ( $self, $bundle_item ) = @_;
|
||||
|
||||
my $schema = Koha::Database->new->schema;
|
||||
|
||||
my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
|
||||
|
||||
try {
|
||||
$schema->txn_do(
|
||||
sub {
|
||||
$self->_result->add_to_item_bundles_hosts(
|
||||
{ item => $bundle_item->itemnumber } );
|
||||
|
||||
$bundle_item->notforloan($BundleNotLoanValue)->store();
|
||||
}
|
||||
);
|
||||
}
|
||||
catch {
|
||||
|
||||
# FIXME: See if we can move the below copy/paste from Koha::Object::store into it's own class and catch at a lower level in the Schema instantiation.. take inspiration fro DBIx::Error
|
||||
if ( ref($_) eq 'DBIx::Class::Exception' ) {
|
||||
warn $_->{msg};
|
||||
if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
|
||||
# FK constraints
|
||||
# FIXME: MySQL error, if we support more DB engines we should implement this for each
|
||||
if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
|
||||
Koha::Exceptions::Object::FKConstraint->throw(
|
||||
error => 'Broken FK constraint',
|
||||
broken_fk => $+{column}
|
||||
);
|
||||
}
|
||||
}
|
||||
elsif (
|
||||
$_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
|
||||
{
|
||||
Koha::Exceptions::Object::DuplicateID->throw(
|
||||
error => 'Duplicate ID',
|
||||
duplicate_id => $+{key}
|
||||
);
|
||||
}
|
||||
elsif ( $_->{msg} =~
|
||||
/Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
|
||||
)
|
||||
{ # The optional \W in the regex might be a quote or backtick
|
||||
my $type = $+{type};
|
||||
my $value = $+{value};
|
||||
my $property = $+{property};
|
||||
$property =~ s/['`]//g;
|
||||
Koha::Exceptions::Object::BadValue->throw(
|
||||
type => $type,
|
||||
value => $value,
|
||||
property => $property =~ /(\w+\.\w+)$/
|
||||
? $1
|
||||
: $property
|
||||
, # results in table.column without quotes or backtics
|
||||
);
|
||||
}
|
||||
|
||||
# Catch-all for foreign key breakages. It will help find other use cases
|
||||
$_->rethrow();
|
||||
}
|
||||
else {
|
||||
$_;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
=head3 remove_from_bundle
|
||||
|
||||
Remove this item from any bundle it may have been attached to.
|
||||
|
||||
=cut
|
||||
|
||||
sub remove_from_bundle {
|
||||
my ($self) = @_;
|
||||
|
||||
my $bundle_item_rs = $self->_result->item_bundles_item;
|
||||
if ( $bundle_item_rs ) {
|
||||
$bundle_item_rs->delete;
|
||||
$self->notforloan(0)->store();
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
=head2 Internal methods
|
||||
|
||||
=head3 _after_item_action_hooks
|
||||
|
|
|
|||
|
|
@ -148,4 +148,125 @@ sub pickup_locations {
|
|||
};
|
||||
}
|
||||
|
||||
=head3 bundled_items
|
||||
|
||||
Controller function that handles bundled_items Koha::Item objects
|
||||
|
||||
=cut
|
||||
|
||||
sub bundled_items {
|
||||
my $c = shift->openapi->valid_input or return;
|
||||
|
||||
my $item_id = $c->validation->param('item_id');
|
||||
my $item = Koha::Items->find( $item_id );
|
||||
|
||||
unless ($item) {
|
||||
return $c->render(
|
||||
status => 404,
|
||||
openapi => { error => "Item not found" }
|
||||
);
|
||||
}
|
||||
|
||||
return try {
|
||||
my $items_set = $item->bundle_items;
|
||||
my $items = $c->objects->search( $items_set );
|
||||
return $c->render(
|
||||
status => 200,
|
||||
openapi => $items
|
||||
);
|
||||
}
|
||||
catch {
|
||||
$c->unhandled_exception($_);
|
||||
};
|
||||
}
|
||||
|
||||
=head3 add_to_bundle
|
||||
|
||||
Controller function that handles adding items to this bundle
|
||||
|
||||
=cut
|
||||
|
||||
sub add_to_bundle {
|
||||
my $c = shift->openapi->valid_input or return;
|
||||
|
||||
my $item_id = $c->validation->param('item_id');
|
||||
my $item = Koha::Items->find( $item_id );
|
||||
|
||||
unless ($item) {
|
||||
return $c->render(
|
||||
status => 404,
|
||||
openapi => { error => "Item not found" }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
my $bundle_item_id = $c->validation->param('body')->{'external_id'};
|
||||
my $bundle_item = Koha::Items->find( { barcode => $bundle_item_id } );
|
||||
|
||||
unless ($bundle_item) {
|
||||
return $c->render(
|
||||
status => 404,
|
||||
openapi => { error => "Bundle item not found" }
|
||||
);
|
||||
}
|
||||
|
||||
return try {
|
||||
my $link = $item->add_to_bundle($bundle_item);
|
||||
return $c->render(
|
||||
status => 201,
|
||||
openapi => $bundle_item
|
||||
);
|
||||
}
|
||||
catch {
|
||||
if ( ref($_) eq 'Koha::Exceptions::Object::DuplicateID' ) {
|
||||
return $c->render(
|
||||
status => 409,
|
||||
openapi => {
|
||||
error => 'Item is already bundled',
|
||||
key => $_->duplicate_id
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
$c->unhandled_exception($_);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
=head3 remove_from_bundle
|
||||
|
||||
Controller function that handles removing items from this bundle
|
||||
|
||||
=cut
|
||||
|
||||
sub remove_from_bundle {
|
||||
my $c = shift->openapi->valid_input or return;
|
||||
|
||||
my $item_id = $c->validation->param('item_id');
|
||||
my $item = Koha::Items->find( $item_id );
|
||||
|
||||
unless ($item) {
|
||||
return $c->render(
|
||||
status => 404,
|
||||
openapi => { error => "Item not found" }
|
||||
);
|
||||
}
|
||||
|
||||
my $bundle_item_id = $c->validation->param('bundled_item_id');
|
||||
my $bundle_item = Koha::Items->find( { itemnumber => $bundle_item_id } );
|
||||
|
||||
unless ($bundle_item) {
|
||||
return $c->render(
|
||||
status => 404,
|
||||
openapi => { error => "Bundle item not found" }
|
||||
);
|
||||
}
|
||||
|
||||
$bundle_item->remove_from_bundle;
|
||||
return $c->render(
|
||||
status => 204,
|
||||
openapi => q{}
|
||||
);
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
|||
|
|
@ -525,6 +525,28 @@ modules:
|
|||
-
|
||||
columnname: checkin_on
|
||||
|
||||
bundle_tables:
|
||||
columns:
|
||||
-
|
||||
columnname: title
|
||||
cannot_be_toggled: 1
|
||||
-
|
||||
columnname: author
|
||||
-
|
||||
columnname: collection_code
|
||||
-
|
||||
columnname: item_type
|
||||
-
|
||||
columnname: callnumber
|
||||
-
|
||||
columnname: external_id
|
||||
-
|
||||
columnname: status
|
||||
-
|
||||
columnname: bundle_actions
|
||||
cannot_be_toggled: 1
|
||||
cannot_be_modified: 1
|
||||
|
||||
cataloguing:
|
||||
addbooks:
|
||||
reservoir-table:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
type: object
|
||||
properties:
|
||||
item_id:
|
||||
type:
|
||||
- integer
|
||||
- "null"
|
||||
description: Internal item identifier
|
||||
external_id:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
description: Item barcode
|
||||
additionalProperties: false
|
||||
|
|
@ -7,6 +7,7 @@ properties:
|
|||
biblio_id:
|
||||
type: integer
|
||||
description: Internal identifier for the parent bibliographic record
|
||||
biblio: {}
|
||||
external_id:
|
||||
type:
|
||||
- string
|
||||
|
|
|
|||
|
|
@ -93,6 +93,163 @@
|
|||
x-koha-authorization:
|
||||
permissions:
|
||||
catalogue: "1"
|
||||
"/items/{item_id}/bundled_items":
|
||||
post:
|
||||
x-mojo-to: Items#add_to_bundle
|
||||
operationId: addToBundle
|
||||
tags:
|
||||
- items
|
||||
summary: Add item to bundle
|
||||
parameters:
|
||||
- $ref: "../swagger.yaml#/parameters/item_id_pp"
|
||||
- name: body
|
||||
in: body
|
||||
description: A JSON object containing information about the new bundle link
|
||||
required: true
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/bundle_link"
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: A successfully created bundle link
|
||||
schema:
|
||||
items:
|
||||
$ref: "../swagger.yaml#/definitions/item"
|
||||
"400":
|
||||
description: Bad parameter
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"401":
|
||||
description: Authentication required
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"403":
|
||||
description: Access forbidden
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"409":
|
||||
description: Conflict in creating resource
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"503":
|
||||
description: Under maintenance
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
x-koha-authorization:
|
||||
permissions:
|
||||
catalogue: 1
|
||||
get:
|
||||
x-mojo-to: Items#bundled_items
|
||||
operationId: bundledItems
|
||||
tags:
|
||||
- items
|
||||
summary: List bundled items
|
||||
parameters:
|
||||
- $ref: "../swagger.yaml#/parameters/item_id_pp"
|
||||
- name: external_id
|
||||
in: query
|
||||
description: Search on the item's barcode
|
||||
required: false
|
||||
type: string
|
||||
- $ref: "../swagger.yaml#/parameters/match"
|
||||
- $ref: "../swagger.yaml#/parameters/order_by"
|
||||
- $ref: "../swagger.yaml#/parameters/page"
|
||||
- $ref: "../swagger.yaml#/parameters/per_page"
|
||||
- $ref: "../swagger.yaml#/parameters/q_param"
|
||||
- $ref: "../swagger.yaml#/parameters/q_body"
|
||||
- $ref: "../swagger.yaml#/parameters/q_header"
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: A list of item
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "../swagger.yaml#/definitions/item"
|
||||
"401":
|
||||
description: Authentication required
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"403":
|
||||
description: Access forbidden
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"503":
|
||||
description: Under maintenance
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
x-koha-authorization:
|
||||
permissions:
|
||||
catalogue: "1"
|
||||
x-koha-embed:
|
||||
- biblio
|
||||
- checkout
|
||||
"/items/{item_id}/bundled_items/{bundled_item_id}":
|
||||
delete:
|
||||
x-mojo-to: Items#remove_from_bundle
|
||||
operationId: removeFromBundle
|
||||
tags:
|
||||
- items
|
||||
summary: Remove item from bundle
|
||||
parameters:
|
||||
- $ref: "../swagger.yaml#/parameters/item_id_pp"
|
||||
- name: bundled_item_id
|
||||
in: path
|
||||
description: Internal identifier for the bundled item
|
||||
required: true
|
||||
type: string
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"204":
|
||||
description: Bundle link deleted
|
||||
"400":
|
||||
description: Bad parameter
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"401":
|
||||
description: Authentication required
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"403":
|
||||
description: Access forbidden
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"404":
|
||||
description: Resource not found
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
"503":
|
||||
description: Under maintenance
|
||||
schema:
|
||||
$ref: "../swagger.yaml#/definitions/error"
|
||||
x-koha-authorization:
|
||||
permissions:
|
||||
catalogue: 1
|
||||
"/items/{item_id}/pickup_locations":
|
||||
get:
|
||||
x-mojo-to: Items#pickup_locations
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ definitions:
|
|||
$ref: ./definitions/allows_renewal.yaml
|
||||
basket:
|
||||
$ref: ./definitions/basket.yaml
|
||||
bundle_link:
|
||||
$ref: ./definitions/bundle_link.yaml
|
||||
cashup:
|
||||
$ref: ./definitions/cashup.yaml
|
||||
checkout:
|
||||
|
|
@ -169,6 +171,10 @@ paths:
|
|||
$ref: ./paths/items.yaml#/~1items
|
||||
"/items/{item_id}":
|
||||
$ref: "./paths/items.yaml#/~1items~1{item_id}"
|
||||
"/items/{item_id}/bundled_items":
|
||||
$ref: ./paths/items.yaml#/~1items~1{item_id}~1bundled_items
|
||||
"/items/{item_id}/bundled_items/{bundled_item_id}":
|
||||
$ref: ./paths/items.yaml#/~1items~1{item_id}~1bundled_items~1{bundled_item_id}
|
||||
"/items/{item_id}/pickup_locations":
|
||||
$ref: "./paths/items.yaml#/~1items~1{item_id}~1pickup_locations"
|
||||
/libraries:
|
||||
|
|
|
|||
|
|
@ -206,6 +206,11 @@ if (@hostitems){
|
|||
|
||||
my $dat = &GetBiblioData($biblionumber);
|
||||
|
||||
#is biblio a collection and are bundles enabled
|
||||
my $leader = $record->leader();
|
||||
$dat->{bundlesEnabled} = ( ( substr( $leader, 7, 1 ) eq 'c' )
|
||||
&& C4::Context->preference('BundleNotLoanValue') ) ? 1 : 0;
|
||||
|
||||
#coping with subscriptions
|
||||
my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber);
|
||||
my @subscriptions = SearchSubscriptions({ biblionumber => $biblionumber, orderby => 'title' });
|
||||
|
|
@ -451,6 +456,15 @@ foreach my $item (@items) {
|
|||
}
|
||||
}
|
||||
|
||||
if ($item_object->is_bundle) {
|
||||
$itemfields{bundles} = 1;
|
||||
$item->{is_bundle} = 1;
|
||||
}
|
||||
|
||||
if ($item_object->in_bundle) {
|
||||
$item->{bundle_host} = $item_object->bundle_host;
|
||||
}
|
||||
|
||||
if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
|
||||
if ($itembranchcode and $itembranchcode eq $currentbranch) {
|
||||
push @itemloop, $item;
|
||||
|
|
|
|||
|
|
@ -2355,6 +2355,10 @@ td {
|
|||
display: block;
|
||||
}
|
||||
|
||||
.bundled {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.datedue {
|
||||
color: #999;
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -165,6 +165,10 @@ Cataloging:
|
|||
- and record's last modifier name in MARC subfield
|
||||
- pref: MarcFieldForModifierName
|
||||
- ". <br/><strong>NOTE:</strong> Use a dollar sign between field and subfield like 123$a."
|
||||
-
|
||||
- Use the NOT_LOAN authorised value
|
||||
- pref: BundleNotLoanValue
|
||||
- to represent 'added to bundle' when an item is attached to bundle.
|
||||
Display:
|
||||
-
|
||||
- 'Separate main entry and subdivisions with '
|
||||
|
|
|
|||
|
|
@ -368,12 +368,12 @@
|
|||
[% IF ( analyze ) %]<th id="[% tab | html %]_usedin" data-colname="[% tab | html %]_usedin">Used in</th><th></th>[% END %]
|
||||
[% IF ( ShowCourseReserves ) %]<th id="[% tab | html %]_course_reserves" data-colname="[% tab | html %]_course_reserves">Course reserves</th>[% END %]
|
||||
[% IF ( SpineLabelShowPrintOnBibDetails ) %]<th id="[% tab | html %]_spinelabel" data-colname="[% tab | html %]_spinelabel" class="NoSort">Spine label</th>[% END %]
|
||||
[% IF ( CAN_user_editcatalogue_edit_items ) %]<th id="[% tab | html %]_actions" data-colname="[% tab | html %]_actions"class="NoSort"> </th>[% END %]
|
||||
[% IF ( CAN_user_editcatalogue_edit_items ) %]<th id="[% tab | html %]_actions" data-colname="[% tab | html %]_actions"class="NoSort noExport"> </th>[% END %]
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
[% FOREACH item IN items %]
|
||||
<tr>
|
||||
<tr id="item_[% item.itemnumber | html %]" data-itemnumber="[% item.itemnumber | html %]">
|
||||
[% IF (StaffDetailItemSelection) %]
|
||||
<td style="text-align:center;vertical-align:middle">
|
||||
<input type="checkbox" value="[% item.itemnumber | html %]" name="itemnumber" />
|
||||
|
|
@ -545,6 +545,11 @@ Note that permanent location is a code, and location may be an authval.
|
|||
[% IF ( item.restricted ) %]
|
||||
<span class="restricted">([% item.restrictedvalue | html %])</span>
|
||||
[% END %]
|
||||
|
||||
[% IF ( item.bundle_host ) %]
|
||||
<span class="bundled">In bundle: [% INCLUDE 'biblio-title.inc' biblio = item.bundle_host.biblio link = 1 %]</span>
|
||||
[% END %]
|
||||
|
||||
</td>
|
||||
<td class="datelastseen" data-order="[% item.datelastseen | html %]">[% item.datelastseen | $KohaDates %]</td>
|
||||
<td class="issues" data-order="[% item.issues || 0 | html %]">[% item.issues || 0 | html %]</td>
|
||||
|
|
@ -631,6 +636,9 @@ Note that permanent location is a code, and location may be an authval.
|
|||
<a class="btn btn-default btn-xs" href="/cgi-bin/koha/cataloguing/additem.pl?op=edititem&biblionumber=[% item.biblionumber | html %]&itemnumber=[% item.itemnumber | html %]#edititem"><i class="fa fa-pencil"></i> Edit</a>
|
||||
[% END %]
|
||||
[% END %]
|
||||
[% IF bundlesEnabled %]
|
||||
<button class="btn btn-default btn-xs details-control"><i class="fa fa-folder"></i> Manage bundle</button>
|
||||
[% END %]
|
||||
</td>
|
||||
[% END %]
|
||||
</tr>
|
||||
|
|
@ -1222,6 +1230,37 @@ Note that permanent location is a code, and location may be an authval.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
[% IF bundlesEnabled %]
|
||||
<div class="modal" id="bundleItemsModal" tabindex="-1" role="dialog" aria-labelledby="bundleItemsLabel">
|
||||
<form id="bundleItemsForm" action="">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3 id="bundleItemsLabel">Add to bundle</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="result"></div>
|
||||
<fieldset class="rows">
|
||||
<ol>
|
||||
<li>
|
||||
<label class="required" for="external_id">Item barcode: </label>
|
||||
<input type="text" id="external_id" name="external_id" required="required">
|
||||
<span class="required">Required</span>
|
||||
</li>
|
||||
</ol>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-default">Submit</button>
|
||||
<button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
[% END %]
|
||||
|
||||
[% MACRO jsinclude BLOCK %]
|
||||
[% INCLUDE 'catalog-strings.inc' %]
|
||||
[% Asset.js("js/catalog.js") | $raw %]
|
||||
|
|
@ -1528,6 +1567,7 @@ Note that permanent location is a code, and location may be an authval.
|
|||
[% INCLUDE 'datatables.inc' %]
|
||||
[% Asset.js("lib/jquery/plugins/jquery.dataTables.columnFilter.js") | $raw %]
|
||||
[% INCLUDE 'columns_settings.inc' %]
|
||||
[% INCLUDE 'js-date-format.inc' %]
|
||||
[% Asset.js("js/browser.js") | $raw %]
|
||||
[% Asset.js("js/table_filters.js") | $raw %]
|
||||
<script>
|
||||
|
|
@ -1535,7 +1575,198 @@ Note that permanent location is a code, and location may be an authval.
|
|||
browser = KOHA.browser('[% searchid | html %]', parseInt(biblionumber, 10));
|
||||
browser.show();
|
||||
|
||||
[% IF bundlesEnabled %]
|
||||
var bundle_settings = [% TablesSettings.GetTableSettings('catalogue', 'detail','bundle_tables','json') | $raw %];
|
||||
[% END %]
|
||||
$(document).ready(function() {
|
||||
|
||||
[% IF bundlesEnabled %] // Bundle handling
|
||||
function createChild ( row, itemnumber ) {
|
||||
|
||||
// Toolbar
|
||||
var bundle_toolbar = $('<div id="toolbar" class="btn-toolbar"><a class="btn btn-default" data-toggle="modal" data-target="#bundleItemsModal" data-item="' + itemnumber + '"><i class="fa fa-plus"></i> Add to bundle</a></div>');
|
||||
|
||||
// This is the table we'll convert into a DataTable
|
||||
var bundles_table = $('<table class="display tbundle" data-itemnumber="'+itemnumber+'" id="bundle_table_'+itemnumber+'" width="100%"/>');
|
||||
|
||||
// Display it the child row
|
||||
row.child( bundle_toolbar.add(bundles_table) ).show();
|
||||
|
||||
// Initialise as a DataTable
|
||||
var bundle_table_url = "/api/v1/items/" + itemnumber + "/bundled_items?";
|
||||
var bundle_table = bundles_table.kohaTable({
|
||||
"ajax": {
|
||||
"url": bundle_table_url
|
||||
},
|
||||
"header_filter": false,
|
||||
"embed": [
|
||||
"biblio"
|
||||
],
|
||||
"order": [[ 1, "asc" ]],
|
||||
"columnDefs": [ {
|
||||
"targets": [0,1,2,3,4,5],
|
||||
"render": function (data, type, row, meta) {
|
||||
if ( data && type == 'display' ) {
|
||||
return data.escapeHtml();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
} ],
|
||||
"columns": [
|
||||
{
|
||||
"data": "biblio.title:biblio.medium",
|
||||
"title": "Title",
|
||||
"searchable": true,
|
||||
"orderable": true,
|
||||
"render": function(data, type, row, meta) {
|
||||
var title = "";
|
||||
if ( row.biblio.title ) {
|
||||
title = title.concat('<span class="biblio-title">',row.biblio.title,'</span>');
|
||||
}
|
||||
if ( row.biblio.subtitle ) {
|
||||
title = title.concat('<span class="biblio-subtitle">',row.biblio.subtitle,'</span>');
|
||||
}
|
||||
if ( row.biblio.medium ) {
|
||||
title = title.concat('<span class="biblio-medium">',row.biblio.medium,'</span>');
|
||||
}
|
||||
return title;
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": "biblio.author",
|
||||
"title": "Author",
|
||||
"searchable": true,
|
||||
"orderable": true,
|
||||
},
|
||||
{
|
||||
"data": "collection_code",
|
||||
"title": "Collection code",
|
||||
"searchable": true,
|
||||
"orderable": true,
|
||||
},
|
||||
{
|
||||
"data": "item_type",
|
||||
"title": "Item Type",
|
||||
"searchable": false,
|
||||
"orderable": true,
|
||||
},
|
||||
{
|
||||
"data": "callnumber",
|
||||
"title": "Callnumber",
|
||||
"searchable": true,
|
||||
"orderable": true,
|
||||
},
|
||||
{
|
||||
"data": "external_id",
|
||||
"title": "Barcode",
|
||||
"searchable": true,
|
||||
"orderable": true,
|
||||
},
|
||||
{
|
||||
"data": "lost_status:last_seen_date",
|
||||
"title": "Status",
|
||||
"searchable": false,
|
||||
"orderable": true,
|
||||
"render": function(data, type, row, meta) {
|
||||
if ( row.lost_status ) {
|
||||
return "Lost: " + row.lost_status;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": function( row, type, val, meta ) {
|
||||
var result = '<button class="btn btn-default btn-xs remove" role="button" data-itemnumber="'+row.item_id+'"><i class="fa fa-minus" aria-hidden="true"></i> '+_("Remove")+'</button>\n';
|
||||
return result;
|
||||
},
|
||||
"title": "Actions",
|
||||
"searchable": false,
|
||||
"orderable": false,
|
||||
"class": "noExport"
|
||||
}
|
||||
]
|
||||
}, bundle_settings, 1);
|
||||
|
||||
$(".tbundle").on("click", ".remove", function(){
|
||||
var bundle_table = $(this).closest('table');
|
||||
var host_itemnumber = bundle_table.data('itemnumber');
|
||||
var component_itemnumber = $(this).data('itemnumber');
|
||||
var unlink_item_url = "/api/v1/items/" + host_itemnumber + "/bundled_items/" + component_itemnumber;
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url: unlink_item_url,
|
||||
success: function(){
|
||||
bundle_table.DataTable({ 'retrieve': true }).draw(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var bundle_changed;
|
||||
var bundle_form_active;
|
||||
$("#bundleItemsModal").on("shown.bs.modal", function(e){
|
||||
var button = $(e.relatedTarget);
|
||||
var item_id = button.data('item');
|
||||
$("#result").replaceWith('<div id="result"></div>');
|
||||
$("#bundleItemsForm").attr('action', '/api/v1/items/' + item_id + '/bundled_items');
|
||||
$("#external_id").focus();
|
||||
bundle_changed = 0;
|
||||
bundle_form_active = item_id;
|
||||
});
|
||||
|
||||
$("#bundleItemsForm").submit(function(event) {
|
||||
|
||||
/* stop form from submitting normally */
|
||||
event.preventDefault();
|
||||
|
||||
/* get the action attribute from the <form action=""> element */
|
||||
var $form = $(this),
|
||||
url = $form.attr('action');
|
||||
|
||||
/* Send the data using post with external_id */
|
||||
var posting = $.post({
|
||||
url: url,
|
||||
data: JSON.stringify({ external_id: $('#external_id').val()}),
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
/* Report the results */
|
||||
posting.done(function(data) {
|
||||
var barcode = $('#external_id').val();
|
||||
$('#result').replaceWith('<div id="result" class="alert alert-success">Success: Added '+barcode+'</div>');
|
||||
$('#external_id').val('').focus();
|
||||
bundle_changed = 1;
|
||||
});
|
||||
posting.fail(function(data) {
|
||||
var barcode = $('#external_id').val();
|
||||
if ( data.status === 409 ) {
|
||||
var response = data.responseJSON;
|
||||
if ( response.key === "PRIMARY" ) {
|
||||
$('#result').replaceWith('<div id="result" class="alert alert-warning">Warning: Item '+barcode+' already attached</div>');
|
||||
} else {
|
||||
$('#result').replaceWith('<div id="result" class="alert alert-danger">Failure: Item '+barcode+' belongs to another bundle</div>');
|
||||
}
|
||||
} else {
|
||||
$('#result').replaceWith('<div id="result" class="alert alert-danger">Failure: Check the logs for details</div>');
|
||||
}
|
||||
$('#external_id').val('').focus();
|
||||
});
|
||||
});
|
||||
|
||||
$("#bundleItemsModal").on("hidden.bs.modal", function(e){
|
||||
if ( bundle_changed ) {
|
||||
$('#bundle_table_'+bundle_form_active).DataTable({ 'retrieve': true }).ajax.reload();
|
||||
}
|
||||
bundle_form_active = 0;
|
||||
bundle_changed = 0;
|
||||
});
|
||||
|
||||
// End bundle handling
|
||||
[% END %]
|
||||
|
||||
var table_names = [ 'holdings_table', 'otherholdings_table' ];
|
||||
var table_settings = [ [% TablesSettings.GetTableSettings('catalogue', 'detail','holdings_table','json') | $raw %], [% TablesSettings.GetTableSettings('catalogue', 'detail','otherholdings_table','json') | $raw %] ];
|
||||
var has_images = [ "[% itemloop_has_images | html %]", "[% otheritemloop_has_images | html %]" ];
|
||||
|
|
@ -1551,6 +1782,28 @@ Note that permanent location is a code, and location may be an authval.
|
|||
"sDom": 'C<"top pager"ilpfB><"#filter_c">tr<"bottom pager"ip>',
|
||||
};
|
||||
var table = KohaTable( table_name, dt_parameters, table_settings[index], 'with_filters' );
|
||||
|
||||
[% IF bundlesEnabled %]
|
||||
// Add event listener for opening and closing bundle details
|
||||
$('#' + table_name + ' tbody').on('click', 'button.details-control', function () {
|
||||
var tr = $(this).closest('tr');
|
||||
var dTable = $(this).closest('table').DataTable({ 'retrieve': true });
|
||||
|
||||
var itemnumber = tr.data('itemnumber');
|
||||
var row = dTable.row( tr );
|
||||
|
||||
if ( row.child.isShown() ) {
|
||||
// This row is already open - close it
|
||||
row.child.hide();
|
||||
tr.removeClass('shown');
|
||||
}
|
||||
else {
|
||||
// Open this row
|
||||
createChild(row, itemnumber);
|
||||
tr.addClass('shown');
|
||||
}
|
||||
} );
|
||||
[% END %]
|
||||
});
|
||||
|
||||
[% IF Koha.Preference('AcquisitionDetails') %]
|
||||
|
|
@ -1572,6 +1825,7 @@ Note that permanent location is a code, and location may be an authval.
|
|||
"sPaginationType": "full"
|
||||
}));
|
||||
[% END %]
|
||||
|
||||
});
|
||||
|
||||
[% IF (found1) %]
|
||||
|
|
|
|||
Loading…
Reference in New Issue