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

(-)a/Koha/Item.pm (+165 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 output_pref );
27
use Koha::DateUtils qw( dt_from_string output_pref );
Lines 1351-1356 sub move_to_biblio { Link Here
1351
    return $to_biblionumber;
1352
    return $to_biblionumber;
1352
}
1353
}
1353
1354
1355
=head3 bundle_items
1356
1357
  my $bundle_items = $item->bundle_items;
1358
1359
Returns the items associated with this bundle
1360
1361
=cut
1362
1363
sub bundle_items {
1364
    my ($self) = @_;
1365
1366
    if ( !$self->{_bundle_items_cached} ) {
1367
        my $bundle_items = Koha::Items->search(
1368
            { 'item_bundles_item.host' => $self->itemnumber },
1369
            { join                     => 'item_bundles_item' } );
1370
        $self->{_bundle_items}        = $bundle_items;
1371
        $self->{_bundle_items_cached} = 1;
1372
    }
1373
1374
    return $self->{_bundle_items};
1375
}
1376
1377
=head3 is_bundle
1378
1379
  my $is_bundle = $item->is_bundle;
1380
1381
Returns whether the item is a bundle or not
1382
1383
=cut
1384
1385
sub is_bundle {
1386
    my ($self) = @_;
1387
    return $self->bundle_items->count ? 1 : 0;
1388
}
1389
1390
=head3 bundle_host
1391
1392
  my $bundle = $item->bundle_host;
1393
1394
Returns the bundle item this item is attached to
1395
1396
=cut
1397
1398
sub bundle_host {
1399
    my ($self) = @_;
1400
1401
    if ( !$self->{_bundle_host_cached} ) {
1402
        my $bundle_item_rs = $self->_result->item_bundles_item;
1403
        $self->{_bundle_host} =
1404
          $bundle_item_rs
1405
          ? Koha::Item->_new_from_dbic($bundle_item_rs->host)
1406
          : undef;
1407
        $self->{_bundle_host_cached} = 1;
1408
    }
1409
1410
    return $self->{_bundle_host};
1411
}
1412
1413
=head3 in_bundle
1414
1415
  my $in_bundle = $item->in_bundle;
1416
1417
Returns whether this item is currently in a bundle
1418
1419
=cut
1420
1421
sub in_bundle {
1422
    my ($self) = @_;
1423
    return $self->bundle_host ? 1 : 0;
1424
}
1425
1426
=head3 add_to_bundle
1427
1428
  my $link = $item->add_to_bundle($bundle_item);
1429
1430
Adds the bundle_item passed to this item
1431
1432
=cut
1433
1434
sub add_to_bundle {
1435
    my ( $self, $bundle_item ) = @_;
1436
1437
    my $schema = Koha::Database->new->schema;
1438
1439
    my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1440
1441
    try {
1442
        $schema->txn_do(
1443
            sub {
1444
                $self->_result->add_to_item_bundles_hosts(
1445
                    { item => $bundle_item->itemnumber } );
1446
1447
                $bundle_item->notforloan($BundleNotLoanValue)->store();
1448
            }
1449
        );
1450
    }
1451
    catch {
1452
1453
        # 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
1454
        if ( ref($_) eq 'DBIx::Class::Exception' ) {
1455
            warn $_->{msg};
1456
            if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1457
                # FK constraints
1458
                # FIXME: MySQL error, if we support more DB engines we should implement this for each
1459
                if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1460
                    Koha::Exceptions::Object::FKConstraint->throw(
1461
                        error     => 'Broken FK constraint',
1462
                        broken_fk => $+{column}
1463
                    );
1464
                }
1465
            }
1466
            elsif (
1467
                $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1468
            {
1469
                Koha::Exceptions::Object::DuplicateID->throw(
1470
                    error        => 'Duplicate ID',
1471
                    duplicate_id => $+{key}
1472
                );
1473
            }
1474
            elsif ( $_->{msg} =~
1475
/Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1476
              )
1477
            {    # The optional \W in the regex might be a quote or backtick
1478
                my $type     = $+{type};
1479
                my $value    = $+{value};
1480
                my $property = $+{property};
1481
                $property =~ s/['`]//g;
1482
                Koha::Exceptions::Object::BadValue->throw(
1483
                    type     => $type,
1484
                    value    => $value,
1485
                    property => $property =~ /(\w+\.\w+)$/
1486
                    ? $1
1487
                    : $property
1488
                    ,    # results in table.column without quotes or backtics
1489
                );
1490
            }
1491
1492
            # Catch-all for foreign key breakages. It will help find other use cases
1493
            $_->rethrow();
1494
        }
1495
        else {
1496
            $_;
1497
        }
1498
    };
1499
}
1500
1501
=head3 remove_from_bundle
1502
1503
Remove this item from any bundle it may have been attached to.
1504
1505
=cut
1506
1507
sub remove_from_bundle {
1508
    my ($self) = @_;
1509
1510
    my $bundle_item_rs = $self->_result->item_bundles_item;
1511
    if ( $bundle_item_rs ) {
1512
        $bundle_item_rs->delete;
1513
        $self->notforloan(0)->store();
1514
        return 1;
1515
    }
1516
    return 0;
1517
}
1518
1354
=head2 Internal methods
1519
=head2 Internal methods
1355
1520
1356
=head3 _after_item_action_hooks
1521
=head3 _after_item_action_hooks
(-)a/Koha/REST/V1/Items.pm (+121 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('bundled_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
        openapi => q{}
273
    );
274
}
275
155
1;
276
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 (+2 lines)
Lines 9-14 Link Here
9
      "type": "integer",
9
      "type": "integer",
10
      "description": "Internal identifier for the parent bibliographic record"
10
      "description": "Internal identifier for the parent bibliographic record"
11
    },
11
    },
12
    "biblio": {
13
    },
12
    "external_id": {
14
    "external_id": {
13
      "type": ["string", "null"],
15
      "type": ["string", "null"],
14
      "description": "The item's barcode"
16
      "description": "The item's barcode"
(-)a/api/v1/swagger/paths.json (+9 lines)
Lines 86-91 Link Here
86
  "/items/{item_id}": {
86
  "/items/{item_id}": {
87
    "$ref": "paths/items.json#/~1items~1{item_id}"
87
    "$ref": "paths/items.json#/~1items~1{item_id}"
88
  },
88
  },
89
  "/items/{item_id}/bundled_items": {
90
    "$ref": "paths/items.json#/~1items~1{item_id}~1bundled_items"
91
  },
92
  "/items/{item_id}/bundled_items/item": {
93
    "$ref": "paths/items.json#/~1items~1{item_id}~1bundled_items~1item"
94
  },
95
  "/items/{item_id}/bundled_items/item/{bundled_item_id}": {
96
    "$ref": "paths/items.json#/~1items~1{item_id}~1bundled_items~1item~1{bundled_item_id}"
97
  },
89
  "/items/{item_id}/pickup_locations": {
98
  "/items/{item_id}/pickup_locations": {
90
    "$ref": "paths/items.json#/~1items~1{item_id}~1pickup_locations"
99
    "$ref": "paths/items.json#/~1items~1{item_id}~1pickup_locations"
91
  },
100
  },
(-)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
  },
303
  "/items/{item_id}/bundled_items/item/{bundled_item_id}": {
304
      "delete": {
305
          "x-mojo-to": "Items#remove_from_bundle",
306
          "operationId": "removeFromBundle",
307
          "tags": ["items"],
308
          "summary": "Remove item from bundle",
309
          "parameters": [{
310
                  "$ref": "../parameters.json#/item_id_pp"
311
              },
312
              {
313
                    "name": "bundled_item_id",
314
                    "in": "path",
315
                    "description": "Internal identifier for the bundled item",
316
                    "required": true,
317
                    "type": "string"
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 (+14 lines)
Lines 209-214 if (@hostitems){ Link Here
209
209
210
my $dat = &GetBiblioData($biblionumber);
210
my $dat = &GetBiblioData($biblionumber);
211
211
212
#is biblio a collection
213
my $leader = $record->leader();
214
$dat->{collection} = ( substr($leader,7,1) eq 'c' ) ? 1 : 0;
215
216
212
#coping with subscriptions
217
#coping with subscriptions
213
my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber);
218
my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber);
214
my @subscriptions       = SearchSubscriptions({ biblionumber => $biblionumber, orderby => 'title' });
219
my @subscriptions       = SearchSubscriptions({ biblionumber => $biblionumber, orderby => 'title' });
Lines 401-406 foreach my $item (@items) { Link Here
401
        $item->{cover_images} = $item_object->cover_images;
406
        $item->{cover_images} = $item_object->cover_images;
402
    }
407
    }
403
408
409
    if ($item_object->is_bundle) {
410
        $itemfields{bundles} = 1;
411
        $item->{is_bundle} = 1;
412
    }
413
414
    if ($item_object->in_bundle) {
415
        $item->{bundle_host} = $item_object->bundle_host;
416
    }
417
404
    if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
418
    if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
405
        if ($itembranchcode and $itembranchcode eq $currentbranch) {
419
        if ($itembranchcode and $itembranchcode eq $currentbranch) {
406
            push @itemloop, $item;
420
            push @itemloop, $item;
(-)a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss (+4 lines)
Lines 2304-2309 td { Link Here
2304
    display: block;
2304
    display: block;
2305
}
2305
}
2306
2306
2307
.bundled {
2308
    display: block;
2309
}
2310
2307
.datedue {
2311
.datedue {
2308
    color: #999;
2312
    color: #999;
2309
    display: block;
2313
    display: block;
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref (+4 lines)
Lines 165-170 Cataloging: Link Here
165
            - and record's last modifier name in MARC subfield
165
            - and record's last modifier name in MARC subfield
166
            - pref: MarcFieldForModifierName
166
            - pref: MarcFieldForModifierName
167
            - ". <br/><strong>NOTE:</strong> Use a dollar sign between field and subfield like 123$a."
167
            - ". <br/><strong>NOTE:</strong> Use a dollar sign between field and subfield like 123$a."
168
        -
169
            - Use the NOT_LOAN authorised value
170
            - pref: BundleNotLoanValue
171
            - to represent 'added to bundle' when an item is attached to bundle.
168
    Display:
172
    Display:
169
        -
173
        -
170
            - 'Separate main entry and subdivisions with '
174
            - 'Separate main entry and subdivisions with '
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt (-3 / +229 lines)
Lines 342-353 Link Here
342
                [% IF ( analyze ) %]<th id="[% tab | html %]_usedin" data-colname="[% tab | html %]_usedin">Used in</th><th></th>[% END %]
342
                [% IF ( analyze ) %]<th id="[% tab | html %]_usedin" data-colname="[% tab | html %]_usedin">Used in</th><th></th>[% END %]
343
                [% IF ( ShowCourseReserves ) %]<th id="[% tab | html %]_course_reserves" data-colname="[% tab | html %]_course_reserves">Course reserves</th>[% END %]
343
                [% IF ( ShowCourseReserves ) %]<th id="[% tab | html %]_course_reserves" data-colname="[% tab | html %]_course_reserves">Course reserves</th>[% END %]
344
                [% IF ( SpineLabelShowPrintOnBibDetails ) %]<th id="[% tab | html %]_spinelabel" data-colname="[% tab | html %]_spinelabel" class="NoSort">Spine label</th>[% END %]
344
                [% IF ( SpineLabelShowPrintOnBibDetails ) %]<th id="[% tab | html %]_spinelabel" data-colname="[% tab | html %]_spinelabel" class="NoSort">Spine label</th>[% END %]
345
                [% IF ( CAN_user_editcatalogue_edit_items ) %]<th id="[% tab | html %]_actions" data-colname="[% tab | html %]_actions"class="NoSort">&nbsp;</th>[% END %]
345
                [% IF ( CAN_user_editcatalogue_edit_items ) %]<th id="[% tab | html %]_actions" data-colname="[% tab | html %]_actions"class="NoSort noExport">&nbsp;</th>[% END %]
346
            </tr>
346
            </tr>
347
        </thead>
347
        </thead>
348
        <tbody>
348
        <tbody>
349
            [% FOREACH item IN items %]
349
            [% FOREACH item IN items %]
350
                <tr>
350
                <tr id="item_[% item.itemnumber | html %]" data-itemnumber="[% item.itemnumber | html %]">
351
                [% IF (StaffDetailItemSelection) %]
351
                [% IF (StaffDetailItemSelection) %]
352
                    <td style="text-align:center;vertical-align:middle">
352
                    <td style="text-align:center;vertical-align:middle">
353
                        <input type="checkbox" value="[% item.itemnumber | html %]" name="itemnumber" />
353
                        <input type="checkbox" value="[% item.itemnumber | html %]" name="itemnumber" />
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>
509
                            <span class="restricted">([% item.restrictedvalue | html %])</span>
510
                        [% END %]
510
                        [% END %]
511
511
512
                        [% IF ( item.bundle_host ) %]
513
                            <span class="bundled">In bundle: [% INCLUDE 'biblio-title.inc' biblio = item.bundle_host.biblio link = 1 %]</span>
514
                        [% END %]
515
512
                    </td>
516
                    </td>
513
                    <td class="datelastseen" data-order="[% item.datelastseen | html %]">[% item.datelastseen | $KohaDates %]</td>
517
                    <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>
518
                    <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>
597
                                <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 %]
598
                            [% END %]
595
                        [% END %]
599
                        [% END %]
600
                        [% IF collection %]
601
                            <button class="btn btn-default btn-xs details-control"><i class="fa fa-folder"></i> Manage bundle</button>
602
                        [% END %]
596
                    </td>
603
                    </td>
597
                [% END %]
604
                [% END %]
598
                </tr>
605
                </tr>
Lines 1020-1025 Note that permanent location is a code, and location may be an authval. Link Here
1020
1027
1021
[% END %]
1028
[% END %]
1022
1029
1030
    <div class="modal" id="bundleItemsModal" tabindex="-1" role="dialog" aria-labelledby="bundleItemsLabel">
1031
        <form id="bundleItemsForm" action="">
1032
            <div class="modal-dialog" role="document">
1033
                <div class="modal-content">
1034
                    <div class="modal-header">
1035
                        <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
1036
                        <h3 id="bundleItemsLabel">Add to bundle</h3>
1037
                    </div>
1038
                    <div class="modal-body">
1039
                        <div id="result"></div>
1040
                        <fieldset class="rows">
1041
                            <ol>
1042
                                <li>
1043
                                    <label class="required" for="external_id">Item barcode: </label>
1044
                                    <input type="text" id="external_id" name="external_id" required="required">
1045
                                    <span class="required">Required</span>
1046
                                </li>
1047
                            </ol>
1048
                        </fieldset>
1049
                    </div>
1050
                    <div class="modal-footer">
1051
                        <button type="submit" class="btn btn-default">Submit</button>
1052
                        <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
1053
                    </div>
1054
                </div>
1055
            </div>
1056
        </form>
1057
    </div>
1058
1023
[% MACRO jsinclude BLOCK %]
1059
[% MACRO jsinclude BLOCK %]
1024
    [% INCLUDE 'catalog-strings.inc' %]
1060
    [% INCLUDE 'catalog-strings.inc' %]
1025
    [% Asset.js("js/catalog.js") | $raw %]
1061
    [% 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' %]
1351
    [% INCLUDE 'datatables.inc' %]
1316
    [% Asset.js("lib/jquery/plugins/jquery.dataTables.columnFilter.js") | $raw %]
1352
    [% Asset.js("lib/jquery/plugins/jquery.dataTables.columnFilter.js") | $raw %]
1317
    [% INCLUDE 'columns_settings.inc' %]
1353
    [% INCLUDE 'columns_settings.inc' %]
1354
    [% INCLUDE 'js-date-format.inc' %]
1318
    [% Asset.js("js/browser.js") | $raw %]
1355
    [% Asset.js("js/browser.js") | $raw %]
1319
    [% Asset.js("js/table_filters.js") | $raw %]
1356
    [% Asset.js("js/table_filters.js") | $raw %]
1320
    <script>
1357
    <script>
Lines 1323-1328 Note that permanent location is a code, and location may be an authval. Link Here
1323
        browser.show();
1360
        browser.show();
1324
1361
1325
        $(document).ready(function() {
1362
        $(document).ready(function() {
1363
1364
            function createChild ( row, itemnumber ) {
1365
1366
                // Toolbar
1367
                var bundle_toolbar = $('<div id="toolbar" class="btn-toolbar"><a class="btn btn-default" data-toggle="modal" data-target="#bundleItemsModal" data-item="' + itemnumber + '"><i class="fa fa-plus"></i> Add to bundle</a></div>');
1368
1369
                // This is the table we'll convert into a DataTable
1370
                var bundles_table = $('<table class="display tbundle" data-itemnumber="'+itemnumber+'" id="bundle_table_'+itemnumber+'" width="100%"/>');
1371
1372
                // Display it the child row
1373
                row.child( bundle_toolbar.add(bundles_table) ).show();
1374
1375
                // Initialise as a DataTable
1376
                var bundle_table_url = "/api/v1/items/" + itemnumber + "/bundled_items?";
1377
                var bundle_table = bundles_table.api({
1378
                    "ajax": {
1379
                        "url": bundle_table_url
1380
                    },
1381
                    "header_filter": false,
1382
                    "embed": [
1383
                        "biblio"
1384
                    ],
1385
                    "order": [[ 1, "asc" ]],
1386
                    "columns": [
1387
                        {
1388
                            "data": "biblio.title:biblio.medium",
1389
                            "title": "Title",
1390
                            "searchable": true,
1391
                            "orderable": true,
1392
                            "render": function(data, type, row, meta) {
1393
                                var title = "";
1394
                                if ( row.biblio.title ) {
1395
                                    title = title.concat('<span class="biblio-title">',row.biblio.title,'</span>');
1396
                                }
1397
                                if ( row.biblio.medium ) {
1398
                                    title = title.concat('<span class="biblio-medium">',row.biblio.medium,'</span>');
1399
                                }
1400
                                return title;
1401
                            }
1402
                        },
1403
                        {
1404
                            "data": "biblio.author",
1405
                            "title": "Author",
1406
                            "searchable": true,
1407
                            "orderable": true,
1408
                        },
1409
                        {
1410
                            "data": "collection_code",
1411
                            "title": "Collection code",
1412
                            "searchable": true,
1413
                            "orderable": true,
1414
                        },
1415
                        {
1416
                            "data": "item_type",
1417
                            "title": "Item Type",
1418
                            "searchable": false,
1419
                            "orderable": true,
1420
                        },
1421
                        {
1422
                            "data": "callnumber",
1423
                            "title": "Callnumber",
1424
                            "searchable": true,
1425
                            "orderable": true,
1426
                        },
1427
                        {
1428
                            "data": "external_id",
1429
                            "title": "Barcode",
1430
                            "searchable": true,
1431
                            "orderable": true,
1432
                        },
1433
                        {
1434
                            "data": "lost_status:last_seen_date",
1435
                            "title": "Status",
1436
                            "searchable": false,
1437
                            "orderable": true,
1438
                            "render": function(data, type, row, meta) {
1439
                                if ( row.lost_status ) {
1440
                                    return "Lost: " + row.lost_status;
1441
                                }
1442
                                return "";
1443
                            }
1444
                        },
1445
                        {
1446
                            "data": function( row, type, val, meta ) {
1447
                                var result = '<button class="btn btn-default btn-xs remove" role="button" data-itemnumber="'+row.item_id+'"><i class="fa fa-minus" aria-hidden="true"></i> '+_("Remove")+'</button>\n';
1448
                                return result;
1449
                            },
1450
                            "title": "Actions",
1451
                            "searchable": false,
1452
                            "orderable": false,
1453
                            "class": "noExport"
1454
                        }
1455
                    ]
1456
                }, [], 1);
1457
1458
                $(".tbundle").on("click", ".remove", function(){
1459
                    var bundle_table = $(this).closest('table');
1460
                    var host_itemnumber = bundle_table.data('itemnumber');
1461
                    var component_itemnumber = $(this).data('itemnumber');
1462
                    var unlink_item_url = "/api/v1/items/" + host_itemnumber + "/bundled_items/item/" + component_itemnumber;
1463
                    $.ajax({
1464
                        type: "DELETE",
1465
                        url: unlink_item_url,
1466
                        success: function(){
1467
                            bundle_table.DataTable({ 'retrieve': true }).draw(false);
1468
                        }
1469
                    });
1470
                });
1471
1472
                return;
1473
            }
1474
1475
1326
            var ids = ['holdings_table', 'otherholdings_table'];
1476
            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 %] ];
1477
            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 %]"];
1478
            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>',
1489
                    "sDom": 'C<"top pager"ilpfB><"#filter_c">tr<"bottom pager"ip>',
1340
                };
1490
                };
1341
                var table = KohaTable(id, dt_parameters, columns_settings[i], 'with_filters');
1491
                var table = KohaTable(id, dt_parameters, columns_settings[i], 'with_filters');
1492
1493
                // Add event listener for opening and closing details
1494
                $('#' + id + ' tbody').on('click', 'button.details-control', function () {
1495
                    var tr = $(this).closest('tr');
1496
                    var dTable = $(this).closest('table').DataTable({ 'retrieve': true });
1497
1498
                    var itemnumber = tr.data('itemnumber');
1499
                    var row = dTable.row( tr );
1500
1501
                    if ( row.child.isShown() ) {
1502
                        // This row is already open - close it
1503
                        row.child.hide();
1504
                        tr.removeClass('shown');
1505
                    }
1506
                    else {
1507
                        // Open this row
1508
                        createChild(row, itemnumber);
1509
                        tr.addClass('shown');
1510
                    }
1511
                } );
1342
            }
1512
            }
1343
1513
1344
            [% IF Koha.Preference('AcquisitionDetails') %]
1514
            [% 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"
1530
                    "sPaginationType": "full"
1361
                }));
1531
                }));
1362
            [% END %]
1532
            [% END %]
1533
1534
            var bundle_changed;
1535
            var bundle_form_active;
1536
            $("#bundleItemsModal").on("shown.bs.modal", function(e){
1537
                var button = $(e.relatedTarget);
1538
                var item_id = button.data('item');
1539
                $("#result").replaceWith('<div id="result"></div>');
1540
                $("#bundleItemsForm").attr('action', '/api/v1/items/' + item_id + '/bundled_items/item');
1541
                $("#external_id").focus();
1542
                bundle_changed = 0;
1543
                bundle_form_active = item_id;
1544
            });
1545
1546
            $("#bundleItemsForm").submit(function(event) {
1547
1548
                  /* stop form from submitting normally */
1549
                  event.preventDefault();
1550
1551
                  /* get the action attribute from the <form action=""> element */
1552
                  var $form = $(this),
1553
                  url = $form.attr('action');
1554
1555
                  /* Send the data using post with external_id */
1556
                  var posting = $.post(url, JSON.stringify({
1557
                      external_id: $('#external_id').val()
1558
                  }), null, "json");
1559
1560
                  /* Report the results */
1561
                  posting.done(function(data) {
1562
                      var barcode = $('#external_id').val();
1563
                      $('#result').replaceWith('<div id="result" class="alert alert-success">Success: Added '+barcode+'</div>');
1564
                      $('#external_id').val('').focus();
1565
                      bundle_changed = 1;
1566
                  });
1567
                  posting.fail(function(data) {
1568
                      var barcode = $('#external_id').val();
1569
                      if ( data.status === 409 ) {
1570
                          var response = data.responseJSON;
1571
                          if ( response.key === "PRIMARY" ) {
1572
                              $('#result').replaceWith('<div id="result" class="alert alert-warning">Warning: Item '+barcode+' already attached</div>');
1573
                          } else {
1574
                              $('#result').replaceWith('<div id="result" class="alert alert-danger">Failure: Item '+barcode+' belongs to another bundle</div>');
1575
                          }
1576
                      } else {
1577
                          $('#result').replaceWith('<div id="result" class="alert alert-danger">Failure</div>');
1578
                      }
1579
                      $('#external_id').val('').focus();
1580
                  });
1581
            });
1582
1583
            $("#bundleItemsModal").on("hidden.bs.modal", function(e){
1584
                if ( bundle_changed ) {
1585
                    $('#bundle_table_'+bundle_form_active).DataTable({ 'retrieve': true }).ajax.reload();
1586
                }
1587
                bundle_form_active = 0;
1588
                bundle_changed = 0;
1589
            });
1363
        });
1590
        });
1364
1591
1365
        $(document).ready(function() {
1592
        $(document).ready(function() {
1366
- 

Return to bug 28854