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

(-)a/Koha/Item.pm (+158 lines)
Lines 20-25 package Koha::Item; Link Here
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use List::MoreUtils qw( any );
22
use List::MoreUtils qw( any );
23
use Try::Tiny qw( catch try );
23
24
24
use Koha::Database;
25
use Koha::Database;
25
use Koha::DateUtils qw( dt_from_string output_pref );
26
use Koha::DateUtils qw( dt_from_string output_pref );
Lines 1465-1470 sub move_to_biblio { Link Here
1465
    return $to_biblionumber;
1466
    return $to_biblionumber;
1466
}
1467
}
1467
1468
1469
=head3 bundle_items
1470
1471
  my $bundle_items = $item->bundle_items;
1472
1473
Returns the items associated with this bundle
1474
1475
=cut
1476
1477
sub bundle_items {
1478
    my ($self) = @_;
1479
1480
    if ( !$self->{_bundle_items_cached} ) {
1481
        my $bundle_items = Koha::Items->search(
1482
            { 'item_bundles_item.host' => $self->itemnumber },
1483
            { join                     => 'item_bundles_item' } );
1484
        $self->{_bundle_items}        = $bundle_items;
1485
        $self->{_bundle_items_cached} = 1;
1486
    }
1487
1488
    return $self->{_bundle_items};
1489
}
1490
1491
=head3 is_bundle
1492
1493
  my $is_bundle = $item->is_bundle;
1494
1495
Returns whether the item is a bundle or not
1496
1497
=cut
1498
1499
sub is_bundle {
1500
    my ($self) = @_;
1501
    return $self->bundle_items->count ? 1 : 0;
1502
}
1503
1504
=head3 bundle_host
1505
1506
  my $bundle = $item->bundle_host;
1507
1508
Returns the bundle item this item is attached to
1509
1510
=cut
1511
1512
sub bundle_host {
1513
    my ($self) = @_;
1514
1515
    my $bundle_items_rs = $self->_result->item_bundles_item;
1516
    return unless $bundle_items_rs;
1517
    return Koha::Item->_new_from_dbic($bundle_items_rs->host);
1518
}
1519
1520
=head3 in_bundle
1521
1522
  my $in_bundle = $item->in_bundle;
1523
1524
Returns whether this item is currently in a bundle
1525
1526
=cut
1527
1528
sub in_bundle {
1529
    my ($self) = @_;
1530
    return $self->bundle_host ? 1 : 0;
1531
}
1532
1533
=head3 add_to_bundle
1534
1535
  my $link = $item->add_to_bundle($bundle_item);
1536
1537
Adds the bundle_item passed to this item
1538
1539
=cut
1540
1541
sub add_to_bundle {
1542
    my ( $self, $bundle_item ) = @_;
1543
1544
    my $schema = Koha::Database->new->schema;
1545
1546
    my $BundleNotLoanValue = C4::Context->preference('BundleNotLoanValue');
1547
1548
    try {
1549
        $schema->txn_do(
1550
            sub {
1551
                $self->_result->add_to_item_bundles_hosts(
1552
                    { item => $bundle_item->itemnumber } );
1553
1554
                $bundle_item->notforloan($BundleNotLoanValue)->store();
1555
            }
1556
        );
1557
    }
1558
    catch {
1559
1560
        # 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
1561
        if ( ref($_) eq 'DBIx::Class::Exception' ) {
1562
            warn $_->{msg};
1563
            if ( $_->{msg} =~ /Cannot add or update a child row: a foreign key constraint fails/ ) {
1564
                # FK constraints
1565
                # FIXME: MySQL error, if we support more DB engines we should implement this for each
1566
                if ( $_->{msg} =~ /FOREIGN KEY \(`(?<column>.*?)`\)/ ) {
1567
                    Koha::Exceptions::Object::FKConstraint->throw(
1568
                        error     => 'Broken FK constraint',
1569
                        broken_fk => $+{column}
1570
                    );
1571
                }
1572
            }
1573
            elsif (
1574
                $_->{msg} =~ /Duplicate entry '(.*?)' for key '(?<key>.*?)'/ )
1575
            {
1576
                Koha::Exceptions::Object::DuplicateID->throw(
1577
                    error        => 'Duplicate ID',
1578
                    duplicate_id => $+{key}
1579
                );
1580
            }
1581
            elsif ( $_->{msg} =~
1582
/Incorrect (?<type>\w+) value: '(?<value>.*)' for column \W?(?<property>\S+)/
1583
              )
1584
            {    # The optional \W in the regex might be a quote or backtick
1585
                my $type     = $+{type};
1586
                my $value    = $+{value};
1587
                my $property = $+{property};
1588
                $property =~ s/['`]//g;
1589
                Koha::Exceptions::Object::BadValue->throw(
1590
                    type     => $type,
1591
                    value    => $value,
1592
                    property => $property =~ /(\w+\.\w+)$/
1593
                    ? $1
1594
                    : $property
1595
                    ,    # results in table.column without quotes or backtics
1596
                );
1597
            }
1598
1599
            # Catch-all for foreign key breakages. It will help find other use cases
1600
            $_->rethrow();
1601
        }
1602
        else {
1603
            $_;
1604
        }
1605
    };
1606
}
1607
1608
=head3 remove_from_bundle
1609
1610
Remove this item from any bundle it may have been attached to.
1611
1612
=cut
1613
1614
sub remove_from_bundle {
1615
    my ($self) = @_;
1616
1617
    my $bundle_item_rs = $self->_result->item_bundles_item;
1618
    if ( $bundle_item_rs ) {
1619
        $bundle_item_rs->delete;
1620
        $self->notforloan(0)->store();
1621
        return 1;
1622
    }
1623
    return 0;
1624
}
1625
1468
=head2 Internal methods
1626
=head2 Internal methods
1469
1627
1470
=head3 _after_item_action_hooks
1628
=head3 _after_item_action_hooks
(-)a/Koha/REST/V1/Items.pm (+121 lines)
Lines 148-151 sub pickup_locations { Link Here
148
    };
148
    };
149
}
149
}
150
150
151
=head3 bundled_items
152
153
Controller function that handles bundled_items Koha::Item objects
154
155
=cut
156
157
sub bundled_items {
158
    my $c = shift->openapi->valid_input or return;
159
160
    my $item_id = $c->validation->param('item_id');
161
    my $item = Koha::Items->find( $item_id );
162
163
    unless ($item) {
164
        return $c->render(
165
            status  => 404,
166
            openapi => { error => "Item not found" }
167
        );
168
    }
169
170
    return try {
171
        my $items_set = $item->bundle_items;
172
        my $items     = $c->objects->search( $items_set );
173
        return $c->render(
174
            status  => 200,
175
            openapi => $items
176
        );
177
    }
178
    catch {
179
        $c->unhandled_exception($_);
180
    };
181
}
182
183
=head3 add_to_bundle
184
185
Controller function that handles adding items to this bundle
186
187
=cut
188
189
sub add_to_bundle {
190
    my $c = shift->openapi->valid_input or return;
191
192
    my $item_id = $c->validation->param('item_id');
193
    my $item = Koha::Items->find( $item_id );
194
195
    unless ($item) {
196
        return $c->render(
197
            status  => 404,
198
            openapi => { error => "Item not found" }
199
        );
200
    }
201
202
203
    my $bundle_item_id = $c->validation->param('body')->{'external_id'};
204
    my $bundle_item = Koha::Items->find( { barcode => $bundle_item_id } );
205
206
    unless ($bundle_item) {
207
        return $c->render(
208
            status  => 404,
209
            openapi => { error => "Bundle item not found" }
210
        );
211
    }
212
213
    return try {
214
        my $link = $item->add_to_bundle($bundle_item);
215
        return $c->render(
216
            status  => 201,
217
            openapi => $bundle_item
218
        );
219
    }
220
    catch {
221
        if ( ref($_) eq 'Koha::Exceptions::Object::DuplicateID' ) {
222
            return $c->render(
223
                status  => 409,
224
                openapi => {
225
                    error => 'Item is already bundled',
226
                    key   => $_->duplicate_id
227
                }
228
            );
229
        }
230
        else {
231
            $c->unhandled_exception($_);
232
        }
233
    };
234
}
235
236
=head3 remove_from_bundle
237
238
Controller function that handles removing items from this bundle
239
240
=cut
241
242
sub remove_from_bundle {
243
    my $c = shift->openapi->valid_input or return;
244
245
    my $item_id = $c->validation->param('item_id');
246
    my $item = Koha::Items->find( $item_id );
247
248
    unless ($item) {
249
        return $c->render(
250
            status  => 404,
251
            openapi => { error => "Item not found" }
252
        );
253
    }
254
255
    my $bundle_item_id = $c->validation->param('bundled_item_id');
256
    my $bundle_item = Koha::Items->find( { itemnumber => $bundle_item_id } );
257
258
    unless ($bundle_item) {
259
        return $c->render(
260
            status  => 404,
261
            openapi => { error => "Bundle item not found" }
262
        );
263
    }
264
265
    $bundle_item->remove_from_bundle;
266
    return $c->render(
267
        status  => 204,
268
        openapi => q{}
269
    );
270
}
271
151
1;
272
1;
(-)a/admin/columns_settings.yml (+22 lines)
Lines 525-530 modules: Link Here
525
            -
525
            -
526
              columnname: checkin_on
526
              columnname: checkin_on
527
527
528
      bundle_tables:
529
        columns:
530
            -
531
              columnname: title
532
              cannot_be_toggled: 1
533
            -
534
              columnname: author
535
            -
536
              columnname: collection_code
537
            -
538
              columnname: item_type
539
            -
540
              columnname: callnumber
541
            -
542
              columnname: external_id
543
            -
544
              columnname: status
545
            -
546
              columnname: bundle_actions
547
              cannot_be_toggled: 1
548
              cannot_be_modified: 1
549
528
  cataloguing:
550
  cataloguing:
529
    addbooks:
551
    addbooks:
530
      reservoir-table:
552
      reservoir-table:
(-)a/api/v1/swagger/definitions/bundle_link.yaml (+14 lines)
Line 0 Link Here
1
---
2
type: object
3
properties:
4
  item_id:
5
    type:
6
      - integer
7
      - "null"
8
    description: Internal item identifier
9
  external_id:
10
    type:
11
      - string
12
      - "null"
13
    description: Item barcode
14
additionalProperties: false
(-)a/api/v1/swagger/definitions/item.yaml (+1 lines)
Lines 7-12 properties: Link Here
7
  biblio_id:
7
  biblio_id:
8
    type: integer
8
    type: integer
9
    description: Internal identifier for the parent bibliographic record
9
    description: Internal identifier for the parent bibliographic record
10
  biblio: {}
10
  external_id:
11
  external_id:
11
    type:
12
    type:
12
      - string
13
      - string
(-)a/api/v1/swagger/paths/items.yaml (+157 lines)
Lines 93-98 Link Here
93
    x-koha-authorization:
93
    x-koha-authorization:
94
      permissions:
94
      permissions:
95
        catalogue: "1"
95
        catalogue: "1"
96
"/items/{item_id}/bundled_items":
97
  post:
98
    x-mojo-to: Items#add_to_bundle
99
    operationId: addToBundle
100
    tags:
101
      - items
102
    summary: Add item to bundle
103
    parameters:
104
      - $ref: "../swagger.yaml#/parameters/item_id_pp"
105
      - name: body
106
        in: body
107
        description: A JSON object containing information about the new bundle link
108
        required: true
109
        schema:
110
          $ref: "../swagger.yaml#/definitions/bundle_link"
111
    consumes:
112
      - application/json
113
    produces:
114
      - application/json
115
    responses:
116
      "201":
117
        description: A successfully created bundle link
118
        schema:
119
          items:
120
            $ref: "../swagger.yaml#/definitions/item"
121
      "400":
122
        description: Bad parameter
123
        schema:
124
          $ref: "../swagger.yaml#/definitions/error"
125
      "401":
126
        description: Authentication required
127
        schema:
128
          $ref: "../swagger.yaml#/definitions/error"
129
      "403":
130
        description: Access forbidden
131
        schema:
132
          $ref: "../swagger.yaml#/definitions/error"
133
      "404":
134
        description: Resource not found
135
        schema:
136
          $ref: "../swagger.yaml#/definitions/error"
137
      "409":
138
        description: Conflict in creating resource
139
        schema:
140
          $ref: "../swagger.yaml#/definitions/error"
141
      "500":
142
        description: Internal server error
143
        schema:
144
          $ref: "../swagger.yaml#/definitions/error"
145
      "503":
146
        description: Under maintenance
147
        schema:
148
          $ref: "../swagger.yaml#/definitions/error"
149
    x-koha-authorization:
150
      permissions:
151
        catalogue: 1
152
  get:
153
    x-mojo-to: Items#bundled_items
154
    operationId: bundledItems
155
    tags:
156
      - items
157
    summary: List bundled items
158
    parameters:
159
      - $ref: "../swagger.yaml#/parameters/item_id_pp"
160
      - name: external_id
161
        in: query
162
        description: Search on the item's barcode
163
        required: false
164
        type: string
165
      - $ref: "../swagger.yaml#/parameters/match"
166
      - $ref: "../swagger.yaml#/parameters/order_by"
167
      - $ref: "../swagger.yaml#/parameters/page"
168
      - $ref: "../swagger.yaml#/parameters/per_page"
169
      - $ref: "../swagger.yaml#/parameters/q_param"
170
      - $ref: "../swagger.yaml#/parameters/q_body"
171
      - $ref: "../swagger.yaml#/parameters/q_header"
172
    consumes:
173
      - application/json
174
    produces:
175
      - application/json
176
    responses:
177
      "200":
178
        description: A list of item
179
        schema:
180
          type: array
181
          items:
182
            $ref: "../swagger.yaml#/definitions/item"
183
      "401":
184
        description: Authentication required
185
        schema:
186
          $ref: "../swagger.yaml#/definitions/error"
187
      "403":
188
        description: Access forbidden
189
        schema:
190
          $ref: "../swagger.yaml#/definitions/error"
191
      "500":
192
        description: Internal server error
193
        schema:
194
          $ref: "../swagger.yaml#/definitions/error"
195
      "503":
196
        description: Under maintenance
197
        schema:
198
          $ref: "../swagger.yaml#/definitions/error"
199
    x-koha-authorization:
200
      permissions:
201
        catalogue: "1"
202
    x-koha-embed:
203
      - biblio
204
      - checkout
205
"/items/{item_id}/bundled_items/{bundled_item_id}":
206
  delete:
207
    x-mojo-to: Items#remove_from_bundle
208
    operationId: removeFromBundle
209
    tags:
210
      - items
211
    summary: Remove item from bundle
212
    parameters:
213
      - $ref: "../swagger.yaml#/parameters/item_id_pp"
214
      - name: bundled_item_id
215
        in: path
216
        description: Internal identifier for the bundled item
217
        required: true
218
        type: string
219
    consumes:
220
      - application/json
221
    produces:
222
      - application/json
223
    responses:
224
      "204":
225
        description: Bundle link deleted
226
      "400":
227
        description: Bad parameter
228
        schema:
229
          $ref: "../swagger.yaml#/definitions/error"
230
      "401":
231
        description: Authentication required
232
        schema:
233
          $ref: "../swagger.yaml#/definitions/error"
234
      "403":
235
        description: Access forbidden
236
        schema:
237
          $ref: "../swagger.yaml#/definitions/error"
238
      "404":
239
        description: Resource not found
240
        schema:
241
          $ref: "../swagger.yaml#/definitions/error"
242
      "500":
243
        description: Internal server error
244
        schema:
245
          $ref: "../swagger.yaml#/definitions/error"
246
      "503":
247
        description: Under maintenance
248
        schema:
249
          $ref: "../swagger.yaml#/definitions/error"
250
    x-koha-authorization:
251
      permissions:
252
        catalogue: 1
96
"/items/{item_id}/pickup_locations":
253
"/items/{item_id}/pickup_locations":
97
  get:
254
  get:
98
    x-mojo-to: Items#pickup_locations
255
    x-mojo-to: Items#pickup_locations
(-)a/api/v1/swagger/swagger.yaml (+6 lines)
Lines 10-15 definitions: Link Here
10
    $ref: ./definitions/allows_renewal.yaml
10
    $ref: ./definitions/allows_renewal.yaml
11
  basket:
11
  basket:
12
    $ref: ./definitions/basket.yaml
12
    $ref: ./definitions/basket.yaml
13
  bundle_link:
14
    $ref: ./definitions/bundle_link.yaml
13
  cashup:
15
  cashup:
14
    $ref: ./definitions/cashup.yaml
16
    $ref: ./definitions/cashup.yaml
15
  checkout:
17
  checkout:
Lines 169-174 paths: Link Here
169
    $ref: ./paths/items.yaml#/~1items
171
    $ref: ./paths/items.yaml#/~1items
170
  "/items/{item_id}":
172
  "/items/{item_id}":
171
    $ref: "./paths/items.yaml#/~1items~1{item_id}"
173
    $ref: "./paths/items.yaml#/~1items~1{item_id}"
174
  "/items/{item_id}/bundled_items":
175
    $ref: ./paths/items.yaml#/~1items~1{item_id}~1bundled_items
176
  "/items/{item_id}/bundled_items/{bundled_item_id}":
177
    $ref: ./paths/items.yaml#/~1items~1{item_id}~1bundled_items~1{bundled_item_id}
172
  "/items/{item_id}/pickup_locations":
178
  "/items/{item_id}/pickup_locations":
173
    $ref: "./paths/items.yaml#/~1items~1{item_id}~1pickup_locations"
179
    $ref: "./paths/items.yaml#/~1items~1{item_id}~1pickup_locations"
174
  /libraries:
180
  /libraries:
(-)a/catalogue/detail.pl (+14 lines)
Lines 206-211 if (@hostitems){ Link Here
206
206
207
my $dat = &GetBiblioData($biblionumber);
207
my $dat = &GetBiblioData($biblionumber);
208
208
209
#is biblio a collection and are bundles enabled
210
my $leader = $record->leader();
211
$dat->{bundlesEnabled} = ( ( substr( $leader, 7, 1 ) eq 'c' )
212
      && C4::Context->preference('BundleNotLoanValue') ) ? 1 : 0;
213
209
#coping with subscriptions
214
#coping with subscriptions
210
my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber);
215
my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber);
211
my @subscriptions       = SearchSubscriptions({ biblionumber => $biblionumber, orderby => 'title' });
216
my @subscriptions       = SearchSubscriptions({ biblionumber => $biblionumber, orderby => 'title' });
Lines 451-456 foreach my $item (@items) { Link Here
451
        }
456
        }
452
    }
457
    }
453
458
459
    if ($item_object->is_bundle) {
460
        $itemfields{bundles} = 1;
461
        $item->{is_bundle} = 1;
462
    }
463
464
    if ($item_object->in_bundle) {
465
        $item->{bundle_host} = $item_object->bundle_host;
466
    }
467
454
    if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
468
    if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
455
        if ($itembranchcode and $itembranchcode eq $currentbranch) {
469
        if ($itembranchcode and $itembranchcode eq $currentbranch) {
456
            push @itemloop, $item;
470
            push @itemloop, $item;
(-)a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss (+4 lines)
Lines 2355-2360 td { Link Here
2355
    display: block;
2355
    display: block;
2356
}
2356
}
2357
2357
2358
.bundled {
2359
    display: block;
2360
}
2361
2358
.datedue {
2362
.datedue {
2359
    color: #999;
2363
    color: #999;
2360
    display: block;
2364
    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 / +256 lines)
Lines 368-379 Link Here
368
                [% IF ( analyze ) %]<th id="[% tab | html %]_usedin" data-colname="[% tab | html %]_usedin">Used in</th><th></th>[% END %]
368
                [% IF ( analyze ) %]<th id="[% tab | html %]_usedin" data-colname="[% tab | html %]_usedin">Used in</th><th></th>[% END %]
369
                [% IF ( ShowCourseReserves ) %]<th id="[% tab | html %]_course_reserves" data-colname="[% tab | html %]_course_reserves">Course reserves</th>[% END %]
369
                [% IF ( ShowCourseReserves ) %]<th id="[% tab | html %]_course_reserves" data-colname="[% tab | html %]_course_reserves">Course reserves</th>[% END %]
370
                [% IF ( SpineLabelShowPrintOnBibDetails ) %]<th id="[% tab | html %]_spinelabel" data-colname="[% tab | html %]_spinelabel" class="NoSort">Spine label</th>[% END %]
370
                [% IF ( SpineLabelShowPrintOnBibDetails ) %]<th id="[% tab | html %]_spinelabel" data-colname="[% tab | html %]_spinelabel" class="NoSort">Spine label</th>[% END %]
371
                [% IF ( CAN_user_editcatalogue_edit_items ) %]<th id="[% tab | html %]_actions" data-colname="[% tab | html %]_actions"class="NoSort">&nbsp;</th>[% END %]
371
                [% IF ( CAN_user_editcatalogue_edit_items ) %]<th id="[% tab | html %]_actions" data-colname="[% tab | html %]_actions"class="NoSort noExport">&nbsp;</th>[% END %]
372
            </tr>
372
            </tr>
373
        </thead>
373
        </thead>
374
        <tbody>
374
        <tbody>
375
            [% FOREACH item IN items %]
375
            [% FOREACH item IN items %]
376
                <tr>
376
                <tr id="item_[% item.itemnumber | html %]" data-itemnumber="[% item.itemnumber | html %]">
377
                [% IF (StaffDetailItemSelection) %]
377
                [% IF (StaffDetailItemSelection) %]
378
                    <td style="text-align:center;vertical-align:middle">
378
                    <td style="text-align:center;vertical-align:middle">
379
                        <input type="checkbox" value="[% item.itemnumber | html %]" name="itemnumber" />
379
                        <input type="checkbox" value="[% item.itemnumber | html %]" name="itemnumber" />
Lines 545-550 Note that permanent location is a code, and location may be an authval. Link Here
545
                        [% IF ( item.restricted ) %]
545
                        [% IF ( item.restricted ) %]
546
                            <span class="restricted">([% item.restrictedvalue | html %])</span>
546
                            <span class="restricted">([% item.restrictedvalue | html %])</span>
547
                        [% END %]
547
                        [% END %]
548
549
                        [% IF ( item.bundle_host ) %]
550
                            <span class="bundled">In bundle: [% INCLUDE 'biblio-title.inc' biblio = item.bundle_host.biblio link = 1 %]</span>
551
                        [% END %]
552
548
                    </td>
553
                    </td>
549
                    <td class="datelastseen" data-order="[% item.datelastseen | html %]">[% item.datelastseen | $KohaDates %]</td>
554
                    <td class="datelastseen" data-order="[% item.datelastseen | html %]">[% item.datelastseen | $KohaDates %]</td>
550
                    <td class="issues" data-order="[% item.issues || 0 | html %]">[% item.issues || 0 | html %]</td>
555
                    <td class="issues" data-order="[% item.issues || 0 | html %]">[% item.issues || 0 | html %]</td>
Lines 631-636 Note that permanent location is a code, and location may be an authval. Link Here
631
                                <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>
636
                                <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>
632
                            [% END %]
637
                            [% END %]
633
                        [% END %]
638
                        [% END %]
639
                        [% IF bundlesEnabled %]
640
                            <button class="btn btn-default btn-xs details-control"><i class="fa fa-folder"></i> Manage bundle</button>
641
                        [% END %]
634
                    </td>
642
                    </td>
635
                [% END %]
643
                [% END %]
636
                </tr>
644
                </tr>
Lines 1222-1227 Note that permanent location is a code, and location may be an authval. Link Here
1222
    </div>
1230
    </div>
1223
</div>
1231
</div>
1224
1232
1233
    [% IF bundlesEnabled %]
1234
    <div class="modal" id="bundleItemsModal" tabindex="-1" role="dialog" aria-labelledby="bundleItemsLabel">
1235
        <form id="bundleItemsForm" action="">
1236
            <div class="modal-dialog" role="document">
1237
                <div class="modal-content">
1238
                    <div class="modal-header">
1239
                        <button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
1240
                        <h3 id="bundleItemsLabel">Add to bundle</h3>
1241
                    </div>
1242
                    <div class="modal-body">
1243
                        <div id="result"></div>
1244
                        <fieldset class="rows">
1245
                            <ol>
1246
                                <li>
1247
                                    <label class="required" for="external_id">Item barcode: </label>
1248
                                    <input type="text" id="external_id" name="external_id" required="required">
1249
                                    <span class="required">Required</span>
1250
                                </li>
1251
                            </ol>
1252
                        </fieldset>
1253
                    </div>
1254
                    <div class="modal-footer">
1255
                        <button type="submit" class="btn btn-default">Submit</button>
1256
                        <button class="btn btn-default" data-dismiss="modal" aria-hidden="true">Close</button>
1257
                    </div>
1258
                </div>
1259
            </div>
1260
        </form>
1261
    </div>
1262
    [% END %]
1263
1225
[% MACRO jsinclude BLOCK %]
1264
[% MACRO jsinclude BLOCK %]
1226
    [% INCLUDE 'catalog-strings.inc' %]
1265
    [% INCLUDE 'catalog-strings.inc' %]
1227
    [% Asset.js("js/catalog.js") | $raw %]
1266
    [% Asset.js("js/catalog.js") | $raw %]
Lines 1528-1533 Note that permanent location is a code, and location may be an authval. Link Here
1528
    [% INCLUDE 'datatables.inc' %]
1567
    [% INCLUDE 'datatables.inc' %]
1529
    [% Asset.js("lib/jquery/plugins/jquery.dataTables.columnFilter.js") | $raw %]
1568
    [% Asset.js("lib/jquery/plugins/jquery.dataTables.columnFilter.js") | $raw %]
1530
    [% INCLUDE 'columns_settings.inc' %]
1569
    [% INCLUDE 'columns_settings.inc' %]
1570
    [% INCLUDE 'js-date-format.inc' %]
1531
    [% Asset.js("js/browser.js") | $raw %]
1571
    [% Asset.js("js/browser.js") | $raw %]
1532
    [% Asset.js("js/table_filters.js") | $raw %]
1572
    [% Asset.js("js/table_filters.js") | $raw %]
1533
    <script>
1573
    <script>
Lines 1535-1541 Note that permanent location is a code, and location may be an authval. Link Here
1535
        browser = KOHA.browser('[% searchid | html %]', parseInt(biblionumber, 10));
1575
        browser = KOHA.browser('[% searchid | html %]', parseInt(biblionumber, 10));
1536
        browser.show();
1576
        browser.show();
1537
1577
1578
        [% IF bundlesEnabled %]
1579
        var bundle_settings = [% TablesSettings.GetTableSettings('catalogue', 'detail','bundle_tables','json') | $raw %];
1580
        [% END %]
1538
        $(document).ready(function() {
1581
        $(document).ready(function() {
1582
1583
            [% IF bundlesEnabled %] // Bundle handling
1584
            function createChild ( row, itemnumber ) {
1585
1586
                // Toolbar
1587
                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>');
1588
1589
                // This is the table we'll convert into a DataTable
1590
                var bundles_table = $('<table class="display tbundle" data-itemnumber="'+itemnumber+'" id="bundle_table_'+itemnumber+'" width="100%"/>');
1591
1592
                // Display it the child row
1593
                row.child( bundle_toolbar.add(bundles_table) ).show();
1594
1595
                // Initialise as a DataTable
1596
                var bundle_table_url = "/api/v1/items/" + itemnumber + "/bundled_items?";
1597
                var bundle_table = bundles_table.kohaTable({
1598
                    "ajax": {
1599
                        "url": bundle_table_url
1600
                    },
1601
                    "header_filter": false,
1602
                    "embed": [
1603
                        "biblio"
1604
                    ],
1605
                    "order": [[ 1, "asc" ]],
1606
                    "columnDefs": [ {
1607
                        "targets": [0,1,2,3,4,5],
1608
                        "render": function (data, type, row, meta) {
1609
                            if ( data && type == 'display' ) {
1610
                                return data.escapeHtml();
1611
                            }
1612
                            return data;
1613
                        }
1614
                    } ],
1615
                    "columns": [
1616
                        {
1617
                            "data": "biblio.title:biblio.medium",
1618
                            "title": "Title",
1619
                            "searchable": true,
1620
                            "orderable": true,
1621
                            "render": function(data, type, row, meta) {
1622
                                var title = "";
1623
                                if ( row.biblio.title ) {
1624
                                    title = title.concat('<span class="biblio-title">',row.biblio.title,'</span>');
1625
                                }
1626
                                if ( row.biblio.subtitle ) {
1627
                                    title = title.concat('<span class="biblio-subtitle">',row.biblio.subtitle,'</span>');
1628
                                }
1629
                                if ( row.biblio.medium ) {
1630
                                    title = title.concat('<span class="biblio-medium">',row.biblio.medium,'</span>');
1631
                                }
1632
                                return title;
1633
                            }
1634
                        },
1635
                        {
1636
                            "data": "biblio.author",
1637
                            "title": "Author",
1638
                            "searchable": true,
1639
                            "orderable": true,
1640
                        },
1641
                        {
1642
                            "data": "collection_code",
1643
                            "title": "Collection code",
1644
                            "searchable": true,
1645
                            "orderable": true,
1646
                        },
1647
                        {
1648
                            "data": "item_type",
1649
                            "title": "Item Type",
1650
                            "searchable": false,
1651
                            "orderable": true,
1652
                        },
1653
                        {
1654
                            "data": "callnumber",
1655
                            "title": "Callnumber",
1656
                            "searchable": true,
1657
                            "orderable": true,
1658
                        },
1659
                        {
1660
                            "data": "external_id",
1661
                            "title": "Barcode",
1662
                            "searchable": true,
1663
                            "orderable": true,
1664
                        },
1665
                        {
1666
                            "data": "lost_status:last_seen_date",
1667
                            "title": "Status",
1668
                            "searchable": false,
1669
                            "orderable": true,
1670
                            "render": function(data, type, row, meta) {
1671
                                if ( row.lost_status ) {
1672
                                    return "Lost: " + row.lost_status;
1673
                                }
1674
                                return "";
1675
                            }
1676
                        },
1677
                        {
1678
                            "data": function( row, type, val, meta ) {
1679
                                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';
1680
                                return result;
1681
                            },
1682
                            "title": "Actions",
1683
                            "searchable": false,
1684
                            "orderable": false,
1685
                            "class": "noExport"
1686
                        }
1687
                    ]
1688
                }, bundle_settings, 1);
1689
1690
                $(".tbundle").on("click", ".remove", function(){
1691
                    var bundle_table = $(this).closest('table');
1692
                    var host_itemnumber = bundle_table.data('itemnumber');
1693
                    var component_itemnumber = $(this).data('itemnumber');
1694
                    var unlink_item_url = "/api/v1/items/" + host_itemnumber + "/bundled_items/" + component_itemnumber;
1695
                    $.ajax({
1696
                        type: "DELETE",
1697
                        url: unlink_item_url,
1698
                        success: function(){
1699
                            bundle_table.DataTable({ 'retrieve': true }).draw(false);
1700
                        }
1701
                    });
1702
                });
1703
1704
                return;
1705
            }
1706
1707
            var bundle_changed;
1708
            var bundle_form_active;
1709
            $("#bundleItemsModal").on("shown.bs.modal", function(e){
1710
                var button = $(e.relatedTarget);
1711
                var item_id = button.data('item');
1712
                $("#result").replaceWith('<div id="result"></div>');
1713
                $("#bundleItemsForm").attr('action', '/api/v1/items/' + item_id + '/bundled_items');
1714
                $("#external_id").focus();
1715
                bundle_changed = 0;
1716
                bundle_form_active = item_id;
1717
            });
1718
1719
            $("#bundleItemsForm").submit(function(event) {
1720
1721
                  /* stop form from submitting normally */
1722
                  event.preventDefault();
1723
1724
                  /* get the action attribute from the <form action=""> element */
1725
                  var $form = $(this),
1726
                  url = $form.attr('action');
1727
1728
                  /* Send the data using post with external_id */
1729
                  var posting = $.post({
1730
                      url: url,
1731
                      data: JSON.stringify({ external_id: $('#external_id').val()}),
1732
                      contentType: "application/json; charset=utf-8",
1733
                      dataType: "json"
1734
                  });
1735
1736
                  /* Report the results */
1737
                  posting.done(function(data) {
1738
                      var barcode = $('#external_id').val();
1739
                      $('#result').replaceWith('<div id="result" class="alert alert-success">Success: Added '+barcode+'</div>');
1740
                      $('#external_id').val('').focus();
1741
                      bundle_changed = 1;
1742
                  });
1743
                  posting.fail(function(data) {
1744
                      var barcode = $('#external_id').val();
1745
                      if ( data.status === 409 ) {
1746
                          var response = data.responseJSON;
1747
                          if ( response.key === "PRIMARY" ) {
1748
                              $('#result').replaceWith('<div id="result" class="alert alert-warning">Warning: Item '+barcode+' already attached</div>');
1749
                          } else {
1750
                              $('#result').replaceWith('<div id="result" class="alert alert-danger">Failure: Item '+barcode+' belongs to another bundle</div>');
1751
                          }
1752
                      } else {
1753
                          $('#result').replaceWith('<div id="result" class="alert alert-danger">Failure: Check the logs for details</div>');
1754
                      }
1755
                      $('#external_id').val('').focus();
1756
                  });
1757
            });
1758
1759
            $("#bundleItemsModal").on("hidden.bs.modal", function(e){
1760
                if ( bundle_changed ) {
1761
                    $('#bundle_table_'+bundle_form_active).DataTable({ 'retrieve': true }).ajax.reload();
1762
                }
1763
                bundle_form_active = 0;
1764
                bundle_changed = 0;
1765
            });
1766
1767
            // End bundle handling
1768
            [% END %]
1769
1539
            var table_names = [ 'holdings_table', 'otherholdings_table' ];
1770
            var table_names = [ 'holdings_table', 'otherholdings_table' ];
1540
            var table_settings = [ [% TablesSettings.GetTableSettings('catalogue', 'detail','holdings_table','json') | $raw %], [% TablesSettings.GetTableSettings('catalogue', 'detail','otherholdings_table','json')  | $raw %] ];
1771
            var table_settings = [ [% TablesSettings.GetTableSettings('catalogue', 'detail','holdings_table','json') | $raw %], [% TablesSettings.GetTableSettings('catalogue', 'detail','otherholdings_table','json')  | $raw %] ];
1541
            var has_images = [ "[% itemloop_has_images | html %]", "[% otheritemloop_has_images | html %]" ];
1772
            var has_images = [ "[% itemloop_has_images | html %]", "[% otheritemloop_has_images | html %]" ];
Lines 1551-1556 Note that permanent location is a code, and location may be an authval. Link Here
1551
                    "sDom": 'C<"top pager"ilpfB><"#filter_c">tr<"bottom pager"ip>',
1782
                    "sDom": 'C<"top pager"ilpfB><"#filter_c">tr<"bottom pager"ip>',
1552
                };
1783
                };
1553
                var table = KohaTable( table_name, dt_parameters, table_settings[index], 'with_filters' );
1784
                var table = KohaTable( table_name, dt_parameters, table_settings[index], 'with_filters' );
1785
1786
                [% IF bundlesEnabled %]
1787
                // Add event listener for opening and closing bundle details
1788
                $('#' + table_name + ' tbody').on('click', 'button.details-control', function () {
1789
                    var tr = $(this).closest('tr');
1790
                    var dTable = $(this).closest('table').DataTable({ 'retrieve': true });
1791
1792
                    var itemnumber = tr.data('itemnumber');
1793
                    var row = dTable.row( tr );
1794
1795
                    if ( row.child.isShown() ) {
1796
                        // This row is already open - close it
1797
                        row.child.hide();
1798
                        tr.removeClass('shown');
1799
                    }
1800
                    else {
1801
                        // Open this row
1802
                        createChild(row, itemnumber);
1803
                        tr.addClass('shown');
1804
                    }
1805
                } );
1806
                [% END %]
1554
            });
1807
            });
1555
1808
1556
            [% IF Koha.Preference('AcquisitionDetails') %]
1809
            [% IF Koha.Preference('AcquisitionDetails') %]
Lines 1572-1577 Note that permanent location is a code, and location may be an authval. Link Here
1572
                    "sPaginationType": "full"
1825
                    "sPaginationType": "full"
1573
                }));
1826
                }));
1574
            [% END %]
1827
            [% END %]
1828
1575
        });
1829
        });
1576
1830
1577
        [% IF (found1) %]
1831
        [% IF (found1) %]
1578
- 

Return to bug 28854