View | Details | Raw Unified | Return to bug 28854
Collapse All | Expand All

(-)a/C4/Accounts.pm (-1 / +6 lines)
Lines 70-76 FIXME : if no replacement price, borrower just doesn't get charged? Link Here
70
sub chargelostitem {
70
sub chargelostitem {
71
    my $dbh = C4::Context->dbh();
71
    my $dbh = C4::Context->dbh();
72
    my ($borrowernumber, $itemnumber, $amount, $description) = @_;
72
    my ($borrowernumber, $itemnumber, $amount, $description) = @_;
73
    my $itype = Koha::ItemTypes->find({ itemtype => Koha::Items->find($itemnumber)->effective_itemtype() });
73
    my $item  = Koha::Items->find($itemnumber);
74
    my $itype = Koha::ItemTypes->find({ itemtype => $item->effective_itemtype() });
74
    my $replacementprice = $amount;
75
    my $replacementprice = $amount;
75
    my $defaultreplacecost = $itype->defaultreplacecost;
76
    my $defaultreplacecost = $itype->defaultreplacecost;
76
    my $processfee = $itype->processfee;
77
    my $processfee = $itype->processfee;
Lines 80-85 sub chargelostitem { Link Here
80
        $replacementprice = $defaultreplacecost;
81
        $replacementprice = $defaultreplacecost;
81
    }
82
    }
82
    my $checkout = Koha::Checkouts->find({ itemnumber => $itemnumber });
83
    my $checkout = Koha::Checkouts->find({ itemnumber => $itemnumber });
84
    if ( !$checkout && $item->in_bundle ) {
85
        my $host = $item->bundle_host;
86
        $checkout = $host->checkout;
87
    }
83
    my $issue_id = $checkout ? $checkout->issue_id : undef;
88
    my $issue_id = $checkout ? $checkout->issue_id : undef;
84
89
85
    my $account = Koha::Account->new({ patron_id => $borrowernumber });
90
    my $account = Koha::Account->new({ patron_id => $borrowernumber });
(-)a/C4/Items.pm (+6 lines)
Lines 526-531 sub GetItemsForInventory { Link Here
526
    my $minlocation  = $parameters->{'minlocation'}  // '';
526
    my $minlocation  = $parameters->{'minlocation'}  // '';
527
    my $maxlocation  = $parameters->{'maxlocation'}  // '';
527
    my $maxlocation  = $parameters->{'maxlocation'}  // '';
528
    my $class_source = $parameters->{'class_source'}  // C4::Context->preference('DefaultClassificationSource');
528
    my $class_source = $parameters->{'class_source'}  // C4::Context->preference('DefaultClassificationSource');
529
    my $items_bundle = $parameters->{'items_bundle'} // '';
529
    my $location     = $parameters->{'location'}     // '';
530
    my $location     = $parameters->{'location'}     // '';
530
    my $itemtype     = $parameters->{'itemtype'}     // '';
531
    my $itemtype     = $parameters->{'itemtype'}     // '';
531
    my $ignoreissued = $parameters->{'ignoreissued'} // '';
532
    my $ignoreissued = $parameters->{'ignoreissued'} // '';
Lines 572-577 sub GetItemsForInventory { Link Here
572
        push @bind_params, $max_cnsort;
573
        push @bind_params, $max_cnsort;
573
    }
574
    }
574
575
576
    if ($items_bundle) {
577
        push @where_strings, 'items.itemnumber IN (SELECT itemnumber FROM items_bundle_item WHERE items_bundle_id = ?)';
578
        push @bind_params, $items_bundle;
579
    }
580
575
    if ($datelastseen) {
581
    if ($datelastseen) {
576
        $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
582
        $datelastseen = output_pref({ str => $datelastseen, dateformat => 'iso', dateonly => 1 });
577
        push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
583
        push @where_strings, '(datelastseen < ? OR datelastseen IS NULL)';
(-)a/Koha/Biblio.pm (+20 lines)
Lines 37-42 use Koha::Biblio::Metadatas; Link Here
37
use Koha::Biblioitems;
37
use Koha::Biblioitems;
38
use Koha::CirculationRules;
38
use Koha::CirculationRules;
39
use Koha::Item::Transfer::Limits;
39
use Koha::Item::Transfer::Limits;
40
use Koha::Item::Bundles;
40
use Koha::Items;
41
use Koha::Items;
41
use Koha::Libraries;
42
use Koha::Libraries;
42
use Koha::Suggestions;
43
use Koha::Suggestions;
Lines 419-424 sub items { Link Here
419
    return Koha::Items->_new_from_dbic( $items_rs );
420
    return Koha::Items->_new_from_dbic( $items_rs );
420
}
421
}
421
422
423
=head3 item_bundles
424
425
  my $bundles = $biblio->item_bundles();
426
427
Returns the related Koha::Items limited to items that are bundles for this biblio;
428
429
=cut
430
431
sub item_bundles {
432
    my ($self) = @_;
433
434
    my $items_rs = $self->_result->items(
435
        { 'item_bundles_hosts.host' => { '!=' => undef } },
436
        { join                      => 'item_bundles_hosts', collapse => 1 }
437
    );
438
439
    return Koha::Item::Bundles->_new_from_dbic($items_rs);
440
}
441
422
=head3 itemtype
442
=head3 itemtype
423
443
424
my $itemtype = $biblio->itemtype();
444
my $itemtype = $biblio->itemtype();
(-)a/Koha/Item.pm (+199 lines)
Lines 21-26 use Modern::Perl; Link Here
21
21
22
use List::MoreUtils qw( any );
22
use List::MoreUtils qw( any );
23
use Data::Dumper qw( Dumper );
23
use Data::Dumper qw( Dumper );
24
use Try::Tiny qw( catch try );
24
25
25
use Koha::Database;
26
use Koha::Database;
26
use Koha::DateUtils qw( dt_from_string );
27
use Koha::DateUtils qw( dt_from_string );
Lines 393-398 sub checkout { Link Here
393
    return Koha::Checkout->_new_from_dbic( $checkout_rs );
394
    return Koha::Checkout->_new_from_dbic( $checkout_rs );
394
}
395
}
395
396
397
=head3 last_checkout
398
399
  my $old_checkout = $item->last_checkout;
400
401
Return the last_checkout for this item
402
403
=cut
404
405
sub last_checkout {
406
    my ( $self ) = @_;
407
    my $checkout_rs = $self->_result->old_issues->search( {},
408
        { order_by => { '-desc' => 'returndate' }, rows => 1 } )->single;
409
    return unless $checkout_rs;
410
    return Koha::Old::Checkout->_new_from_dbic( $checkout_rs );
411
}
412
413
=head3 loss_checkout
414
415
  my $loss_checkout = $item->loss_checkout;
416
417
Return the old checkout from which this item was marked as lost
418
419
=cut
420
421
sub loss_checkout {
422
    my ( $self ) = @_;
423
    my $items_lost_issue_rs = $self->_result->items_lost_issue;
424
    return unless $items_lost_issue_rs;
425
    my $issue_rs = $items_lost_issue_rs->issue;
426
    return unless $issue_rs;
427
    return Koha::Old::Checkout->_new_from_dbic( $issue_rs );
428
}
429
396
=head3 holds
430
=head3 holds
397
431
398
my $holds = $item->holds();
432
my $holds = $item->holds();
Lines 1172-1177 sub itemtype { Link Here
1172
    return Koha::ItemTypes->find( $self->effective_itemtype );
1206
    return Koha::ItemTypes->find( $self->effective_itemtype );
1173
}
1207
}
1174
1208
1209
1210
=head3 bundle_items
1211
1212
  my $bundle_items = $item->bundle_items;
1213
1214
Returns the items associated with this bundle
1215
1216
=cut
1217
1218
sub bundle_items {
1219
    my ($self) = @_;
1220
1221
    if ( !$self->{_bundle_items_cached} ) {
1222
        my $bundle_items = Koha::Items->search(
1223
            { 'item_bundles_item.host' => $self->itemnumber },
1224
            { join                     => 'item_bundles_item' } );
1225
        $self->{_bundle_items}        = $bundle_items;
1226
        $self->{_bundle_items_cached} = 1;
1227
    }
1228
1229
    return $self->{_bundle_items};
1230
}
1231
1232
=head3 is_bundle
1233
1234
  my $is_bundle = $item->is_bundle;
1235
1236
Returns whether the item is a bundle or not
1237
1238
=cut
1239
1240
sub is_bundle {
1241
    my ($self) = @_;
1242
    return $self->bundle_items->count ? 1 : 0;
1243
}
1244
1245
=head3 bundle_host
1246
1247
  my $bundle = $item->bundle_host;
1248
1249
Returns the bundle item this item is attached to
1250
1251
=cut
1252
1253
sub bundle_host {
1254
    my ($self) = @_;
1255
1256
    if ( !$self->{_bundle_host_cached} ) {
1257
        my $bundle_item_rs = $self->_result->item_bundles_item;
1258
        $self->{_bundle_host} =
1259
          $bundle_item_rs
1260
          ? Koha::Item->_new_from_dbic($bundle_item_rs->host)
1261
          : undef;
1262
        $self->{_bundle_host_cached} = 1;
1263
    }
1264
1265
    return $self->{_bundle_host};
1266
}
1267
1268
=head3 in_bundle
1269
1270
  my $in_bundle = $item->in_bundle;
1271
1272
Returns whether this item is currently in a bundle
1273
1274
=cut
1275
1276
sub in_bundle {
1277
    my ($self) = @_;
1278
    return $self->bundle_host ? 1 : 0;
1279
}
1280
1281
=head3 add_to_bundle
1282
1283
  my $link = $item->add_to_bundle($bundle_item);
1284
1285
Adds the bundle_item passed to this item
1286
1287
=cut
1288
1289
sub add_to_bundle {
1290
    my ( $self, $bundle_item ) = @_;
1291
1292
    my $schema = Koha::Database->new->schema;
1293
1294
    my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1295
1296
    try {
1297
        $schema->txn_do(
1298
            sub {
1299
                $self->_result->add_to_item_bundles_hosts(
1300
                    { item => $bundle_item->itemnumber } );
1301
1302
                $bundle_item->notforloan($BundleNotLoanValue)->store();
1303
            }
1304
        );
1305
    }
1306
    catch {
1307
1308
        # 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
1309
        if ( ref($_) eq 'DBIx::Class::Exception' ) {
1310
            warn $_->{msg};
1311
            if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1312
                # FK constraints
1313
                # FIXME: MySQL error, if we support more DB engines we should implement this for each
1314
                if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1315
                    Koha::Exceptions::Object::FKConstraint->throw(
1316
                        error     => 'Broken FK constraint',
1317
                        broken_fk => $+{column}
1318
                    );
1319
                }
1320
            }
1321
            elsif (
1322
                $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1323
            {
1324
                Koha::Exceptions::Object::DuplicateID->throw(
1325
                    error        => 'Duplicate ID',
1326
                    duplicate_id => $+{key}
1327
                );
1328
            }
1329
            elsif ( $_->{msg} =~
1330
/Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1331
              )
1332
            {    # The optional \W in the regex might be a quote or backtick
1333
                my $type     = $+{type};
1334
                my $value    = $+{value};
1335
                my $property = $+{property};
1336
                $property =~ s/['`]//g;
1337
                Koha::Exceptions::Object::BadValue->throw(
1338
                    type     => $type,
1339
                    value    => $value,
1340
                    property => $property =~ /(\w+\.\w+)$/
1341
                    ? $1
1342
                    : $property
1343
                    ,    # results in table.column without quotes or backtics
1344
                );
1345
            }
1346
1347
            # Catch-all for foreign key breakages. It will help find other use cases
1348
            $_->rethrow();
1349
        }
1350
        else {
1351
            $_;
1352
        }
1353
    };
1354
}
1355
1356
=head3 remove_from_bundle
1357
1358
Remove this item from any bundle it may have been attached to.
1359
1360
=cut
1361
1362
sub remove_from_bundle {
1363
    my ($self) = @_;
1364
1365
    my $bundle_item_rs = $self->_result->item_bundles_item;
1366
    if ( $bundle_item_rs ) {
1367
        $bundle_item_rs->delete;
1368
        $self->notforloan(0)->store();
1369
        return 1;
1370
    }
1371
    return 0;
1372
}
1373
1175
=head2 Internal methods
1374
=head2 Internal methods
1176
1375
1177
=head3 _after_item_action_hooks
1376
=head3 _after_item_action_hooks
(-)a/Koha/Item/Bundle.pm (+68 lines)
Line 0 Link Here
1
package Koha::Item::Bundle;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Koha::Database;
21
22
use base qw(Koha::Item);
23
24
=head1 NAME
25
26
Koha::Item::Bundle - Koha Item Bundle Object class
27
28
=head1 API
29
30
=head2 Class Methods
31
32
=head3 host
33
34
  my $host = $bundle->host;
35
36
Returns the associated host item for this bundle.
37
38
=cut
39
40
sub host {
41
    my ($self) = @_;
42
    my $host_rs = $self->_result->itemnumber;
43
    return Koha::Item->_new_from_dbic($host_rs);
44
}
45
46
=head3 items
47
48
  my $items = $bundle->items;
49
50
Returns the associated items attached to this bundle.
51
52
=cut
53
54
sub items {
55
    my ($self) = @_;
56
    my $items_rs = $self->_result->itemnumbers;
57
    return Koha::Items->_new_from_dbic($items_rs);
58
}
59
60
=head3 type
61
62
=cut
63
64
sub _type {
65
    return 'Item';
66
}
67
68
1;
(-)a/Koha/Item/Bundles.pm (+63 lines)
Line 0 Link Here
1
package Koha::Item::Bundles;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Koha::Database;
21
use Koha::Item::Bundle;
22
23
use base qw(Koha::Items);
24
25
=head1 NAME
26
27
Koha::Item::Bundles - Koha Item Bundles Object set class
28
29
=head1 API
30
31
=head2 Class Methods
32
33
=head3 search
34
35
  my $bundles = Koha::Item::Bundles->search( $where, $attr );
36
37
Return a Koha::Item::Bundles set.
38
39
=cut
40
41
sub search {
42
    my ($self , $where, $attr ) = @_;
43
44
    my $rs = $self->SUPER::search(
45
        { 'item_bundles_hosts.host' => { '!=' => undef } },
46
        { join                      => 'item_bundles_hosts', collapse => 1 }
47
    );
48
    return $rs->search( $where, $attr );
49
}
50
51
=head3 type
52
53
=cut
54
55
sub _type {
56
    return 'Item';
57
}
58
59
sub object_class {
60
    return 'Koha::Item::Bundle';
61
}
62
63
1;
(-)a/Koha/REST/V1/Items.pm (+120 lines)
Lines 152-155 sub pickup_locations { Link Here
152
    };
152
    };
153
}
153
}
154
154
155
=head3 bundled_items
156
157
Controller function that handles bundled_items Koha::Item objects
158
159
=cut
160
161
sub bundled_items {
162
    my $c = shift->openapi->valid_input or return;
163
164
    my $item_id = $c->validation->param('item_id');
165
    my $item = Koha::Items->find( $item_id );
166
167
    unless ($item) {
168
        return $c->render(
169
            status  => 404,
170
            openapi => { error => "Item not found" }
171
        );
172
    }
173
174
    return try {
175
        my $items_set = $item->bundle_items;
176
        my $items     = $c->objects->search( $items_set );
177
        return $c->render(
178
            status  => 200,
179
            openapi => $items
180
        );
181
    }
182
    catch {
183
        $c->unhandled_exception($_);
184
    };
185
}
186
187
=head3 add_to_bundle
188
189
Controller function that handles adding items to this bundle
190
191
=cut
192
193
sub add_to_bundle {
194
    my $c = shift->openapi->valid_input or return;
195
196
    my $item_id = $c->validation->param('item_id');
197
    my $item = Koha::Items->find( $item_id );
198
199
    unless ($item) {
200
        return $c->render(
201
            status  => 404,
202
            openapi => { error => "Item not found" }
203
        );
204
    }
205
206
207
    my $bundle_item_id = $c->validation->param('body')->{'external_id'};
208
    my $bundle_item = Koha::Items->find( { barcode => $bundle_item_id } );
209
210
    unless ($bundle_item) {
211
        return $c->render(
212
            status  => 404,
213
            openapi => { error => "Bundle item not found" }
214
        );
215
    }
216
217
    return try {
218
        my $link = $item->add_to_bundle($bundle_item);
219
        return $c->render(
220
            status  => 201,
221
            openapi => $bundle_item
222
        );
223
    }
224
    catch {
225
        if ( ref($_) eq 'Koha::Exceptions::Object::DuplicateID' ) {
226
            return $c->render(
227
                status  => 409,
228
                openapi => {
229
                    error => 'Item is already bundled',
230
                    key   => $_->duplicate_id
231
                }
232
            );
233
        }
234
        else {
235
            $c->unhandled_exception($_);
236
        }
237
    };
238
}
239
240
=head3 remove_from_bundle
241
242
Controller function that handles removing items from this bundle
243
244
=cut
245
246
sub remove_from_bundle {
247
    my $c = shift->openapi->valid_input or return;
248
249
    my $item_id = $c->validation->param('item_id');
250
    my $item = Koha::Items->find( $item_id );
251
252
    unless ($item) {
253
        return $c->render(
254
            status  => 404,
255
            openapi => { error => "Item not found" }
256
        );
257
    }
258
259
    my $bundle_item_id = $c->validation->param('body')->{'item_id'};
260
    my $bundle_item = Koha::Items->find( { itemnumber => $bundle_item_id } );
261
262
    unless ($bundle_item) {
263
        return $c->render(
264
            status  => 404,
265
            openapi => { error => "Bundle item not found" }
266
        );
267
    }
268
269
    $bundle_item->remove_from_bundle;
270
    return $c->render(
271
        status  => 204,
272
    );
273
}
274
155
1;
275
1;
(-)a/Koha/Schema/Result/Item.pm (-2 / +32 lines)
Lines 729-734 __PACKAGE__->might_have( Link Here
729
  { cascade_copy => 0, cascade_delete => 0 },
729
  { cascade_copy => 0, cascade_delete => 0 },
730
);
730
);
731
731
732
=head2 item_bundles_hosts
733
734
Type: has_many
735
736
Related object: L<Koha::Schema::Result::ItemBundle>
737
738
=cut
739
740
__PACKAGE__->has_many(
741
  "item_bundles_hosts",
742
  "Koha::Schema::Result::ItemBundle",
743
  { "foreign.host" => "self.itemnumber" },
744
  { cascade_copy => 0, cascade_delete => 0 },
745
);
746
747
=head2 item_bundles_item
748
749
Type: might_have
750
751
Related object: L<Koha::Schema::Result::ItemBundle>
752
753
=cut
754
755
__PACKAGE__->might_have(
756
  "item_bundles_item",
757
  "Koha::Schema::Result::ItemBundle",
758
  { "foreign.item" => "self.itemnumber" },
759
  { cascade_copy => 0, cascade_delete => 0 },
760
);
761
732
=head2 items_last_borrower
762
=head2 items_last_borrower
733
763
734
Type: might_have
764
Type: might_have
Lines 850-857 __PACKAGE__->has_many( Link Here
850
);
880
);
851
881
852
882
853
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-01-21 13:39:29
883
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-08-10 13:47:56
854
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:U5Tm2JfUnfhACRDJ4SpFgQ
884
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:zKbxMr6eySAbQsgtVP7kVg
855
885
856
__PACKAGE__->belongs_to( biblioitem => "Koha::Schema::Result::Biblioitem", "biblioitemnumber" );
886
__PACKAGE__->belongs_to( biblioitem => "Koha::Schema::Result::Biblioitem", "biblioitemnumber" );
857
887
(-)a/Koha/Schema/Result/ItemBundle.pm (+113 lines)
Line 0 Link Here
1
use utf8;
2
package Koha::Schema::Result::ItemBundle;
3
4
# Created by DBIx::Class::Schema::Loader
5
# DO NOT MODIFY THE FIRST PART OF THIS FILE
6
7
=head1 NAME
8
9
Koha::Schema::Result::ItemBundle
10
11
=cut
12
13
use strict;
14
use warnings;
15
16
use base 'DBIx::Class::Core';
17
18
=head1 TABLE: C<item_bundles>
19
20
=cut
21
22
__PACKAGE__->table("item_bundles");
23
24
=head1 ACCESSORS
25
26
=head2 item
27
28
  data_type: 'integer'
29
  is_foreign_key: 1
30
  is_nullable: 0
31
32
=head2 host
33
34
  data_type: 'integer'
35
  is_foreign_key: 1
36
  is_nullable: 0
37
38
=cut
39
40
__PACKAGE__->add_columns(
41
  "item",
42
  { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
43
  "host",
44
  { data_type => "integer", is_foreign_key => 1, is_nullable => 0 },
45
);
46
47
=head1 PRIMARY KEY
48
49
=over 4
50
51
=item * L</host>
52
53
=item * L</item>
54
55
=back
56
57
=cut
58
59
__PACKAGE__->set_primary_key("host", "item");
60
61
=head1 UNIQUE CONSTRAINTS
62
63
=head2 C<item_bundles_uniq_1>
64
65
=over 4
66
67
=item * L</item>
68
69
=back
70
71
=cut
72
73
__PACKAGE__->add_unique_constraint("item_bundles_uniq_1", ["item"]);
74
75
=head1 RELATIONS
76
77
=head2 host
78
79
Type: belongs_to
80
81
Related object: L<Koha::Schema::Result::Item>
82
83
=cut
84
85
__PACKAGE__->belongs_to(
86
  "host",
87
  "Koha::Schema::Result::Item",
88
  { itemnumber => "host" },
89
  { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
90
);
91
92
=head2 item
93
94
Type: belongs_to
95
96
Related object: L<Koha::Schema::Result::Item>
97
98
=cut
99
100
__PACKAGE__->belongs_to(
101
  "item",
102
  "Koha::Schema::Result::Item",
103
  { itemnumber => "item" },
104
  { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
105
);
106
107
108
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2021-08-10 13:47:56
109
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:QXlgF8iwZUFSsg+Eo5kNUw
110
111
112
# You can replace this text with custom code or comments, and it will be preserved on regeneration
113
1;
(-)a/api/v1/swagger/definitions.json (+3 lines)
Lines 5-10 Link Here
5
  "basket": {
5
  "basket": {
6
    "$ref": "definitions/basket.json"
6
    "$ref": "definitions/basket.json"
7
  },
7
  },
8
  "bundle_link": {
9
    "$ref": "definitions/bundle_link.json"
10
  },
8
  "cashup": {
11
  "cashup": {
9
    "$ref": "definitions/cashup.json"
12
    "$ref": "definitions/cashup.json"
10
  },
13
  },
(-)a/api/v1/swagger/definitions/bundle_link.json (+14 lines)
Line 0 Link Here
1
{
2
    "type": "object",
3
    "properties": {
4
        "item_id": {
5
            "type": ["integer", "null"],
6
            "description": "Internal item identifier"
7
        },
8
        "external_id": {
9
            "type": ["string", "null"],
10
            "description": "Item barcode"
11
        }
12
    },
13
    "additionalProperties": false
14
}
(-)a/api/v1/swagger/definitions/item.json (+4 lines)
Lines 180-185 Link Here
180
    "exclude_from_local_holds_priority": {
180
    "exclude_from_local_holds_priority": {
181
      "type": "boolean",
181
      "type": "boolean",
182
      "description": "Exclude this item from local holds priority."
182
      "description": "Exclude this item from local holds priority."
183
    },
184
    "biblio": {
185
    },
186
    "checkout": {
183
    }
187
    }
184
  },
188
  },
185
  "additionalProperties": false,
189
  "additionalProperties": false,
(-)a/api/v1/swagger/paths.json (+6 lines)
Lines 83-88 Link Here
83
  "/items/{item_id}": {
83
  "/items/{item_id}": {
84
    "$ref": "paths/items.json#/~1items~1{item_id}"
84
    "$ref": "paths/items.json#/~1items~1{item_id}"
85
  },
85
  },
86
  "/items/{item_id}/bundled_items": {
87
    "$ref": "paths/items.json#/~1items~1{item_id}~1bundled_items"
88
  },
89
  "/items/{item_id}/bundled_items/item": {
90
    "$ref": "paths/items.json#/~1items~1{item_id}~1bundled_items~1item"
91
  },
86
  "/items/{item_id}/pickup_locations": {
92
  "/items/{item_id}/pickup_locations": {
87
    "$ref": "paths/items.json#/~1items~1{item_id}~1pickup_locations"
93
    "$ref": "paths/items.json#/~1items~1{item_id}~1pickup_locations"
88
  },
94
  },
(-)a/api/v1/swagger/paths/items.json (+240 lines)
Lines 127-132 Link Here
127
      }
127
      }
128
    }
128
    }
129
  },
129
  },
130
  "/items/{item_id}/bundled_items": {
131
    "get": {
132
      "x-mojo-to": "Items#bundled_items",
133
      "operationId": "bundledItems",
134
      "tags": [
135
        "items"
136
      ],
137
      "summary": "List bundled items",
138
      "parameters": [
139
        {
140
          "$ref": "../parameters.json#/item_id_pp"
141
        },
142
        {
143
          "name": "external_id",
144
          "in": "query",
145
          "description": "Search on the item's barcode",
146
          "required": false,
147
          "type": "string"
148
        },
149
        {
150
          "$ref": "../parameters.json#/match"
151
        },
152
        {
153
          "$ref": "../parameters.json#/order_by"
154
        },
155
        {
156
          "$ref": "../parameters.json#/page"
157
        },
158
        {
159
          "$ref": "../parameters.json#/per_page"
160
        },
161
        {
162
          "$ref": "../parameters.json#/q_param"
163
        },
164
        {
165
          "$ref": "../parameters.json#/q_body"
166
        },
167
        {
168
          "$ref": "../parameters.json#/q_header"
169
        }
170
      ],
171
      "consumes": [
172
        "application/json"
173
      ],
174
      "produces": [
175
        "application/json"
176
      ],
177
      "responses": {
178
        "200": {
179
          "description": "A list of item",
180
          "schema": {
181
            "type": "array",
182
            "items": {
183
              "$ref": "../definitions.json#/item"
184
            }
185
          }
186
        },
187
        "401": {
188
          "description": "Authentication required",
189
          "schema": {
190
            "$ref": "../definitions.json#/error"
191
          }
192
        },
193
        "403": {
194
          "description": "Access forbidden",
195
          "schema": {
196
            "$ref": "../definitions.json#/error"
197
          }
198
        },
199
        "500": {
200
          "description": "Internal server error",
201
          "schema": {
202
            "$ref": "../definitions.json#/error"
203
          }
204
        },
205
        "503": {
206
          "description": "Under maintenance",
207
          "schema": {
208
            "$ref": "../definitions.json#/error"
209
          }
210
        }
211
      },
212
      "x-koha-authorization": {
213
        "permissions": {
214
          "catalogue": "1"
215
        }
216
      },
217
      "x-koha-embed": [
218
        "biblio",
219
        "checkout"
220
      ]
221
    }
222
  },
223
  "/items/{item_id}/bundled_items/item": {
224
      "post": {
225
          "x-mojo-to": "Items#add_to_bundle",
226
          "operationId": "addToBundle",
227
          "tags": ["items"],
228
          "summary": "Add item to bundle",
229
          "parameters": [{
230
                  "$ref": "../parameters.json#/item_id_pp"
231
              },
232
              {
233
                  "name": "body",
234
                  "in": "body",
235
                  "description": "A JSON object containing information about the new bundle link",
236
                  "required": true,
237
                  "schema": {
238
                      "$ref": "../definitions.json#/bundle_link"
239
                  }
240
              }
241
          ],
242
          "consumes": ["application/json"],
243
          "produces": ["application/json"],
244
          "responses": {
245
              "201": {
246
                  "description": "A successfully created bundle link",
247
                  "schema": {
248
                      "items": {
249
                          "$ref": "../definitions.json#/item"
250
                      }
251
                  }
252
              },
253
              "400": {
254
                  "description": "Bad parameter",
255
                  "schema": {
256
                      "$ref": "../definitions.json#/error"
257
                  }
258
              },
259
              "401": {
260
                  "description": "Authentication required",
261
                  "schema": {
262
                      "$ref": "../definitions.json#/error"
263
                  }
264
              },
265
              "403": {
266
                  "description": "Access forbidden",
267
                  "schema": {
268
                      "$ref": "../definitions.json#/error"
269
                  }
270
              },
271
              "404": {
272
                  "description": "Resource not found",
273
                  "schema": {
274
                      "$ref": "../definitions.json#/error"
275
                  }
276
              },
277
              "409": {
278
                  "description": "Conflict in creating resource",
279
                  "schema": {
280
                      "$ref": "../definitions.json#/error"
281
                  }
282
              },
283
              "500": {
284
                  "description": "Internal server error",
285
                  "schema": {
286
                      "$ref": "../definitions.json#/error"
287
                  }
288
              },
289
              "503": {
290
                  "description": "Under maintenance",
291
                  "schema": {
292
                      "$ref": "../definitions.json#/error"
293
                  }
294
              }
295
          },
296
          "x-koha-authorization": {
297
              "permissions": {
298
                  "catalogue": 1
299
              }
300
          }
301
      },
302
      "delete": {
303
          "x-mojo-to": "Items#remove_from_bundle",
304
          "operationId": "removeFromBundle",
305
          "tags": ["items"],
306
          "summary": "Remove item from bundle",
307
          "parameters": [{
308
                  "$ref": "../parameters.json#/item_id_pp"
309
              },
310
              {
311
                  "name": "body",
312
                  "in": "body",
313
                  "description": "A JSON object containing information about the bundle link to remove",
314
                  "required": true,
315
                  "schema": {
316
                      "$ref": "../definitions.json#/bundle_link"
317
                  }
318
              }
319
          ],
320
          "consumes": ["application/json"],
321
          "produces": ["application/json"],
322
          "responses": {
323
              "204": {
324
                  "description": "Bundle link deleted"
325
              },
326
              "400": {
327
                  "description": "Bad parameter",
328
                  "schema": {
329
                      "$ref": "../definitions.json#/error"
330
                  }
331
              },
332
              "401": {
333
                  "description": "Authentication required",
334
                  "schema": {
335
                      "$ref": "../definitions.json#/error"
336
                  }
337
              },
338
              "403": {
339
                  "description": "Access forbidden",
340
                  "schema": {
341
                      "$ref": "../definitions.json#/error"
342
                  }
343
              },
344
              "404": {
345
                  "description": "Resource not found",
346
                  "schema": {
347
                      "$ref": "../definitions.json#/error"
348
                  }
349
              },
350
              "500": {
351
                  "description": "Internal server error",
352
                  "schema": {
353
                      "$ref": "../definitions.json#/error"
354
                  }
355
              },
356
              "503": {
357
                  "description": "Under maintenance",
358
                  "schema": {
359
                      "$ref": "../definitions.json#/error"
360
                  }
361
              }
362
          },
363
          "x-koha-authorization": {
364
              "permissions": {
365
                  "catalogue": 1
366
              }
367
          }
368
      }
369
  },
130
  "/items/{item_id}/pickup_locations": {
370
  "/items/{item_id}/pickup_locations": {
131
    "get": {
371
    "get": {
132
      "x-mojo-to": "Items#pickup_locations",
372
      "x-mojo-to": "Items#pickup_locations",
(-)a/catalogue/detail.pl (+18 lines)
Lines 49-54 use Koha::AuthorisedValues; Link Here
49
use Koha::Biblios;
49
use Koha::Biblios;
50
use Koha::CoverImages;
50
use Koha::CoverImages;
51
use Koha::Illrequests;
51
use Koha::Illrequests;
52
use Koha::Database;
52
use Koha::Items;
53
use Koha::Items;
53
use Koha::ItemTypes;
54
use Koha::ItemTypes;
54
use Koha::Patrons;
55
use Koha::Patrons;
Lines 56-61 use Koha::Virtualshelves; Link Here
56
use Koha::Plugins;
57
use Koha::Plugins;
57
use Koha::SearchEngine::Search;
58
use Koha::SearchEngine::Search;
58
59
60
my $schema = Koha::Database->new()->schema();
61
59
my $query = CGI->new();
62
my $query = CGI->new();
60
63
61
my $analyze = $query->param('analyze');
64
my $analyze = $query->param('analyze');
Lines 203-208 if (@hostitems){ Link Here
203
206
204
my $dat = &GetBiblioData($biblionumber);
207
my $dat = &GetBiblioData($biblionumber);
205
208
209
#is biblio a collection
210
my $leader = $record->leader();
211
$dat->{collection} = ( substr($leader,7,1) eq 'c' ) ? 1 : 0;
212
213
206
#coping with subscriptions
214
#coping with subscriptions
207
my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber);
215
my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber);
208
my @subscriptions       = SearchSubscriptions({ biblionumber => $biblionumber, orderby => 'title' });
216
my @subscriptions       = SearchSubscriptions({ biblionumber => $biblionumber, orderby => 'title' });
Lines 395-400 foreach my $item (@items) { Link Here
395
        $item->{cover_images} = $item_object->cover_images;
403
        $item->{cover_images} = $item_object->cover_images;
396
    }
404
    }
397
405
406
    if ($item_object->is_bundle) {
407
        $itemfields{bundles} = 1;
408
        $item->{is_bundle} = 1;
409
    }
410
411
    if ($item_object->in_bundle) {
412
        $item->{bundle_host} = $item_object->bundle_host;
413
    }
414
398
    if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
415
    if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
399
        if ($itembranchcode and $itembranchcode eq $currentbranch) {
416
        if ($itembranchcode and $itembranchcode eq $currentbranch) {
400
            push @itemloop, $item;
417
            push @itemloop, $item;
Lines 451-456 $template->param( Link Here
451
    itemdata_copynumber => $itemfields{copynumber},
468
    itemdata_copynumber => $itemfields{copynumber},
452
    itemdata_stocknumber => $itemfields{stocknumber},
469
    itemdata_stocknumber => $itemfields{stocknumber},
453
    itemdata_publisheddate => $itemfields{publisheddate},
470
    itemdata_publisheddate => $itemfields{publisheddate},
471
    itemdata_bundles       => $itemfields{bundles},
454
    volinfo                => $itemfields{enumchron},
472
    volinfo                => $itemfields{enumchron},
455
        itemdata_itemnotes  => $itemfields{itemnotes},
473
        itemdata_itemnotes  => $itemfields{itemnotes},
456
        itemdata_nonpublicnotes => $itemfields{itemnotes_nonpublic},
474
        itemdata_nonpublicnotes => $itemfields{itemnotes_nonpublic},
(-)a/circ/returns.pl (-3 / +71 lines)
Lines 38-44 use C4::Auth qw( get_template_and_user get_session haspermission ); Link Here
38
use C4::Output qw( output_html_with_http_headers );
38
use C4::Output qw( output_html_with_http_headers );
39
use C4::Circulation qw( barcodedecode GetBranchItemRule AddReturn updateWrongTransfer LostItem );
39
use C4::Circulation qw( barcodedecode GetBranchItemRule AddReturn updateWrongTransfer LostItem );
40
use C4::Reserves qw( ModReserve ModReserveAffect GetOtherReserves );
40
use C4::Reserves qw( ModReserve ModReserveAffect GetOtherReserves );
41
use C4::Circulation qw( barcodedecode GetBranchItemRule AddReturn updateWrongTransfer LostItem );
42
use C4::Context;
41
use C4::Context;
43
use C4::Items qw( ModItemTransfer );
42
use C4::Items qw( ModItemTransfer );
44
use C4::Members::Messaging;
43
use C4::Members::Messaging;
Lines 272-277 if ($barcode) { Link Here
272
271
273
        my $checkout = $item->checkout;
272
        my $checkout = $item->checkout;
274
        my $biblio   = $item->biblio;
273
        my $biblio   = $item->biblio;
274
275
        $template->param(
275
        $template->param(
276
            title                => $biblio->title,
276
            title                => $biblio->title,
277
            returnbranch         => $returnbranch,
277
            returnbranch         => $returnbranch,
Lines 282-287 if ($barcode) { Link Here
282
            issue                => $checkout,
282
            issue                => $checkout,
283
            item                 => $item,
283
            item                 => $item,
284
        );
284
        );
285
285
    } # FIXME else we should not call AddReturn but set BadBarcode directly instead
286
    } # FIXME else we should not call AddReturn but set BadBarcode directly instead
286
287
287
    my %input = (
288
    my %input = (
Lines 301-310 if ($barcode) { Link Here
301
    $template->param( 'multiple_confirmed' => 1 )
302
    $template->param( 'multiple_confirmed' => 1 )
302
      if $query->param('multiple_confirm');
303
      if $query->param('multiple_confirm');
303
304
305
    # Block return if bundle and confirm has not been received
306
    my $bundle_confirm =
307
         $item
308
      && $item->is_bundle
309
      && !$query->param('confirm_items_bundle_return');
310
    $template->param( 'confirm_items_bundle_returned' => 1 )
311
      if $query->param('confirm_items_bundle_return');
312
304
    # do the return
313
    # do the return
305
    ( $returned, $messages, $issue, $borrower ) =
314
    ( $returned, $messages, $issue, $borrower ) =
306
      AddReturn( $barcode, $userenv_branch, $exemptfine, $return_date )
315
      AddReturn( $barcode, $userenv_branch, $exemptfine, $return_date )
307
          unless $needs_confirm;
316
          unless ( $needs_confirm || $bundle_confirm );
308
317
309
    if ($returned) {
318
    if ($returned) {
310
        my $time_now = dt_from_string()->truncate( to => 'minute');
319
        my $time_now = dt_from_string()->truncate( to => 'minute');
Lines 345-351 if ($barcode) { Link Here
345
                );
354
                );
346
            }
355
            }
347
        }
356
        }
348
    } elsif ( C4::Context->preference('ShowAllCheckins') and !$messages->{'BadBarcode'} and !$needs_confirm ) {
357
358
        # Mark missing bundle items as lost and report unexpected items
359
        if ( $item->is_bundle ) {
360
            my $BundleLostValue = C4::Context->preference('BundleLostValue');
361
            my $checkout = $item->last_checkout;
362
            my $barcodes = $query->param('verify-items-bundle-contents-barcodes');
363
            my @barcodes = map { s/^\s+|\s+$//gr } ( split /\n/, $barcodes );
364
            my $expected_items = { map { $_->barcode => $_ } $item->bundle_items->as_list };
365
            my $verify_items = Koha::Items->search( { barcode => { 'in' => \@barcodes } } );
366
            my @unexpected_items;
367
            my @missing_items;
368
            my @bundle_items;
369
            while ( my $verify_item = $verify_items->next ) {
370
                # Fix and lost statuses
371
                $verify_item->itemlost(0);
372
                
373
                # Expected item, remove from lookup table
374
                if ( delete $expected_items->{$verify_item->barcode} ) {
375
                    push @bundle_items, $verify_item;
376
                }
377
                # Unexpected item, warn and remove from bundle
378
                else {
379
                    $verify_item->remove_from_bundle;
380
                    push @unexpected_items, $verify_item;
381
                }
382
                # Store results
383
                $verify_item->store();
384
            }
385
            for my $missing_item ( keys %{$expected_items} ) {
386
                my $bundle_item = $expected_items->{$missing_item};
387
                $bundle_item->itemlost($BundleLostValue)->store();
388
                $bundle_item->_result->update_or_create_related(
389
                    'items_lost_issue', { issue_id => $checkout->issue_id } );
390
                push @missing_items, $bundle_item;
391
                if ( C4::Context->preference('WhenLostChargeReplacementFee') ) {
392
                    C4::Accounts::chargelostitem(
393
                        $checkout->borrowernumber,
394
                        $bundle_item->itemnumber,
395
                        $bundle_item->replacementprice,
396
                        sprintf( "%s %s %s",
397
                            $bundle_item->biblio->title  || q{},
398
                            $bundle_item->barcode        || q{},
399
                            $bundle_item->itemcallnumber || q{},
400
                        ),
401
                    );
402
                }
403
            }
404
            $template->param(
405
                unexpected_items => \@unexpected_items,
406
                missing_items    => \@missing_items,
407
                bundle_items     => \@bundle_items
408
            );
409
        }
410
    } elsif ( C4::Context->preference('ShowAllCheckins') and !$messages->{'BadBarcode'} and !$needs_confirm and !$bundle_confirm ) {
349
        $input{duedate}   = 0;
411
        $input{duedate}   = 0;
350
        $returneditems{0} = $barcode;
412
        $returneditems{0} = $barcode;
351
        $riduedate{0}     = 0;
413
        $riduedate{0}     = 0;
Lines 356-361 if ($barcode) { Link Here
356
    if ( $needs_confirm ) {
418
    if ( $needs_confirm ) {
357
        $template->param( needs_confirm => $needs_confirm );
419
        $template->param( needs_confirm => $needs_confirm );
358
    }
420
    }
421
422
    if ( $bundle_confirm ) {
423
        $template->param(
424
            items_bundle_return_confirmation => 1,
425
        );
426
    }
359
}
427
}
360
$template->param( inputloop => \@inputloop );
428
$template->param( inputloop => \@inputloop );
361
429
(-)a/installer/data/mysql/atomicupdate/bug-24023-items-bundles.perl (+55 lines)
Line 0 Link Here
1
$DBversion = 'XXX';
2
if( CheckVersion( $DBversion ) ) {
3
    if( !TableExists( 'item_bundles' ) ) {
4
        $dbh->do(q{
5
            CREATE TABLE `item_bundles` (
6
              `item` int(11) NOT NULL,
7
              `host` int(11) NOT NULL,
8
              PRIMARY KEY (`host`, `item`),
9
              UNIQUE KEY `item_bundles_uniq_1` (`item`),
10
              CONSTRAINT `item_bundles_ibfk_1` FOREIGN KEY (`item`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE,
11
              CONSTRAINT `item_bundles_ibfk_2` FOREIGN KEY (`host`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE
12
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
13
        });
14
    }
15
16
    if( !TableExists( 'items_lost_issue' ) ) {
17
        $dbh->do(q{
18
            CREATE TABLE `items_lost_issue` (
19
              `id` int(11) NOT NULL AUTO_INCREMENT,
20
              `itemnumber` int(11) NOT NULL,
21
              `issue_id` int(11) DEFAULT NULL,
22
              `created_on` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
23
              PRIMARY KEY (`id`),
24
              UNIQUE KEY `itemnumber` (`itemnumber`),
25
              KEY `issue_id` (`issue_id`),
26
              CONSTRAINT `items_lost_issue_ibfk_1` FOREIGN KEY (`itemnumber`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE,
27
              CONSTRAINT `items_lost_issue_ibfk_2` FOREIGN KEY (`issue_id`) REFERENCES `old_issues` (`issue_id`) ON DELETE SET NULL ON UPDATE CASCADE
28
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
29
        });
30
    }
31
32
    my ($lost_val) = $dbh->selectrow_array( "SELECT MAX(authorised_value) FROM authorised_values WHERE category = 'LOST'", {} );
33
    $lost_val++;
34
35
    $dbh->do(qq{
36
       INSERT INTO authorised_values (category,authorised_value,lib) VALUES ('LOST',$lost_val,'Missing from bundle')
37
    });
38
39
    my ($nfl_val) = $dbh->selectrow_array( "SELECT MAX(authorised_value) FROM authorised_values WHERE category = 'NOT_LOAN'", {} );
40
    $nfl_val++;
41
42
    $dbh->do(qq{
43
       INSERT INTO authorised_values (category,authorised_value,lib) VALUES ('NOT_LOAN',$nfl_val,'Added to bundle')
44
    });
45
46
    $dbh->do(qq{
47
        INSERT IGNORE INTO systempreferences( `variable`, `value`, `options`, `explanation`, `type` )
48
        VALUES
49
          ( 'BundleLostValue', $lost_val, '', 'Sets the LOST AV value that represents "Missing from bundle" as a lost value', 'Free' ),
50
          ( 'BundleNotLoanValue', $nfl_val, '', 'Sets the NOT_LOAN AV value that represents "Added to bundle" as a not for loan value', 'Free')
51
    });
52
53
    SetVersion( $DBversion );
54
    print "Upgrade to $DBversion done (Bug 24023 - Items bundles)\n";
55
}
(-)a/installer/data/mysql/kohastructure.sql (+37 lines)
Lines 3063-3068 CREATE TABLE `items` ( Link Here
3063
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3063
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3064
/*!40101 SET character_set_client = @saved_cs_client */;
3064
/*!40101 SET character_set_client = @saved_cs_client */;
3065
3065
3066
--
3067
-- Table structure for table item_bundles
3068
--
3069
3070
DROP TABLE IF EXISTS `item_bundles`;
3071
/*!40101 SET @saved_cs_client     = @@character_set_client */;
3072
/*!40101 SET character_set_client = utf8 */;
3073
CREATE TABLE `item_bundles` (
3074
  `item` int(11) NOT NULL,
3075
  `host` int(11) NOT NULL,
3076
  PRIMARY KEY (`host`, `item`),
3077
  UNIQUE KEY `item_bundles_uniq_1` (`item`),
3078
  CONSTRAINT `item_bundles_ibfk_1` FOREIGN KEY (`item`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE,
3079
  CONSTRAINT `item_bundles_ibfk_2` FOREIGN KEY (`host`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE
3080
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3081
/*!40101 SET character_set_client = @saved_cs_client */;
3082
3066
--
3083
--
3067
-- Table structure for table `items_last_borrower`
3084
-- Table structure for table `items_last_borrower`
3068
--
3085
--
Lines 3083-3088 CREATE TABLE `items_last_borrower` ( Link Here
3083
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3100
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3084
/*!40101 SET character_set_client = @saved_cs_client */;
3101
/*!40101 SET character_set_client = @saved_cs_client */;
3085
3102
3103
--
3104
-- Table structure for table `items_lost_issue`
3105
--
3106
3107
DROP TABLE IF EXISTS `items_lost_issue`;
3108
/*!40101 SET @saved_cs_client     = @@character_set_client */;
3109
/*!40101 SET character_set_client = utf8 */;
3110
CREATE TABLE `items_lost_issue` (
3111
  `id` int(11) NOT NULL AUTO_INCREMENT,
3112
  `itemnumber` int(11) NOT NULL,
3113
  `issue_id` int(11) DEFAULT NULL,
3114
  `created_on` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
3115
  PRIMARY KEY (`id`),
3116
  UNIQUE KEY `itemnumber` (`itemnumber`),
3117
  KEY `issue_id` (`issue_id`),
3118
  CONSTRAINT `items_lost_issue_ibfk_1` FOREIGN KEY (`itemnumber`) REFERENCES `items` (`itemnumber`) ON DELETE CASCADE ON UPDATE CASCADE,
3119
  CONSTRAINT `items_lost_issue_ibfk_2` FOREIGN KEY (`issue_id`) REFERENCES `old_issues` (`issue_id`) ON DELETE SET NULL ON UPDATE CASCADE
3120
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
3121
/*!40101 SET character_set_client = @saved_cs_client */;
3122
3086
--
3123
--
3087
-- Table structure for table `items_search_fields`
3124
-- Table structure for table `items_search_fields`
3088
--
3125
--
(-)a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss (+4 lines)
Lines 2283-2288 td { Link Here
2283
    display: block;
2283
    display: block;
2284
}
2284
}
2285
2285
2286
.bundled {
2287
    display: block;
2288
}
2289
2286
.datedue {
2290
.datedue {
2287
    color: #999;
2291
    color: #999;
2288
    display: block;
2292
    display: block;
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/item-status.inc (+84 lines)
Line 0 Link Here
1
[% USE AuthorisedValues %]
2
[% USE Branches %]
3
[% USE Koha %]
4
[% USE KohaDates %]
5
[% PROCESS 'i18n.inc' %]
6
[% transfer = item.get_transfer() %]
7
[% IF item.checkout %]
8
    <span class="datedue">
9
        [% IF item.checkout.onsite_checkout %]
10
            [% t('Currently in local use by') | html %]
11
        [% ELSE %]
12
            [% t('Checked out to') | html %]
13
        [% END %]
14
        [% INCLUDE 'patron-title.inc' patron=item.checkout.patron hide_patron_infos_if_needed=1 %]
15
        : [% tx('due {date_due}', { date_due = item.checkout.date_due }) | html %]
16
    </span>
17
[% ELSIF transfer %]
18
    [% datesent = BLOCK %][% transfer.datesent | $KohaDates %][% END %]
19
    <span class="intransit">[% tx('In transit from {frombranch} to {tobranch} since {datesent}', { frombranch = Branches.GetName(transfer.frombranch), tobranch = Branches.GetName(transfer.tobranch), datesent = datesent }) %]</span>
20
[% END %]
21
22
[% IF item.itemlost %]
23
    [% itemlost_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.itemlost', authorised_value = item.itemlost }) %]
24
    [% IF itemlost_description %]
25
        <span class="lost">[% itemlost_description | html %]</span>
26
    [% ELSE %]
27
        <span class="lost">[% t('Unavailable (lost or missing)') | html %]</span>
28
    [% END %]
29
[% END %]
30
31
[% IF item.withdrawn %]
32
    [% withdrawn_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.withdrawn', authorised_value = item.withdrawn }) %]
33
    [% IF withdrawn_description %]
34
        <span class="wdn">[% withdrawn_description | html %]</span>
35
    [% ELSE %]
36
        <span class="wdn">[% t('Withdrawn') | html %]</span>
37
    [% END %]
38
[% END %]
39
40
[% IF item.damaged %]
41
    [% damaged_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.damaged', authorised_value = item.damaged }) %]
42
    [% IF damaged_description %]
43
        <span class="dmg">[% damaged_description | html %]</span>
44
    [% ELSE %]
45
        <span class="dmg">[% t('Damaged') | html %]</span>
46
    [% END %]
47
[% END %]
48
49
[% IF item.notforloan || item.effective_itemtype.notforloan %]
50
    <span>
51
        [% t('Not for loan') | html %]
52
        [% notforloan_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.notforloan', authorised_value = item.notforloan }) %]
53
        [% IF notforloan_description %]
54
            ([% notforloan_description | html %])
55
        [% END %]
56
    </span>
57
[% END %]
58
59
[% hold = item.holds.next %]
60
[% IF hold %]
61
    <span>
62
        [% IF hold.waitingdate %]
63
            Waiting at [% Branches.GetName(hold.get_column('branchcode')) | html %] since [% hold.waitingdate | $KohaDates %].
64
        [% ELSE %]
65
            Item-level hold (placed [% hold.reservedate | $KohaDates %]) for delivery at [% Branches.GetName(hold.get_column('branchcode')) | html %].
66
        [% END %]
67
        [% IF Koha.Preference('canreservefromotherbranches') %]
68
            [% t('Hold for:') | html %]
69
            [% INCLUDE 'patron-title.inc' patron=hold.borrower hide_patron_infos_if_needed=1 %]
70
        [% END %]
71
    </span>
72
[% END %]
73
[% UNLESS item.notforloan || item.effective_itemtype.notforloan || item.onloan || item.itemlost || item.withdrawn || item.damaged || transfer || hold %]
74
    <span>[% t('Available') | html %]</span>
75
[% END %]
76
77
[% IF ( item.restricted ) %]
78
    [% restricted_description = AuthorisedValues.GetDescriptionByKohaField({ kohafield = 'items.restricted', authorised_value = item.restricted }) %]
79
    [% IF restricted_description %]
80
        <span class="restricted">([% restricted_description | html %])</span>
81
    [% ELSE %]
82
        <span class="restricted">[% t('Restricted') | html %]</span>
83
    [% END %]
84
[% END %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/bundle_contents.inc (+35 lines)
Line 0 Link Here
1
<!-- Bundle contents modal -->
2
<div class="modal printable" id="bundleContentsModal" tabindex="-1" role="dialog" aria-labelledby="bundleContentsLabel">
3
    <div class="modal-dialog" role="document">
4
        <div class="modal-content">
5
            <div class="modal-header">
6
                <button type="button" class="closebtn" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
7
                <h4 class="modal-title" id="bundleContentsLabel">Bundle contents</h4>
8
            </div>
9
            <div class="modal-body">
10
                <table style="width:100%">
11
                    <thead>
12
                        <tr>
13
                            <th>Barcode</th>
14
                            <th>Title</th>
15
                        </tr>
16
                    </thead>
17
                    <tbody>
18
                    [% FOREACH bundle_item IN bundle_items %]
19
                        <tr>
20
                            <td>[% bundle_item.barcode | html %]</td>
21
                            <td>[% INCLUDE 'biblio-title.inc' biblio=bundle_item.biblio %]</td>
22
                        </tr>
23
                    [% END %]
24
                    </tbody>
25
                    <tfoot>
26
                    </tfoot>
27
                </table>
28
            </div> <!-- /.modal-body -->
29
            <div class="modal-footer">
30
                <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
31
                <button type="button" class="printModal btn btn-primary"><i class="fa fa-print"></i> Print</button>
32
            </div> <!-- /.modal-footer -->
33
        </div> <!-- /.modal-content -->
34
    </div> <!-- /.modal-dialog -->
35
</div> <!-- /#bundleContentsModal -->
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref (+4 lines)
Lines 158-163 Cataloging: Link Here
158
            - and record's last modifier name in MARC subfield
158
            - and record's last modifier name in MARC subfield
159
            - pref: MarcFieldForModifierName
159
            - pref: MarcFieldForModifierName
160
            - ". <br/><strong>NOTE:</strong> Use a dollar sign between field and subfield like 123$a."
160
            - ". <br/><strong>NOTE:</strong> Use a dollar sign between field and subfield like 123$a."
161
        -
162
            - Use the NOT_LOAN authorised value
163
            - pref: BundleNotLoanValue
164
            - to represent 'added to bundle' when an item is attached to bundle.
161
    Display:
165
    Display:
162
        -
166
        -
163
            - 'Separate main entry and subdivisions with '
167
            - 'Separate main entry and subdivisions with '
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref (+4 lines)
Lines 12-17 Circulation: Link Here
12
                  1: "Require"
12
                  1: "Require"
13
                  0: "Don't require"
13
                  0: "Don't require"
14
            - staff to confirm that all parts of an item are present at checkin/checkout.
14
            - staff to confirm that all parts of an item are present at checkin/checkout.
15
        -
16
            - Use the LOST authorised value
17
            - pref: BundleLostValue
18
            - to represent 'missing from bundle' at return.
15
        -
19
        -
16
            - pref: AutoSwitchPatron
20
            - pref: AutoSwitchPatron
17
              choices:
21
              choices:
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt (-1 / +209 lines)
Lines 315-320 Link Here
315
    <table class="items_table" id="[% tab | html %]_table">
315
    <table class="items_table" id="[% tab | html %]_table">
316
        <thead>
316
        <thead>
317
            <tr>
317
            <tr>
318
                [% IF ( itemdata_bundles ) %]<th id="[% tab | html %]_bundles" data-colname="[% tab | html %]_bundles" class="NoSort"></th>[% END %]
318
                [% IF (StaffDetailItemSelection) %]<th id="[% tab | html %]_checkbox" data-colname="[% tab | html %]_checkbox" class="NoSort"></th>[% END %]
319
                [% IF (StaffDetailItemSelection) %]<th id="[% tab | html %]_checkbox" data-colname="[% tab | html %]_checkbox" class="NoSort"></th>[% END %]
319
                [% IF Koha.Preference('LocalCoverImages') && ( tab == 'holdings' && itemloop_has_images || tab == 'otherholdings' && otheritemloop_has_images ) %]
320
                [% IF Koha.Preference('LocalCoverImages') && ( tab == 'holdings' && itemloop_has_images || tab == 'otherholdings' && otheritemloop_has_images ) %]
320
                    <th id="[% tab | html %]_cover_image" data-colname="[% tab | html %]_cover_image">Cover image</th>
321
                    <th id="[% tab | html %]_cover_image" data-colname="[% tab | html %]_cover_image">Cover image</th>
Lines 347-353 Link Here
347
        </thead>
348
        </thead>
348
        <tbody>
349
        <tbody>
349
            [% FOREACH item IN items %]
350
            [% FOREACH item IN items %]
350
                <tr>
351
                <tr id="item_[% item.itemnumber | html %]" data-itemnumber="[% item.itemnumber | html %]">
352
                [% IF ( itemdata_bundles ) %]
353
                    [% IF ( item.is_bundle ) %]
354
                    <td class="details-control">
355
                        <button><i class="fa fa-folder-open"></i></button>
356
                    </td>
357
                    [% ELSE %]
358
                    <td></td>
359
                    [% END %]
360
                [% END %]
351
                [% IF (StaffDetailItemSelection) %]
361
                [% IF (StaffDetailItemSelection) %]
352
                    <td style="text-align:center;vertical-align:middle">
362
                    <td style="text-align:center;vertical-align:middle">
353
                        <input type="checkbox" value="[% item.itemnumber | html %]" name="itemnumber" />
363
                        <input type="checkbox" value="[% item.itemnumber | html %]" name="itemnumber" />
Lines 455-460 Note that permanent location is a code, and location may be an authval. Link Here
455
                            [% ELSE %]
465
                            [% ELSE %]
456
                                <span class="lost">Unavailable (lost or missing)</span>
466
                                <span class="lost">Unavailable (lost or missing)</span>
457
                            [% END %]
467
                            [% END %]
468
                            [% IF ( item.itemlost == Koha.Preference('BundleLostValue') AND item.loss_checkout ) %]
469
                                <span>[% item.loss_checkout.borrowernumber | html %]</span>
470
                            [% END %]
458
                        [% END %]
471
                        [% END %]
459
472
460
                        [% IF ( item.withdrawn ) %]
473
                        [% IF ( item.withdrawn ) %]
Lines 509-514 Note that permanent location is a code, and location may be an authval. Link Here
509
                            <span class="restricted">([% item.restrictedvalue | html %])</span>
522
                            <span class="restricted">([% item.restrictedvalue | html %])</span>
510
                        [% END %]
523
                        [% END %]
511
524
525
                        [% IF ( item.bundle_host ) %]
526
                            <span class="bundled">In bundle: [% INCLUDE 'biblio-title.inc' biblio = item.bundle_host.biblio link = 1 %]</span>
527
                        [% END %]
528
512
                    </td>
529
                    </td>
513
                    <td class="datelastseen" data-order="[% item.datelastseen | html %]">[% item.datelastseen | $KohaDates %]</td>
530
                    <td class="datelastseen" data-order="[% item.datelastseen | html %]">[% item.datelastseen | $KohaDates %]</td>
514
                    <td class="dateaccessioned" data-order="[% item.dateaccessioned | html %]">[% item.dateaccessioned | $KohaDates %]</td>
531
                    <td class="dateaccessioned" data-order="[% item.dateaccessioned | html %]">[% item.dateaccessioned | $KohaDates %]</td>
Lines 593-598 Note that permanent location is a code, and location may be an authval. Link Here
593
                                <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>
610
                                <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>
594
                            [% END %]
611
                            [% END %]
595
                        [% END %]
612
                        [% END %]
613
                        [% IF collection %]
614
                        <button type="button" data-toggle="modal" data-target="#bundleItemsModal" data-item="[% item.itemnumber | html %]" class="btn btn-default btn-xs"><i class="fa fa-folder"></i> Bundle</button>
615
                        [% END %]
596
                    </td>
616
                    </td>
597
                [% END %]
617
                [% END %]
598
                </tr>
618
                </tr>
Lines 1020-1025 Note that permanent location is a code, and location may be an authval. Link Here
1020
1040
1021
[% END %]
1041
[% END %]
1022
1042
1043
    <div class="modal" id="bundleItemsModal" tabindex="-1" role="dialog" aria-labelledby="bundleItemsLabel">
1044
        <form id="bundleItemsForm" action="">
1045
            <div class="modal-dialog" role="document">
1046
                <div class="modal-content">
1047
                    <div class="modal-header">
1048
                        <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
1049
                        <h3 id="bundleItemsLabel">Add to bundle</h3>
1050
                    </div>
1051
                    <div class="modal-body">
1052
                        <div id="result"></div>
1053
                        <fieldset class="rows">
1054
                            <ol>
1055
                                <li>
1056
                                    <label class="required" for="external_id">Item barcode: </label>
1057
                                    <input type="text" id="external_id" name="external_id" required="required">
1058
                                    <span class="required">Required</span>
1059
                                </li>
1060
                            </ol>
1061
                        </fieldset>
1062
                    </div>
1063
                    <div class="modal-footer">
1064
                        <button type="submit" class="btn btn-default">Submit</button>
1065
                        <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
1066
                    </div>
1067
                </div>
1068
            </div>
1069
        </form>
1070
    </div>
1071
1023
[% MACRO jsinclude BLOCK %]
1072
[% MACRO jsinclude BLOCK %]
1024
    [% INCLUDE 'catalog-strings.inc' %]
1073
    [% INCLUDE 'catalog-strings.inc' %]
1025
    [% Asset.js("js/catalog.js") | $raw %]
1074
    [% Asset.js("js/catalog.js") | $raw %]
Lines 1315-1320 Note that permanent location is a code, and location may be an authval. Link Here
1315
    [% INCLUDE 'datatables.inc' %]
1364
    [% INCLUDE 'datatables.inc' %]
1316
    [% Asset.js("lib/jquery/plugins/jquery.dataTables.columnFilter.js") | $raw %]
1365
    [% Asset.js("lib/jquery/plugins/jquery.dataTables.columnFilter.js") | $raw %]
1317
    [% INCLUDE 'columns_settings.inc' %]
1366
    [% INCLUDE 'columns_settings.inc' %]
1367
    [% INCLUDE 'js-date-format.inc' %]
1318
    [% Asset.js("js/browser.js") | $raw %]
1368
    [% Asset.js("js/browser.js") | $raw %]
1319
    [% Asset.js("js/table_filters.js") | $raw %]
1369
    [% Asset.js("js/table_filters.js") | $raw %]
1320
    <script>
1370
    <script>
Lines 1323-1328 Note that permanent location is a code, and location may be an authval. Link Here
1323
        browser.show();
1373
        browser.show();
1324
1374
1325
        $(document).ready(function() {
1375
        $(document).ready(function() {
1376
1377
            function createChild ( row, itemnumber ) {
1378
                // This is the table we'll convert into a DataTable
1379
                var bundles_table = $('<table class="display" width="100%"/>');
1380
1381
                // Display it the child row
1382
                row.child( bundles_table ).show();
1383
1384
                // Initialise as a DataTable
1385
                var bundle_table_url = "/api/v1/items/" + itemnumber + "/bundled_items?";
1386
                var bundle_table = bundles_table.api({
1387
                    "ajax": {
1388
                        "url": bundle_table_url
1389
                    },
1390
                    "header_filter": false,
1391
                    "embed": [
1392
                        "biblio",
1393
                        "checkout"
1394
                    ],
1395
                    "order": [[ 1, "asc" ]],
1396
                    "columns": [
1397
                        {
1398
                            "data": "biblio.title:biblio.medium",
1399
                            "title": "Title",
1400
                            "searchable": true,
1401
                            "orderable": true,
1402
                            "render": function(data, type, row, meta) {
1403
                                var title = "";
1404
                                if ( row.biblio.title ) {
1405
                                    title = title.concat('<span class="biblio-title">',row.biblio.title,'</span>');
1406
                                }
1407
                                if ( row.biblio.medium ) {
1408
                                    title = title.concat('<span class="biblio-medium">',row.biblio.medium,'</span>');
1409
                                }
1410
                                return title;
1411
                            }
1412
                        },
1413
                        {
1414
                            "data": "biblio.author",
1415
                            "title": "Author",
1416
                            "searchable": true,
1417
                            "orderable": true,
1418
                        },
1419
                        {
1420
                            "data": "ccode",
1421
                            "title": "Collection code",
1422
                            "searchable": true,
1423
                            "orderable": true,
1424
                        },
1425
                        {
1426
                            "data": "item_type",
1427
                            "title": "Item Type",
1428
                            "searchable": false,
1429
                            "orderable": true,
1430
                        },
1431
                        {
1432
                            "data": "callnumber",
1433
                            "title": "Callnumber",
1434
                            "searchable": true,
1435
                            "orderable": true,
1436
                        },
1437
                        {
1438
                            "data": "external_id",
1439
                            "title": "Barcode",
1440
                            "searchable": true,
1441
                            "orderable": true,
1442
                        },
1443
                        {
1444
                            "data": "lost_status:last_seen_date",
1445
                            "title": "Status",
1446
                            "searchable": false,
1447
                            "orderable": true,
1448
                            "render": function(data, type, row, meta) {
1449
                                if ( row.lost_status ) {
1450
                                    return "Lost: " + row.lost_status;
1451
                                }
1452
                                return "Available";
1453
                            }
1454
                        },
1455
                        {
1456
                            "data": function( row, type, val, meta ) {
1457
                                var result = '<a class="btn btn-default btn-xs" role="button" href="#removeFromBundleConfirmModal"><i class="fa fa-trash" aria-hidden="true"></i> '+_("Remove")+'</a>\n';
1458
                                return result;
1459
                            },
1460
                            "title": "Actions",
1461
                            "searchable": false,
1462
                            "orderable": false
1463
                        }
1464
                    ]
1465
                }, [], 1);
1466
1467
                return;
1468
            }
1469
1326
            var ids = ['holdings_table', 'otherholdings_table'];
1470
            var ids = ['holdings_table', 'otherholdings_table'];
1327
            var columns_settings = [ [% TablesSettings.GetColumns('catalogue', 'detail','holdings_table','json') | $raw %], [% TablesSettings.GetColumns('catalogue', 'detail','otherholdings_table','json')  | $raw %] ];
1471
            var columns_settings = [ [% TablesSettings.GetColumns('catalogue', 'detail','holdings_table','json') | $raw %], [% TablesSettings.GetColumns('catalogue', 'detail','otherholdings_table','json')  | $raw %] ];
1328
            var has_images = ["[% itemloop_has_images | html %]", "[% otheritemloop_has_images | html %]"];
1472
            var has_images = ["[% itemloop_has_images | html %]", "[% otheritemloop_has_images | html %]"];
Lines 1339-1344 Note that permanent location is a code, and location may be an authval. Link Here
1339
                    "sDom": 'C<"top pager"ilpfB><"#filter_c">tr<"bottom pager"ip>',
1483
                    "sDom": 'C<"top pager"ilpfB><"#filter_c">tr<"bottom pager"ip>',
1340
                };
1484
                };
1341
                var table = KohaTable(id, dt_parameters, columns_settings[i], 'with_filters');
1485
                var table = KohaTable(id, dt_parameters, columns_settings[i], 'with_filters');
1486
1487
                // Add event listener for opening and closing details
1488
                $('#' + id + ' tbody').on('click', 'td.details-control', function () {
1489
                    var tr = $(this).closest('tr');
1490
                    var dTable = $(this).closest('table').DataTable({ 'retrieve': true });
1491
1492
                    var itemnumber = tr.data('itemnumber');
1493
                    var row = dTable.row( tr );
1494
1495
                    if ( row.child.isShown() ) {
1496
                        // This row is already open - close it
1497
                        row.child.hide();
1498
                        tr.removeClass('shown');
1499
                    }
1500
                    else {
1501
                        // Open this row
1502
                        createChild(row, itemnumber);
1503
                        tr.addClass('shown');
1504
                    }
1505
                } );
1342
            }
1506
            }
1343
1507
1344
            [% IF Koha.Preference('AcquisitionDetails') %]
1508
            [% IF Koha.Preference('AcquisitionDetails') %]
Lines 1360-1365 Note that permanent location is a code, and location may be an authval. Link Here
1360
                    "sPaginationType": "full"
1524
                    "sPaginationType": "full"
1361
                }));
1525
                }));
1362
            [% END %]
1526
            [% END %]
1527
1528
            $("#bundleItemsModal").on("shown.bs.modal", function(e){
1529
                var button = $(e.relatedTarget);
1530
                var item_id = button.data('item');
1531
                $("#result").replaceWith('<div id="result"></div>');
1532
                $("#bundleItemsForm").attr('action', '/api/v1/items/' + item_id + '/bundled_items/item');
1533
                $("#external_id").focus();
1534
            });
1535
1536
            $("#bundleItemsForm").submit(function(event) {
1537
1538
                  /* stop form from submitting normally */
1539
                  event.preventDefault();
1540
1541
                  /* get the action attribute from the <form action=""> element */
1542
                  var $form = $(this),
1543
                  url = $form.attr('action');
1544
1545
                  /* Send the data using post with external_id */
1546
                  var posting = $.post(url, JSON.stringify({
1547
                      external_id: $('#external_id').val()
1548
                  }), null, "json");
1549
1550
                  /* Report the results */
1551
                  posting.done(function(data) {
1552
                      var barcode = $('#external_id').val();
1553
                      $('#result').replaceWith('<div id="result" class="alert alert-success">Success: Added '+barcode+'</div>');
1554
                      $('#external_id').val('').focus();
1555
                  });
1556
                  posting.fail(function(data) {
1557
                      var barcode = $('#external_id').val();
1558
                      if ( data.status === 409 ) {
1559
                          var response = data.responseJSON;
1560
                          if ( response.key === "PRIMARY" ) {
1561
                              $('#result').replaceWith('<div id="result" class="alert alert-warning">Warning: Item '+barcode+' already attached</div>');
1562
                          } else {
1563
                              $('#result').replaceWith('<div id="result" class="alert alert-danger">Failure: Item '+barcode+' belongs to another bundle</div>');
1564
                          }
1565
                      } else {
1566
                          $('#result').replaceWith('<div id="result" class="alert alert-danger">Failure</div>');
1567
                      }
1568
                      $('#external_id').val('').focus();
1569
                  });
1570
            });
1363
        });
1571
        });
1364
1572
1365
        $(document).ready(function() {
1573
        $(document).ready(function() {
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt (+117 lines)
Lines 6-11 Link Here
6
[% USE ItemTypes %]
6
[% USE ItemTypes %]
7
[% USE AuthorisedValues %]
7
[% USE AuthorisedValues %]
8
[% USE TablesSettings %]
8
[% USE TablesSettings %]
9
[% PROCESS 'i18n.inc' %]
9
[% PROCESS 'member-display-address-style.inc' %]
10
[% PROCESS 'member-display-address-style.inc' %]
10
[% SET footerjs = 1 %]
11
[% SET footerjs = 1 %]
11
[% BLOCK display_bormessagepref %]
12
[% BLOCK display_bormessagepref %]
Lines 217-222 Link Here
217
                                </div>
218
                                </div>
218
                            [% END %]
219
                            [% END %]
219
220
221
                            <!-- Bundle has items missing -->
222
                            [% IF missing_items %]
223
                                <div id="bundle_missing_items" class="dialog alert">
224
                                    <h3>Bundle had missing items</h3>
225
                                    <p>Print new contents list
226
                                    <a class="btn btn-default btn-xs" role="button" data-toggle="modal" href="#bundleContentsModal"><i class="fa fa-glass" aria-hidden="true"></i> Show</a></p>
227
                                </div>
228
                            [% END %]
229
230
                            <!-- Bundle contained unexpected items -->
231
                            [% IF unexpected_items %]
232
                                <div id="bundle_unexpected_items" class="dialog alert">
233
                                    <h3>Bundle had unexpected items</h3>
234
                                    <p>Please place the following items to one side</p>
235
                                    <ul>
236
                                    [% FOREACH unexpected_item IN unexpected_items %]
237
                                        <li>[% INCLUDE 'biblio-title.inc' biblio=unexpected_item.biblio %] - [% unexpected_item.barcode | html %]</li>
238
                                    [% END %]
239
                                    </ul>
240
                                </div>
241
                            [% END %]
220
242
221
                            [% IF ( errmsgloop ) %]
243
                            [% IF ( errmsgloop ) %]
222
                                <div class="dialog alert audio-alert-warning">
244
                                <div class="dialog alert audio-alert-warning">
Lines 381-386 Link Here
381
                            </div>
403
                            </div>
382
                        [% END %]
404
                        [% END %]
383
405
406
                        [% IF items_bundle_return_confirmation %]
407
                        <div id="bundle-needsconfirmation-modal" class="modal fade audio-alert-action block">
408
                            <div class="modal-dialog modal-wide">
409
                                <div class="modal-content">
410
                                    <form method="post">
411
                                        <div class="modal-header">
412
                                            <h3>Please confirm bundle contents</h3>
413
                                        </div>
414
                                        <div class="modal-body">
415
416
                                            <table class="table table-condensed table-bordered" id="items-bundle-contents-table">
417
                                                <thead>
418
                                                    <tr>
419
                                                        <th>[% t('Title') | html %]</th>
420
                                                        <th>[% t('Author') | html %]</th>
421
                                                        <th>[% t('Collection code') | html %]</th>
422
                                                        <th>[% t('Item type') | html %]</th>
423
                                                        <th>[% t('Callnumber') | html %]</th>
424
                                                        <th>[% t('Barcode') | html %]</th>
425
                                                        <th>[% t('Status') | html %]</th>
426
                                                    </tr>
427
                                                </thead>
428
                                                <tbody>
429
                                                    [% FOREACH bundle_item IN item.bundle_items %]
430
                                                    <tr data-barcode="[% bundle_item.barcode | html %]">
431
                                                        <td>[% INCLUDE 'biblio-title.inc' biblio=bundle_item.biblio link = 1 %]</td>
432
                                                        <td>[% bundle_item.biblio.author | html %]</td>
433
                                                        <td>[% AuthorisedValues.GetByCode('CCODE', bundle_item.ccode) | html %]</td>
434
                                                        <td>[% ItemTypes.GetDescription(bundle_item.itype) | html %]</td>
435
                                                        <td>[% bundle_item.itemcallnumber | html %]</td>
436
                                                        <td>[% bundle_item.barcode | html %]</td>
437
                                                        <td>[% INCLUDE 'item-status.inc' item=bundle_item %]</td>
438
                                                    </tr>
439
                                                    [% END %]
440
                                                </tbody>
441
                                            </table>
442
    
443
                                            <div class="form-group">
444
                                                <label for="verify-items-bundle-contents-barcodes">Barcodes</label>
445
                                                <textarea autocomplete="off" id="verify-items-bundle-contents-barcodes" name="verify-items-bundle-contents-barcodes" class="form-control"></textarea>
446
                                                <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>
447
                                            </div>
448
449
                                        </div>
450
                                        <div class="modal-footer">
451
                                            <input type="hidden" name="barcode" value="[% item.barcode | html %]">
452
                                            <input type="hidden" name="confirm_items_bundle_return" value="1">
453
                                            [% FOREACH inputloo IN inputloop %]
454
                                            <input type="hidden" name="ri-[% inputloo.counter | html %]" value="[% inputloo.barcode | html %]" />
455
                                            <input type="hidden" name="dd-[% inputloo.counter | html %]" value="[% inputloo.duedate | html %]" />
456
                                            <input type="hidden" name="bn-[% inputloo.counter | html %]" value="[% inputloo.borrowernumber | html %]" />
457
                                            [% END %]
458
                                            <button type="submit" class="btn btn-default"><i class="fa fa-check"></i> [% t('Confirm checkin and mark missing items as lost') | html %]</button>
459
                                            <button type="button" data-dismiss="modal" class="btn btn-default"><i class="fa fa-close"></i> [% t('Cancel') | html %]</button>
460
                                        </div>
461
                                    </form>
462
                                </div>
463
                            </div>
464
                        </div>
465
                        [% END %]
466
384
                        [% IF wrongbranch %]
467
                        [% IF wrongbranch %]
385
                            <div id="wrong-branch-modal" class="modal fade audio-alert-action block">
468
                            <div id="wrong-branch-modal" class="modal fade audio-alert-action block">
386
                                <div class="modal-dialog">
469
                                <div class="modal-dialog">
Lines 986-991 Link Here
986
            </div> <!-- /.col-sm-12 -->
1069
            </div> <!-- /.col-sm-12 -->
987
        </div> <!-- /.row -->
1070
        </div> <!-- /.row -->
988
1071
1072
    [% INCLUDE 'modals/bundle_contents.inc' %]
1073
989
[% MACRO jsinclude BLOCK %]
1074
[% MACRO jsinclude BLOCK %]
990
    [% INCLUDE 'datatables.inc' %]
1075
    [% INCLUDE 'datatables.inc' %]
991
    [% INCLUDE 'columns_settings.inc' %]
1076
    [% INCLUDE 'columns_settings.inc' %]
Lines 1190-1195 Link Here
1190
1275
1191
        });
1276
        });
1192
    </script>
1277
    </script>
1278
1279
    <script>
1280
        $(document).ready(function () {
1281
            $('#verify-items-bundle-contents-barcodes').on('input', function (ev) {
1282
                const barcodes = ev.target.value.split('\n').map(function(s) { return s.trim() });
1283
                $('#items-bundle-contents-table tr').each(function () {
1284
                    const barcode = this.getAttribute('data-barcode');
1285
                    if (barcodes.includes(barcode)) {
1286
                        this.classList.add('ok');
1287
                    } else {
1288
                        this.classList.remove('ok');
1289
                    }
1290
                })
1291
            });
1292
            
1293
            $('.modal.printable').on('shown.bs.modal', function() {
1294
                $('.modal-dialog', this).addClass('focused');
1295
                $('body').addClass('modalprinter');
1296
1297
                if ($(this).hasClass('autoprint')) {
1298
                    window.print();
1299
                }
1300
            }).on('hidden.bs.modal', function() {
1301
                $('.modal-dialog', this).removeClass('focused');
1302
                $('body').removeClass('modalprinter');
1303
            });
1304
1305
            $('.printModal').click(function() {
1306
                window.print();
1307
            });
1308
        });
1309
    </script>
1193
[% END %]
1310
[% END %]
1194
1311
1195
[% INCLUDE 'intranet-bottom.inc' %]
1312
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/inventory.tt (-1 / +19 lines)
Lines 133-138 Link Here
133
            [% END %]
133
            [% END %]
134
            </select>
134
            </select>
135
          </li>
135
          </li>
136
        [% IF items_bundles.size > 0 %]
137
            <li>
138
                <label for="items_bundle">Items bundle:</label>
139
                <select id="items_bundle" name="items_bundle">
140
                    <option value=""></option>
141
                    [% FOREACH items_bundle IN items_bundles %]
142
                        <option value="[% items_bundle.items_bundle_id | html %]">[% items_bundle.biblionumber.title | html %]</option>
143
                    [% END %]
144
                </select>
145
            </li>
146
        [% END %]
136
    </ol>
147
    </ol>
137
    </fieldset>
148
    </fieldset>
138
149
Lines 348-354 Link Here
348
                    $('#locationloop').val() ||
359
                    $('#locationloop').val() ||
349
                    $('#minlocation').val()  ||
360
                    $('#minlocation').val()  ||
350
                    $('#maxlocation').val()  ||
361
                    $('#maxlocation').val()  ||
351
                    $('#statuses input:checked').length
362
                    $('#statuses input:checked').length ||
363
                    $('#items_bundle').val()
352
                ) ) {
364
                ) ) {
353
                    return confirm(
365
                    return confirm(
354
                        _("You have not selected any catalog filters and are about to compare a file of barcodes to your entire catalog.") + "\n\n" +
366
                        _("You have not selected any catalog filters and are about to compare a file of barcodes to your entire catalog.") + "\n\n" +
Lines 488-493 Link Here
488
            });
500
            });
489
        });
501
        });
490
    </script>
502
    </script>
503
    [% INCLUDE 'select2.inc' %]
504
    <script>
505
        $(document).ready(function () {
506
            $('#items_bundle').select2();
507
        });
508
    </script>
491
[% END %]
509
[% END %]
492
510
493
[% INCLUDE 'intranet-bottom.inc' %]
511
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-detail.tt (-2 / +90 lines)
Lines 1066-1072 Link Here
1066
        <caption class="sr-only">Holdings</caption>
1066
        <caption class="sr-only">Holdings</caption>
1067
        <thead>
1067
        <thead>
1068
            <tr>
1068
            <tr>
1069
1069
                [% IF ( itemdata_bundles ) %]
1070
                    <th id="item_bundle" data-colname="item_bundle"></th>
1071
                [% END %]
1070
                [% IF Koha.Preference('OPACLocalCoverImages') && ( tab == 'holdings' && itemloop_has_images || tab == 'otherholdings' && otheritemloop_has_images ) %]
1072
                [% IF Koha.Preference('OPACLocalCoverImages') && ( tab == 'holdings' && itemloop_has_images || tab == 'otherholdings' && otheritemloop_has_images ) %]
1071
                    <th id="item_cover" data-colname="item_cover">Cover image</th>
1073
                    <th id="item_cover" data-colname="item_cover">Cover image</th>
1072
                [% END %]
1074
                [% END %]
Lines 1116-1122 Link Here
1116
        </thead>
1118
        </thead>
1117
        <tbody>
1119
        <tbody>
1118
            [% FOREACH ITEM_RESULT IN items %]
1120
            [% FOREACH ITEM_RESULT IN items %]
1119
                <tr vocab="http://schema.org/" typeof="Offer">
1121
                <tr vocab="http://schema.org/" typeof="Offer" data-itemnumber="[% ITEM_RESULT.itemnumber | html %]">
1122
1123
                [% IF ( itemdata_bundles ) %]
1124
                    [% IF ITEM_RESULT.is_bundle %]
1125
                    <td class="details-control">
1126
                        <button><i class="fa fa-folder-open"></i></button>
1127
                    </td>
1128
                    [% ELSE %]
1129
                    <td></td>
1130
                    [% END %]
1131
                [% END %]
1120
1132
1121
                [% IF Koha.Preference('OPACLocalCoverImages') && ( tab == 'holdings' && itemloop_has_images || tab == 'otherholdings' && otheritemloop_has_images ) %]
1133
                [% IF Koha.Preference('OPACLocalCoverImages') && ( tab == 'holdings' && itemloop_has_images || tab == 'otherholdings' && otheritemloop_has_images ) %]
1122
                    <td class="cover">
1134
                    <td class="cover">
Lines 1419-1424 Link Here
1419
                "autoWidth": false
1431
                "autoWidth": false
1420
            }, columns_settings);
1432
            }, columns_settings);
1421
1433
1434
            function createChild ( row, itemnumber ) {
1435
                // This is the table we'll convert into a DataTable
1436
                var bundles_table = $('<table class="display" width="100%"/>');
1437
1438
                // Display it the child row
1439
                row.child( bundles_table ).show();
1440
1441
                // Initialise as a DataTable
1442
                var bundle_table_url = "/api/v1/items/" + itemnumber + "/bundled_items?";
1443
                var bundle_table = bundles_table.api({
1444
                    "ajax": {
1445
                        "url": bundle_table_url
1446
                    },
1447
                    "header_filter": false,
1448
                    "embed": [
1449
                        "biblio",
1450
                        "checkout"
1451
                    ],
1452
                    "order": [[ 0, "asc" ]],
1453
                    "columns": [
1454
                        {
1455
                            "data": "item_type",
1456
                            "title": "Item Type",
1457
                            "searchable": false,
1458
                            "orderable": true,
1459
                        },
1460
                        {
1461
                            "data": "biblio.title",
1462
                            "title": "Title",
1463
                            "searchable": true,
1464
                            "orderable": true,
1465
                        },
1466
                        {
1467
                            "data": "damaged_status",
1468
                            "title": "Status",
1469
                            "searchable": false,
1470
                            "orderable": true,
1471
                        },
1472
                        {
1473
                            "data": "external_id",
1474
                            "title": "Barcode",
1475
                            "searchable": true,
1476
                            "orderable": true,
1477
                        },
1478
                        {
1479
                            "data": "callnumber",
1480
                            "title": "Callnumber",
1481
                            "searchable": true,
1482
                            "orderable": true,
1483
                        },
1484
                    ]
1485
                }, [], 1);
1486
1487
                return;
1488
            }
1489
1490
            // Add event listener for opening and closing details
1491
            $('#holdingst tbody').on('click', 'td.details-control', function () {
1492
                var tr = $(this).closest('tr');
1493
                var dTable = $(this).closest('table').DataTable({ 'retrieve': true });
1494
1495
                var itemnumber = tr.data('itemnumber');
1496
                var row = dTable.row( tr );
1497
1498
                if ( row.child.isShown() ) {
1499
                    // This row is already open - close it
1500
                    row.child.hide();
1501
                    tr.removeClass('shown');
1502
                }
1503
                else {
1504
                    // Open this row
1505
                    createChild(row, itemnumber);
1506
                    tr.addClass('shown');
1507
                }
1508
            } );
1509
1422
            var serial_column_settings = [% TablesSettings.GetColumns( 'opac', 'biblio-detail', 'subscriptionst', 'json' ) | $raw %];
1510
            var serial_column_settings = [% TablesSettings.GetColumns( 'opac', 'biblio-detail', 'subscriptionst', 'json' ) | $raw %];
1423
1511
1424
            KohaTable("#subscriptionst", {
1512
            KohaTable("#subscriptionst", {
(-)a/opac/opac-detail.pl (+10 lines)
Lines 751-756 if ( not $viewallitems and @items > $max_items_to_display ) { Link Here
751
        $itm->{cover_images} = $item->cover_images;
751
        $itm->{cover_images} = $item->cover_images;
752
    }
752
    }
753
753
754
    if ($item->is_bundle) {
755
        $itemfields{bundles} = 1;
756
        $itm->{is_bundle} = 1;
757
    }
758
759
    if ($item->in_bundle) {
760
        $itm->{bundle_host} = $item->bundle_host;
761
    }
762
754
    my $itembranch = $itm->{$separatebranch};
763
    my $itembranch = $itm->{$separatebranch};
755
    if ($currentbranch and C4::Context->preference('OpacSeparateHoldings')) {
764
    if ($currentbranch and C4::Context->preference('OpacSeparateHoldings')) {
756
        if ($itembranch and $itembranch eq $currentbranch) {
765
        if ($itembranch and $itembranch eq $currentbranch) {
Lines 801-806 if( C4::Context->preference('ArticleRequests') ) { Link Here
801
                     MARCNOTES               => $marcnotesarray,
810
                     MARCNOTES               => $marcnotesarray,
802
                     norequests              => $norequests,
811
                     norequests              => $norequests,
803
                     RequestOnOpac           => C4::Context->preference("RequestOnOpac"),
812
                     RequestOnOpac           => C4::Context->preference("RequestOnOpac"),
813
                     itemdata_bundles        => $itemfields{bundles},
804
                     itemdata_ccode          => $itemfields{ccode},
814
                     itemdata_ccode          => $itemfields{ccode},
805
                     itemdata_materials      => $itemfields{materials},
815
                     itemdata_materials      => $itemfields{materials},
806
                     itemdata_enumchron      => $itemfields{enumchron},
816
                     itemdata_enumchron      => $itemfields{enumchron},
(-)a/tools/inventory.pl (-1 / +10 lines)
Lines 47-52 my $minlocation=$input->param('minlocation') || ''; Link Here
47
my $maxlocation=$input->param('maxlocation');
47
my $maxlocation=$input->param('maxlocation');
48
my $class_source=$input->param('class_source');
48
my $class_source=$input->param('class_source');
49
$maxlocation=$minlocation.'Z' unless ( $maxlocation || ! $minlocation );
49
$maxlocation=$minlocation.'Z' unless ( $maxlocation || ! $minlocation );
50
my $items_bundle = $input->param('items_bundle');
50
my $location=$input->param('location') || '';
51
my $location=$input->param('location') || '';
51
my $ignoreissued=$input->param('ignoreissued');
52
my $ignoreissued=$input->param('ignoreissued');
52
my $ignore_waiting_holds = $input->param('ignore_waiting_holds');
53
my $ignore_waiting_holds = $input->param('ignore_waiting_holds');
Lines 66-71 my ( $template, $borrowernumber, $cookie ) = get_template_and_user( Link Here
66
    }
67
    }
67
);
68
);
68
69
70
my $schema = Koha::Database->new()->schema();
71
my $items_bundle_rs = $schema->resultset('ItemsBundle');
72
my @items_bundles = $items_bundle_rs->search(undef, {
73
    order_by => { -asc => ['biblionumber.title'] },
74
    join => ['biblionumber'],
75
});
69
my @authorised_value_list;
76
my @authorised_value_list;
70
my $authorisedvalue_categories = '';
77
my $authorisedvalue_categories = '';
71
78
Lines 134-139 foreach my $itemtype ( @itemtypes ) { Link Here
134
141
135
$template->param(
142
$template->param(
136
    authorised_values        => \@authorised_value_list,
143
    authorised_values        => \@authorised_value_list,
144
    items_bundles            => \@items_bundles,
137
    today                    => dt_from_string,
145
    today                    => dt_from_string,
138
    minlocation              => $minlocation,
146
    minlocation              => $minlocation,
139
    maxlocation              => $maxlocation,
147
    maxlocation              => $maxlocation,
Lines 249-254 if ( $op && ( !$uploadbarcodes || $compareinv2barcd )) { Link Here
249
      minlocation  => $minlocation,
257
      minlocation  => $minlocation,
250
      maxlocation  => $maxlocation,
258
      maxlocation  => $maxlocation,
251
      class_source => $class_source,
259
      class_source => $class_source,
260
      items_bundle => $items_bundle,
252
      location     => $location,
261
      location     => $location,
253
      ignoreissued => $ignoreissued,
262
      ignoreissued => $ignoreissued,
254
      datelastseen => $datelastseen,
263
      datelastseen => $datelastseen,
Lines 267-272 if( @scanned_items ) { Link Here
267
      maxlocation  => $maxlocation,
276
      maxlocation  => $maxlocation,
268
      class_source => $class_source,
277
      class_source => $class_source,
269
      location     => $location,
278
      location     => $location,
279
      items_bundle => $items_bundle,
270
      ignoreissued => undef,
280
      ignoreissued => undef,
271
      datelastseen => undef,
281
      datelastseen => undef,
272
      branchcode   => $branchcode,
282
      branchcode   => $branchcode,
273
- 

Return to bug 28854